Compare commits
2 Commits
ab89163685
...
54fa9e772c
| Author | SHA1 | Date | |
|---|---|---|---|
| 54fa9e772c | |||
| 6dec400b0e |
@@ -13,5 +13,5 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"lastUpdated": 1775579765675
|
"lastUpdated": 1775583343042
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Taro, { useRouter } from '@tarojs/taro'
|
import Taro, { useRouter } from '@tarojs/taro'
|
||||||
import { Button, Popup } from '@nutui/nutui-react-taro'
|
import { Button, Popup } from '@nutui/nutui-react-taro'
|
||||||
import { checkAndHandleInviteRelation, hasPendingInvite } from "@/utils/invite";
|
import { checkAndHandleInviteRelation, hasPendingInvite, getStoredInviteParams } from "@/utils/invite";
|
||||||
import { TenantId } from "@/config/app";
|
import { TenantId } from "@/config/app";
|
||||||
import { saveStorageByLoginUser, SERVER_API_URL } from "@/utils/server";
|
import { saveStorageByLoginUser, SERVER_API_URL } from "@/utils/server";
|
||||||
|
|
||||||
@@ -183,6 +183,10 @@ const PhoneAuthLogin = () => {
|
|||||||
try {
|
try {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
||||||
|
// 获取存储的邀请参数
|
||||||
|
const inviteParams = getStoredInviteParams()
|
||||||
|
const refereeId = inviteParams?.inviter ? parseInt(inviteParams.inviter) : 0
|
||||||
|
|
||||||
const res = await Taro.request<LoginResponse>({
|
const res = await Taro.request<LoginResponse>({
|
||||||
url: `${SERVER_API_URL}/wx-login/loginByMpWxPhone`,
|
url: `${SERVER_API_URL}/wx-login/loginByMpWxPhone`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -190,7 +194,10 @@ const PhoneAuthLogin = () => {
|
|||||||
code: phoneCode,
|
code: phoneCode,
|
||||||
encryptedData,
|
encryptedData,
|
||||||
iv,
|
iv,
|
||||||
tenantId: TenantId
|
tenantId: TenantId,
|
||||||
|
notVerifyPhone: true, // 用户未注册时自动注册
|
||||||
|
refereeId: refereeId, // 推荐人ID
|
||||||
|
sceneType: 'save_referee'
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
'content-type': 'application/json',
|
'content-type': 'application/json',
|
||||||
@@ -266,7 +273,7 @@ const PhoneAuthLogin = () => {
|
|||||||
{/* 微信授权按钮 */}
|
{/* 微信授权按钮 */}
|
||||||
<div className="w-full mb-8">
|
<div className="w-full mb-8">
|
||||||
<Button
|
<Button
|
||||||
className="w-full bg-gradient-to-r from-green-500 to-green-600 text-white font-medium py-4 rounded-xl flex items-center justify-center gap-2 shadow-lg shadow-green-200 active:scale-95 transition-transform disabled:opacity-50 disabled:active:scale-100"
|
className="w-full bg-gradient-to-r from-green-500 to-green-600 text-white font-medium py-4 rounded-xl flex items-center justify-center gap-2 shadow-lg shadow-green-200 transition-transform"
|
||||||
open-type="getPhoneNumber"
|
open-type="getPhoneNumber"
|
||||||
onGetPhoneNumber={handleGetPhoneNumber}
|
onGetPhoneNumber={handleGetPhoneNumber}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
@@ -290,9 +297,7 @@ const PhoneAuthLogin = () => {
|
|||||||
{/* 协议勾选 */}
|
{/* 协议勾选 */}
|
||||||
<div className="flex items-start mb-6 px-2">
|
<div className="flex items-start mb-6 px-2">
|
||||||
<div
|
<div
|
||||||
className={`w-5 h-5 rounded border-2 flex items-center justify-center mr-3 mt-1 flex-shrink-0 cursor-pointer transition-colors ${
|
className={`w-5 h-5 rounded border-2 flex items-center justify-center mr-3 mt-1 flex-shrink-0 cursor-pointer transition-colors`}
|
||||||
agreed ? 'bg-blue-500 border-blue-500' : 'border-gray-300 bg-white hover:border-blue-400'
|
|
||||||
}`}
|
|
||||||
onClick={() => setAgreed(!agreed)}
|
onClick={() => setAgreed(!agreed)}
|
||||||
>
|
>
|
||||||
{agreed && (
|
{agreed && (
|
||||||
@@ -304,7 +309,7 @@ const PhoneAuthLogin = () => {
|
|||||||
<div className="text-sm text-gray-600">
|
<div className="text-sm text-gray-600">
|
||||||
我已阅读并同意
|
我已阅读并同意
|
||||||
<span
|
<span
|
||||||
className="text-blue-500 cursor-pointer hover:text-blue-600"
|
className="text-blue-500 cursor-pointer"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setShowAgreement(true);
|
setShowAgreement(true);
|
||||||
@@ -314,7 +319,7 @@ const PhoneAuthLogin = () => {
|
|||||||
</span>
|
</span>
|
||||||
和
|
和
|
||||||
<span
|
<span
|
||||||
className="text-blue-500 cursor-pointer hover:text-blue-600"
|
className="text-blue-500 cursor-pointer"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setShowPrivacy(true);
|
setShowPrivacy(true);
|
||||||
|
|||||||
@@ -5,45 +5,68 @@ import { Success, Failure, Tips, User } from '@nutui/icons-react-taro';
|
|||||||
import Taro, { useRouter } from '@tarojs/taro';
|
import Taro, { useRouter } from '@tarojs/taro';
|
||||||
import { confirmQRLogin } from '@/api/passport/qr-login';
|
import { confirmQRLogin } from '@/api/passport/qr-login';
|
||||||
import { loginByOpenId } from '@/api/passport/wx-login';
|
import { loginByOpenId } from '@/api/passport/wx-login';
|
||||||
|
import { TenantId } from "@/config/app";
|
||||||
|
import { saveStorageByLoginUser, SERVER_API_URL } from "@/utils/server";
|
||||||
|
import { checkAndHandleInviteRelation, hasPendingInvite, getStoredInviteParams } from "@/utils/invite";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 扫码登录确认页面
|
* 扫码登录确认页面
|
||||||
*
|
*
|
||||||
* 支持两种场景:
|
* 支持两种场景:
|
||||||
* 1. 主动扫码:用户点击按钮调用微信扫码,扫描 PC 端二维码
|
* 1. 主动扫码:用户点击按钮调用微信扫码,扫描 PC 端二维码
|
||||||
* 2. URL 扫码(小程序码):用户扫描小程序码后,微信自动打开此页面
|
* 2. URL 扫码(小程序码):用户扫描小程序码后,微信自动打开此页面
|
||||||
*
|
*
|
||||||
* URL 扫码场景:
|
* URL 扫码场景:
|
||||||
* - 微信「扫普通链接二维码打开小程序」配置的二维码规则:`https://websopy.websoft.top/wx-scan/`
|
* - 微信「扫普通链接二维码打开小程序」配置的二维码规则:`https://websopy.websoft.top/wx-scan/`
|
||||||
* - 扫码后 URL:`https://websopy.websoft.top/wx-scan?token=xxx`
|
* - 扫码后 URL:`https://websopy.websoft.top/wx-scan?token=xxx`
|
||||||
* - 小程序接收到参数后自动确认登录
|
* - 小程序接收到参数后自动确认登录
|
||||||
*
|
*
|
||||||
* 登录流程(2026-04-07 实现):
|
* 登录流程(2026-04-08 更新):
|
||||||
* 1. 用户扫码 → 进入 qr-confirm 页面
|
* 1. 用户扫码 → 进入 qr-confirm 页面
|
||||||
* 2. 页面立即调用 wx.login() 获取 code
|
* 2. 页面立即调用 wx.login() 获取 code
|
||||||
* 3. 用 code 调用 /api/wx-login/loginByOpenId 获取/验证用户身份
|
* 3. 用 code 调用 /api/wx-login/loginByOpenId 获取/验证用户身份
|
||||||
* 4. 如果用户不存在 → 跳转到用户页引导注册
|
* 4. 如果用户不存在 → 显示微信手机号授权按钮(一键注册登录)
|
||||||
* 5. 如果用户存在 → 自动调用 confirmQRLogin 确认登录
|
* 5. 如果用户存在 → 自动调用 confirmQRLogin 确认登录
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// 微信获取手机号回调参数类型
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
const QRConfirmPage: React.FC = () => {
|
const QRConfirmPage: React.FC = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
// 移除 useUser 依赖,改用 wx.login() + loginByOpenId 方式验证用户身份
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [confirmed, setConfirmed] = useState(false);
|
const [confirmed, setConfirmed] = useState(false);
|
||||||
const [error, setError] = useState<string>('');
|
const [error, setError] = useState<string>('');
|
||||||
const [token, setToken] = useState<string>('');
|
const [token, setToken] = useState<string>('');
|
||||||
const [loginMethod, setLoginMethod] = useState<'scan' | 'url'>('url');
|
const [loginMethod, setLoginMethod] = useState<'scan' | 'url'>('url');
|
||||||
const [userInfo, setUserInfo] = useState<any>(null);
|
const [userInfo, setUserInfo] = useState<any>(null);
|
||||||
|
const [needAuth, setNeedAuth] = useState(false); // 是否需要手机号授权
|
||||||
|
const [authLoading, setAuthLoading] = useState(false); // 授权中状态
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 从 URL 参数中获取 token
|
// 从 URL 参数中获取 token
|
||||||
const params = router.params;
|
const params = router.params;
|
||||||
|
|
||||||
// 兼容多种参数名
|
// 兼容多种参数名
|
||||||
// 1. 小程序码场景:?scene=xxx(微信会将 scene 参数透传到小程序)
|
|
||||||
// 2. 直接参数:?token=xxx
|
|
||||||
// 3. URL 编码参数:?q=xxx(扫普通链接二维码场景)
|
|
||||||
// 4. 旧版参数:?qrCodeKey=xxx
|
|
||||||
let loginToken = params.scene || params.token || params.qrCodeKey || '';
|
let loginToken = params.scene || params.token || params.qrCodeKey || '';
|
||||||
|
|
||||||
// 如果是 q 参数(URL 编码的完整 URL),需要解析
|
// 如果是 q 参数(URL 编码的完整 URL),需要解析
|
||||||
@@ -51,17 +74,15 @@ const QRConfirmPage: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
const decodedUrl = decodeURIComponent(params.q);
|
const decodedUrl = decodeURIComponent(params.q);
|
||||||
console.log('[QRConfirm] 解码后的 URL:', decodedUrl);
|
console.log('[QRConfirm] 解码后的 URL:', decodedUrl);
|
||||||
|
|
||||||
// 解析 token
|
|
||||||
const url = new URL(decodedUrl);
|
const url = new URL(decodedUrl);
|
||||||
loginToken = url.searchParams.get('token') ||
|
loginToken = url.searchParams.get('token') ||
|
||||||
url.searchParams.get('qrCodeKey') ||
|
url.searchParams.get('qrCodeKey') ||
|
||||||
'';
|
'';
|
||||||
|
|
||||||
setLoginMethod('url');
|
setLoginMethod('url');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[QRConfirm] 解析 q 参数失败:', e);
|
console.error('[QRConfirm] 解析 q 参数失败:', e);
|
||||||
// 尝试直接使用 q 作为 token
|
|
||||||
loginToken = decodeURIComponent(params.q);
|
loginToken = decodeURIComponent(params.q);
|
||||||
setLoginMethod('url');
|
setLoginMethod('url');
|
||||||
}
|
}
|
||||||
@@ -72,9 +93,8 @@ const QRConfirmPage: React.FC = () => {
|
|||||||
if (loginToken) {
|
if (loginToken) {
|
||||||
setToken(loginToken);
|
setToken(loginToken);
|
||||||
console.log('[QRConfirm] 获取到 token:', loginToken);
|
console.log('[QRConfirm] 获取到 token:', loginToken);
|
||||||
|
|
||||||
// 扫码场景:自动确认登录
|
// 扫码场景:自动确认登录
|
||||||
// scene 参数说明是扫描小程序码进来的,token 参数说明是扫码跳转过来的
|
|
||||||
if (params.scene || params.token || params.qrCodeKey || params.q) {
|
if (params.scene || params.token || params.qrCodeKey || params.q) {
|
||||||
console.log('[QRConfirm] 检测到扫码参数,自动确认登录');
|
console.log('[QRConfirm] 检测到扫码参数,自动确认登录');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -88,99 +108,163 @@ const QRConfirmPage: React.FC = () => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 自动确认登录(URL 扫码场景)
|
* 自动确认登录(URL 扫码场景)
|
||||||
*
|
|
||||||
* 新的登录流程(2026-04-07):
|
|
||||||
* 1. 调用 wx.login() 获取 code
|
|
||||||
* 2. 用 code 调用后端 loginByOpenId 接口验证用户身份
|
|
||||||
* 3. 用户存在 → 调用 confirmQRLogin 确认登录
|
|
||||||
* 4. 用户不存在 → 跳转到用户页引导注册
|
|
||||||
*/
|
*/
|
||||||
const handleAutoConfirm = async (loginToken: string) => {
|
const handleAutoConfirm = async (loginToken: string) => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
// 1. 调用微信登录获取 code
|
// 1. 调用微信登录获取 code
|
||||||
console.log('[QRConfirm] 调用 wx.login() 获取 code...');
|
console.log('[QRConfirm] 调用 wx.login() 获取 code...');
|
||||||
const loginResult = await Taro.login();
|
const loginResult = await Taro.login();
|
||||||
|
|
||||||
if (!loginResult.code) {
|
if (!loginResult.code) {
|
||||||
throw new Error('获取微信登录凭证失败');
|
throw new Error('获取微信登录凭证失败');
|
||||||
}
|
}
|
||||||
console.log('[QRConfirm] 获取到 code:', loginResult.code);
|
console.log('[QRConfirm] 获取到 code:', loginResult.code);
|
||||||
|
|
||||||
// 2. 用 code 调用后端接口验证用户身份
|
// 2. 用 code 调用后端接口验证用户身份
|
||||||
console.log('[QRConfirm] 调用后端 loginByOpenId...');
|
console.log('[QRConfirm] 调用后端 loginByOpenId...');
|
||||||
const wxLoginResult = await loginByOpenId({
|
const wxLoginResult = await loginByOpenId({
|
||||||
code: loginResult.code,
|
code: loginResult.code,
|
||||||
tenantId: 10398 // 使用固定租户ID
|
tenantId: 5
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('[QRConfirm] loginByOpenId 结果:', wxLoginResult);
|
console.log('[QRConfirm] loginByOpenId 结果:', wxLoginResult);
|
||||||
|
|
||||||
// 3. 判断用户是否存在
|
// 3. 判断用户是否存在
|
||||||
if (wxLoginResult.success && wxLoginResult.data) {
|
if (wxLoginResult.success && wxLoginResult.data) {
|
||||||
// 用户已注册,保存用户信息并继续确认登录
|
// 用户已注册,保存用户信息并继续确认登录
|
||||||
console.log('[QRConfirm] 用户已注册,开始确认登录...');
|
console.log('[QRConfirm] 用户已注册,开始确认登录...');
|
||||||
setUserInfo(wxLoginResult.data.user);
|
setUserInfo(wxLoginResult.data.user);
|
||||||
|
|
||||||
// 调用确认登录
|
// 调用确认登录
|
||||||
await handleConfirmLogin(loginToken, wxLoginResult.data.user);
|
await handleConfirmLogin(loginToken, wxLoginResult.data.user);
|
||||||
} else {
|
} else {
|
||||||
// 用户未注册,跳转到手机号授权登录页面
|
// 用户未注册,显示手机号授权界面
|
||||||
console.log('[QRConfirm] 用户未注册,跳转到手机号授权登录页面');
|
console.log('[QRConfirm] 用户未注册,显示手机号授权界面');
|
||||||
|
setNeedAuth(true);
|
||||||
Taro.showToast({
|
setLoading(false);
|
||||||
title: '请先授权登录小程序',
|
|
||||||
icon: 'none',
|
|
||||||
duration: 2000
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
// 跳转到手机号授权登录页面,登录/注册成功后返回扫码确认页面
|
|
||||||
Taro.navigateTo({
|
|
||||||
url: '/passport/phone-auth/index?redirect=/passport/qr-confirm'
|
|
||||||
});
|
|
||||||
}, 2000);
|
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('[QRConfirm] 自动确认登录失败:', err);
|
console.error('[QRConfirm] 自动确认登录失败:', err);
|
||||||
setError(err.message || '自动确认登录失败');
|
setError(err.message || '自动确认登录失败');
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理微信手机号授权
|
||||||
|
*/
|
||||||
|
const handleGetPhoneNumber = async ({ detail }: GetPhoneNumberEvent) => {
|
||||||
|
const { code, encryptedData, iv, errMsg } = detail;
|
||||||
|
|
||||||
|
// 用户拒绝授权
|
||||||
|
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: TenantId,
|
||||||
|
notVerifyPhone: true,
|
||||||
|
refereeId: refereeId,
|
||||||
|
sceneType: 'save_referee'
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
'TenantId': TenantId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
setUserInfo(res.data.data.user);
|
||||||
|
|
||||||
|
// 处理邀请关系
|
||||||
|
if (hasPendingInvite()) {
|
||||||
|
try {
|
||||||
|
await checkAndHandleInviteRelation();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('授权登录后处理邀请关系失败:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Taro.showToast({
|
||||||
|
title: '授权成功,正在确认登录...',
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 延迟后自动确认扫码登录
|
||||||
|
setTimeout(() => {
|
||||||
|
handleConfirmLogin(token, res.data.data.user);
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
Taro.showToast({
|
||||||
|
title: error.message || '授权失败',
|
||||||
|
icon: 'error'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setAuthLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 确认登录
|
* 确认登录
|
||||||
* @param loginToken - 可选的登录token,默认使用页面token
|
|
||||||
* @param wxUserInfo - 微信登录获取的用户信息(可选)
|
|
||||||
*/
|
*/
|
||||||
const handleConfirmLogin = async (loginToken?: string, wxUserInfo?: any) => {
|
const handleConfirmLogin = async (loginToken?: string, wxUserInfo?: any) => {
|
||||||
const confirmToken = loginToken || token;
|
const confirmToken = loginToken || token;
|
||||||
|
|
||||||
if (!confirmToken) {
|
if (!confirmToken) {
|
||||||
setError('缺少登录token');
|
setError('缺少登录token');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 优先使用传入的用户信息,否则尝试从本地存储获取
|
|
||||||
const currentUser = wxUserInfo || userInfo;
|
const currentUser = wxUserInfo || userInfo;
|
||||||
|
|
||||||
if (!currentUser?.userId) {
|
if (!currentUser?.userId) {
|
||||||
// 没有用户信息,尝试从本地存储获取
|
|
||||||
const userId = Taro.getStorageSync('UserId');
|
const userId = Taro.getStorageSync('UserId');
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
setError('请先登录小程序');
|
setError('请先登录小程序');
|
||||||
Taro.showToast({
|
setNeedAuth(true);
|
||||||
title: '请先登录小程序',
|
|
||||||
icon: 'none'
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
Taro.switchTab({
|
|
||||||
url: '/pages/user/user'
|
|
||||||
});
|
|
||||||
}, 1500);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
currentUser && (currentUser.userId = Number(userId));
|
currentUser && (currentUser.userId = Number(userId));
|
||||||
@@ -200,25 +284,22 @@ const QRConfirmPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 根据 status 判断成功:confirmed 表示登录成功
|
|
||||||
const isConfirmed = result.status === 'confirmed' || result.success === true;
|
const isConfirmed = result.status === 'confirmed' || result.success === true;
|
||||||
|
|
||||||
if (isConfirmed) {
|
if (isConfirmed) {
|
||||||
setConfirmed(true);
|
setConfirmed(true);
|
||||||
|
setNeedAuth(false);
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
title: result.successMessage || result.message || '登录确认成功',
|
title: result.successMessage || result.message || '登录确认成功',
|
||||||
icon: 'success',
|
icon: 'success',
|
||||||
duration: 2000
|
duration: 2000
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3秒后自动关闭或返回
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// 尝试返回上一页,如果没有则关闭
|
|
||||||
const pages = Taro.getCurrentPages();
|
const pages = Taro.getCurrentPages();
|
||||||
if (pages.length > 1) {
|
if (pages.length > 1) {
|
||||||
Taro.navigateBack();
|
Taro.navigateBack();
|
||||||
} else {
|
} else {
|
||||||
// 小程序场景下,提示用户回到 PC 端
|
|
||||||
Taro.showModal({
|
Taro.showModal({
|
||||||
title: '登录成功',
|
title: '登录成功',
|
||||||
content: '请回到电脑端刷新页面',
|
content: '请回到电脑端刷新页面',
|
||||||
@@ -228,7 +309,6 @@ const QRConfirmPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, 3000);
|
}, 3000);
|
||||||
} else if (result.status === 'bind_phone' || result.needBindPhone) {
|
} else if (result.status === 'bind_phone' || result.needBindPhone) {
|
||||||
// 需要绑定手机号
|
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
title: '请先绑定手机号',
|
title: '请先绑定手机号',
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
@@ -274,7 +354,10 @@ const QRConfirmPage: React.FC = () => {
|
|||||||
const handleRetry = () => {
|
const handleRetry = () => {
|
||||||
setError('');
|
setError('');
|
||||||
setConfirmed(false);
|
setConfirmed(false);
|
||||||
handleConfirmLogin();
|
setNeedAuth(false);
|
||||||
|
if (token) {
|
||||||
|
handleAutoConfirm(token);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -284,34 +367,32 @@ const QRConfirmPage: React.FC = () => {
|
|||||||
Taro.scanCode({
|
Taro.scanCode({
|
||||||
success: async (res) => {
|
success: async (res) => {
|
||||||
console.log('[QRConfirm] 扫码成功:', res);
|
console.log('[QRConfirm] 扫码成功:', res);
|
||||||
|
|
||||||
// 解析二维码内容
|
|
||||||
let scanToken = '';
|
let scanToken = '';
|
||||||
const qrContent = res.result;
|
const qrContent = res.result;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 尝试解析 URL
|
|
||||||
if (qrContent.includes('http')) {
|
if (qrContent.includes('http')) {
|
||||||
const url = new URL(qrContent);
|
const url = new URL(qrContent);
|
||||||
scanToken = url.searchParams.get('token') ||
|
scanToken = url.searchParams.get('token') ||
|
||||||
url.searchParams.get('qrCodeKey') ||
|
url.searchParams.get('qrCodeKey') ||
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 尝试解析 JSON
|
|
||||||
if (!scanToken && qrContent.startsWith('{')) {
|
if (!scanToken && qrContent.startsWith('{')) {
|
||||||
const parsed = JSON.parse(qrContent);
|
const parsed = JSON.parse(qrContent);
|
||||||
scanToken = parsed.token || parsed.qrCodeKey || '';
|
scanToken = parsed.token || parsed.qrCodeKey || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 直接作为 token
|
|
||||||
if (!scanToken && qrContent.length >= 32) {
|
if (!scanToken && qrContent.length >= 32) {
|
||||||
scanToken = qrContent;
|
scanToken = qrContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scanToken) {
|
if (scanToken) {
|
||||||
setToken(scanToken);
|
setToken(scanToken);
|
||||||
setLoginMethod('scan');
|
setLoginMethod('scan');
|
||||||
|
setNeedAuth(false);
|
||||||
|
setError('');
|
||||||
handleConfirmLogin(scanToken);
|
handleConfirmLogin(scanToken);
|
||||||
} else {
|
} else {
|
||||||
setError('无效的二维码内容');
|
setError('无效的二维码内容');
|
||||||
@@ -330,46 +411,56 @@ const QRConfirmPage: React.FC = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 渲染状态:加载中
|
// 渲染状态图标
|
||||||
const renderLoading = () => (
|
const renderStatusIcon = () => {
|
||||||
<View className="mb-6">
|
if (loading || authLoading) return (
|
||||||
<View className="w-16 h-16 mx-auto flex items-center justify-center">
|
<View className="mb-6">
|
||||||
<Loading className="text-blue-500" />
|
<View className="w-16 h-16 mx-auto flex items-center justify-center">
|
||||||
|
<Loading className="text-blue-500" />
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
);
|
||||||
);
|
|
||||||
|
|
||||||
// 渲染状态:成功
|
if (confirmed) return (
|
||||||
const renderSuccess = () => (
|
<View className="mb-6">
|
||||||
<View className="mb-6">
|
<View className="w-16 h-16 mx-auto bg-green-100 rounded-full flex items-center justify-center">
|
||||||
<View className="w-16 h-16 mx-auto bg-green-100 rounded-full flex items-center justify-center">
|
<Success className="text-green-500" size="32" />
|
||||||
<Success className="text-green-500" size="32" />
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
);
|
||||||
);
|
|
||||||
|
|
||||||
// 渲染状态:错误
|
if (error && !needAuth) return (
|
||||||
const renderError = () => (
|
<View className="mb-6">
|
||||||
<View className="mb-6">
|
<View className="w-16 h-16 mx-auto bg-red-100 rounded-full flex items-center justify-center">
|
||||||
<View className="w-16 h-16 mx-auto bg-red-100 rounded-full flex items-center justify-center">
|
<Failure className="text-red-500" size="32" />
|
||||||
<Failure className="text-red-500" size="32" />
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
);
|
||||||
);
|
|
||||||
|
|
||||||
// 渲染状态:初始(用户未扫码)
|
if (needAuth) return (
|
||||||
const renderInitial = () => (
|
<View className="mb-6">
|
||||||
<View className="mb-6">
|
<View className="w-16 h-16 mx-auto bg-green-100 rounded-full flex items-center justify-center">
|
||||||
<View className="w-16 h-16 mx-auto bg-blue-100 rounded-full flex items-center justify-center">
|
<svg width="32" height="32" viewBox="0 0 24 24" fill="#22c55e">
|
||||||
<User size="32" className="text-blue-500" />
|
<path d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 0 0 .167-.054l1.903-1.114a.864.864 0 0 1 .717-.098 10.16 10.16 0 0 0 2.837.403c.276 0 .543-.027.811-.05-.857-2.578.157-4.972 1.932-6.446 1.703-1.415 3.882-1.98 5.853-1.838-.576-3.583-4.196-6.348-8.596-6.348zM5.785 5.991c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178A1.17 1.17 0 0 1 4.623 7.17c0-.651.52-1.18 1.162-1.18zm5.813 0c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178 1.17 1.17 0 0 1-1.162-1.178c0-.651.52-1.18 1.162-1.18zm5.34 2.867c-1.797-.052-3.746.512-5.28 1.786-1.72 1.428-2.687 3.72-1.78 6.22.942 2.453 3.666 4.229 6.884 4.229.826 0 1.622-.12 2.361-.336a.722.722 0 0 1 .598.082l1.584.926a.272.272 0 0 0 .14.047c.134 0 .24-.111.24-.247 0-.06-.023-.12-.038-.177l-.327-1.233a.582.582 0 0 1-.023-.156.49.49 0 0 1 .201-.398C23.024 18.48 24 16.82 24 14.98c0-3.21-2.931-5.837-6.656-6.088V8.89c-.135-.01-.269-.03-.407-.03zm-2.53 3.274c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.97-.982zm4.844 0c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.969-.982z"/>
|
||||||
|
</svg>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
);
|
||||||
);
|
|
||||||
|
return (
|
||||||
|
<View className="mb-6">
|
||||||
|
<View className="w-16 h-16 mx-auto bg-blue-100 rounded-full flex items-center justify-center">
|
||||||
|
<User size="32" className="text-blue-500" />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// 获取标题
|
// 获取标题
|
||||||
const getTitle = () => {
|
const getTitle = () => {
|
||||||
if (loading) return '正在确认登录...';
|
if (loading || authLoading) return '正在处理...';
|
||||||
if (confirmed) return '登录确认成功';
|
if (confirmed) return '登录确认成功';
|
||||||
|
if (needAuth) return '首次登录授权';
|
||||||
if (error) return '登录确认失败';
|
if (error) return '登录确认失败';
|
||||||
return loginMethod === 'url' ? '扫码登录确认' : '确认登录';
|
return loginMethod === 'url' ? '扫码登录确认' : '确认登录';
|
||||||
};
|
};
|
||||||
@@ -377,7 +468,9 @@ const QRConfirmPage: React.FC = () => {
|
|||||||
// 获取描述
|
// 获取描述
|
||||||
const getDescription = () => {
|
const getDescription = () => {
|
||||||
if (loading) return '请稍候,正在为您确认登录';
|
if (loading) return '请稍候,正在为您确认登录';
|
||||||
|
if (authLoading) return '正在授权登录...';
|
||||||
if (confirmed) return '您已成功确认登录,网页端将自动登录';
|
if (confirmed) return '您已成功确认登录,网页端将自动登录';
|
||||||
|
if (needAuth) return '检测到您是首次使用,请授权手机号完成注册并登录';
|
||||||
if (error) return error;
|
if (error) return error;
|
||||||
if (loginMethod === 'url') {
|
if (loginMethod === 'url') {
|
||||||
return '检测到登录请求,是否确认登录?';
|
return '检测到登录请求,是否确认登录?';
|
||||||
@@ -386,6 +479,130 @@ const QRConfirmPage: React.FC = () => {
|
|||||||
return `确认使用 ${displayName} 登录网页端?`;
|
return `确认使用 ${displayName} 登录网页端?`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 渲染操作按钮
|
||||||
|
const renderActions = () => {
|
||||||
|
// 需要授权登录
|
||||||
|
if (needAuth) {
|
||||||
|
return (
|
||||||
|
<View className="space-y-3">
|
||||||
|
<Button
|
||||||
|
type="success"
|
||||||
|
size="large"
|
||||||
|
className="w-full rounded-xl"
|
||||||
|
open-type="getPhoneNumber"
|
||||||
|
onGetPhoneNumber={handleGetPhoneNumber}
|
||||||
|
disabled={authLoading}
|
||||||
|
>
|
||||||
|
{authLoading ? '授权中...' : '微信手机号一键授权'}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="default"
|
||||||
|
size="large"
|
||||||
|
onClick={handleCancel}
|
||||||
|
className="w-full rounded-xl"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type="default"
|
||||||
|
size="large"
|
||||||
|
disabled
|
||||||
|
className="w-full rounded-xl"
|
||||||
|
>
|
||||||
|
确认中...
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (confirmed) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type="success"
|
||||||
|
size="large"
|
||||||
|
onClick={handleCancel}
|
||||||
|
className="w-full rounded-xl"
|
||||||
|
>
|
||||||
|
完成
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<View className="space-y-2">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
onClick={handleRetry}
|
||||||
|
className="w-full rounded-xl"
|
||||||
|
>
|
||||||
|
重试
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="default"
|
||||||
|
size="large"
|
||||||
|
onClick={handleScan}
|
||||||
|
className="w-full rounded-xl"
|
||||||
|
>
|
||||||
|
扫码其他二维码
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="default"
|
||||||
|
size="small"
|
||||||
|
onClick={handleCancel}
|
||||||
|
className="w-full rounded-xl"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loginMethod === 'scan') {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
onClick={handleManualConfirm}
|
||||||
|
className="w-full mb-2 rounded-xl"
|
||||||
|
disabled={!token}
|
||||||
|
>
|
||||||
|
确认登录
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="default"
|
||||||
|
size="large"
|
||||||
|
onClick={handleCancel}
|
||||||
|
className="w-full rounded-xl"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
onClick={handleManualConfirm}
|
||||||
|
className="w-full rounded-xl"
|
||||||
|
>
|
||||||
|
确认登录
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="qr-confirm-page min-h-screen bg-gradient-to-b from-blue-50 to-white">
|
<View className="qr-confirm-page min-h-screen bg-gradient-to-b from-blue-50 to-white">
|
||||||
<View className="p-4">
|
<View className="p-4">
|
||||||
@@ -401,7 +618,7 @@ const QRConfirmPage: React.FC = () => {
|
|||||||
<Card className="bg-white rounded-2xl shadow-xl -mt-4">
|
<Card className="bg-white rounded-2xl shadow-xl -mt-4">
|
||||||
<View className="p-6 text-center">
|
<View className="p-6 text-center">
|
||||||
{/* 状态图标 */}
|
{/* 状态图标 */}
|
||||||
{loading ? renderLoading() : confirmed ? renderSuccess() : error ? renderError() : renderInitial()}
|
{renderStatusIcon()}
|
||||||
|
|
||||||
{/* 标题 */}
|
{/* 标题 */}
|
||||||
<Text className="text-xl font-bold text-gray-800 mb-2 block">
|
<Text className="text-xl font-bold text-gray-800 mb-2 block">
|
||||||
@@ -414,11 +631,11 @@ const QRConfirmPage: React.FC = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{/* 用户信息 */}
|
{/* 用户信息 */}
|
||||||
{!loading && !confirmed && !error && userInfo && (
|
{!loading && !confirmed && !error && !needAuth && userInfo && (
|
||||||
<View className="bg-gray-50 rounded-xl p-4 mb-6">
|
<View className="bg-gray-50 rounded-xl p-4 mb-6">
|
||||||
<View className="flex items-center justify-center">
|
<View className="flex items-center justify-center">
|
||||||
{userInfo.avatar ? (
|
{userInfo.avatar ? (
|
||||||
<View
|
<View
|
||||||
className="w-12 h-12 rounded-full bg-blue-100 mr-3 overflow-hidden"
|
className="w-12 h-12 rounded-full bg-blue-100 mr-3 overflow-hidden"
|
||||||
style={{ backgroundImage: `url(${userInfo.avatar})`, backgroundSize: 'cover' }}
|
style={{ backgroundImage: `url(${userInfo.avatar})`, backgroundSize: 'cover' }}
|
||||||
/>
|
/>
|
||||||
@@ -440,7 +657,7 @@ const QRConfirmPage: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Token 信息 */}
|
{/* Token 信息 */}
|
||||||
{token && !loading && !confirmed && (
|
{token && !loading && !confirmed && !needAuth && (
|
||||||
<View className="bg-blue-50 rounded-lg p-3 mb-4">
|
<View className="bg-blue-50 rounded-lg p-3 mb-4">
|
||||||
<Text className="text-xs text-blue-600">
|
<Text className="text-xs text-blue-600">
|
||||||
登录令牌:{token.substring(0, 20)}...{token.substring(token.length - 10)}
|
登录令牌:{token.substring(0, 20)}...{token.substring(token.length - 10)}
|
||||||
@@ -449,105 +666,28 @@ const QRConfirmPage: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 操作按钮 */}
|
{/* 操作按钮 */}
|
||||||
<View className="space-y-3">
|
{renderActions()}
|
||||||
{loading ? (
|
|
||||||
<Button
|
|
||||||
type="default"
|
|
||||||
size="large"
|
|
||||||
disabled
|
|
||||||
className="w-full rounded-xl"
|
|
||||||
>
|
|
||||||
确认中...
|
|
||||||
</Button>
|
|
||||||
) : confirmed ? (
|
|
||||||
<Button
|
|
||||||
type="success"
|
|
||||||
size="large"
|
|
||||||
onClick={handleCancel}
|
|
||||||
className="w-full rounded-xl"
|
|
||||||
>
|
|
||||||
完成
|
|
||||||
</Button>
|
|
||||||
) : error ? (
|
|
||||||
<View className="space-y-2">
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
size="large"
|
|
||||||
onClick={handleRetry}
|
|
||||||
className="w-full rounded-xl"
|
|
||||||
>
|
|
||||||
重试
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="default"
|
|
||||||
size="large"
|
|
||||||
onClick={handleScan}
|
|
||||||
className="w-full rounded-xl"
|
|
||||||
>
|
|
||||||
扫码其他二维码
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="default"
|
|
||||||
size="small"
|
|
||||||
onClick={handleCancel}
|
|
||||||
className="w-full rounded-xl"
|
|
||||||
fill="none"
|
|
||||||
>
|
|
||||||
取消
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
) : loginMethod === 'scan' ? (
|
|
||||||
<View>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
size="large"
|
|
||||||
onClick={handleManualConfirm}
|
|
||||||
className="w-full mb-2 rounded-xl"
|
|
||||||
disabled={!token}
|
|
||||||
>
|
|
||||||
确认登录
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="default"
|
|
||||||
size="large"
|
|
||||||
onClick={handleCancel}
|
|
||||||
className="w-full rounded-xl"
|
|
||||||
fill="none"
|
|
||||||
>
|
|
||||||
取消
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
// URL 扫码场景:自动确认中
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
size="large"
|
|
||||||
onClick={handleManualConfirm}
|
|
||||||
className="w-full rounded-xl"
|
|
||||||
>
|
|
||||||
确认登录
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 安全提示 */}
|
{/* 安全提示 */}
|
||||||
<Card className="bg-yellow-50 border border-yellow-200 rounded-xl mt-4">
|
{!needAuth && (
|
||||||
<View className="p-4">
|
<Card className="bg-yellow-50 border border-yellow-200 rounded-xl mt-4">
|
||||||
<View className="flex items-start">
|
<View className="p-4">
|
||||||
<Tips className="text-yellow-600 mr-2 mt-1" size="16" />
|
<View className="flex items-start">
|
||||||
<View>
|
<Tips className="text-yellow-600 mr-2 mt-1" size="16" />
|
||||||
<Text className="text-sm font-medium text-yellow-800 mb-1 block">
|
<View>
|
||||||
安全提示
|
<Text className="text-sm font-medium text-yellow-800 mb-1 block">
|
||||||
</Text>
|
安全提示
|
||||||
<Text className="text-xs text-yellow-700 block">
|
</Text>
|
||||||
请确认这是您本人的登录操作。如果不是,请点击取消并检查账户安全。
|
<Text className="text-xs text-yellow-700 block">
|
||||||
</Text>
|
请确认这是您本人的登录操作。如果不是,请点击取消并检查账户安全。
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</Card>
|
||||||
</Card>
|
)}
|
||||||
|
|
||||||
{/* 底部说明 */}
|
{/* 底部说明 */}
|
||||||
<View className="text-center mt-6 pb-8">
|
<View className="text-center mt-6 pb-8">
|
||||||
|
|||||||
Reference in New Issue
Block a user