feat(passport): 添加手机号授权登录功能支持扫码登录场景
- 在app配置中新增手机号授权登录页面路由 - 修改扫码确认登录逻辑,未注册用户跳转手机号授权登录而非用户页 - 优化扫码登录成功后的跳转逻辑,支持返回扫码确认页面或跳转指定页面 - 新增手机号授权登录组件,实现微信手机号快速授权登录流程 - 手机号授权登录页面包括服务协议和隐私政策勾选及弹窗展示 - 登录成功后处理邀请绑定逻辑,支持扫码场景自动返回确认页 - 配置postcss禁用autoprefixer自动添加浏览器前缀避免冲突
This commit is contained in:
17
.workbuddy/expert-history.json
Normal file
17
.workbuddy/expert-history.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"version": 2,
|
||||
"sessions": {
|
||||
"8485265d66ab43638f13f71cec8f191d": [
|
||||
{
|
||||
"expertId": "SeniorDeveloper",
|
||||
"name": "Will",
|
||||
"profession": "高级开发工程师",
|
||||
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
|
||||
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
|
||||
"usedAt": 1775579277895,
|
||||
"industryId": "all"
|
||||
}
|
||||
]
|
||||
},
|
||||
"lastUpdated": 1775579765675
|
||||
}
|
||||
0
.workbuddy/memory/MEMORY.md
Normal file
0
.workbuddy/memory/MEMORY.md
Normal file
@@ -1,6 +1,8 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
autoprefixer: {
|
||||
remove: true // 禁用 autoprefixer,避免添加浏览器前缀
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ export default {
|
||||
"setting",
|
||||
"agreement",
|
||||
"sms-login",
|
||||
"phone-auth/index",
|
||||
'qr-login/index',
|
||||
'qr-confirm/index',
|
||||
'unified-qr/index'
|
||||
|
||||
350
src/passport/phone-auth/index.tsx
Normal file
350
src/passport/phone-auth/index.tsx
Normal file
@@ -0,0 +1,350 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Taro, { useRouter } from '@tarojs/taro'
|
||||
import { Button, Popup } from '@nutui/nutui-react-taro'
|
||||
import { checkAndHandleInviteRelation, hasPendingInvite } from "@/utils/invite";
|
||||
import { TenantId } from "@/config/app";
|
||||
import { saveStorageByLoginUser, SERVER_API_URL } from "@/utils/server";
|
||||
|
||||
/**
|
||||
* 手机号授权登录页面(扫码登录场景专用)
|
||||
* 使用微信手机号快速验证组件
|
||||
*
|
||||
* 流程:
|
||||
* 1. 用户勾选同意服务协议和隐私政策
|
||||
* 2. 点击微信手机号授权按钮
|
||||
* 3. 微信弹出授权确认框
|
||||
* 4. 获取 code 后调用后端接口完成登录
|
||||
* 5. 登录成功后返回扫码确认页面
|
||||
*/
|
||||
|
||||
// 微信获取手机号回调参数类型
|
||||
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 AgreementPopup = ({ visible, onClose }: { visible: boolean; onClose: () => void }) => (
|
||||
<Popup
|
||||
visible={visible}
|
||||
position="bottom"
|
||||
closeable
|
||||
onClose={onClose}
|
||||
style={{ height: '70%' }}
|
||||
>
|
||||
<div className="p-5 overflow-y-auto" style={{ height: '100%' }}>
|
||||
<h3 className="text-lg font-bold mb-4">服务协议</h3>
|
||||
<div className="text-sm text-gray-600 leading-relaxed">
|
||||
<p className="mb-4">
|
||||
【首部及导言】<br/>
|
||||
欢迎您使用 WebSoft 服务!<br/>
|
||||
为了更好地为您提供服务,请您仔细阅读以下协议条款。
|
||||
</p>
|
||||
<p className="mb-4">
|
||||
一、服务内容<br/>
|
||||
WebSoft 提供基于互联网的相关服务,包括但不限于应用开发平台、资源管理、工单系统等。具体服务内容由我们根据实际情况提供。
|
||||
</p>
|
||||
<p className="mb-4">
|
||||
二、用户注册<br/>
|
||||
您在使用本服务前需要注册账号,注册时应提供真实、准确的个人信息。如信息变更,请及时更新。
|
||||
</p>
|
||||
<p className="mb-4">
|
||||
三、用户使用规范<br/>
|
||||
您不得利用本服务从事任何违法违规活动,不得干扰本服务的正常运行。
|
||||
</p>
|
||||
<p className="mb-4">
|
||||
四、隐私保护<br/>
|
||||
我们承诺保护您的个人隐私,未经您同意,不会向第三方透露您的个人信息。
|
||||
</p>
|
||||
<p>
|
||||
五、免责声明<br/>
|
||||
因不可抗力或第三方原因导致的损失,我们不承担相应责任。
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<Button type="primary" onClick={onClose}>我已阅读</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
);
|
||||
|
||||
const PrivacyPopup = ({ visible, onClose }: { visible: boolean; onClose: () => void }) => (
|
||||
<Popup
|
||||
visible={visible}
|
||||
position="bottom"
|
||||
closeable
|
||||
onClose={onClose}
|
||||
style={{ height: '70%' }}
|
||||
>
|
||||
<div className="p-5 overflow-y-auto" style={{ height: '100%' }}>
|
||||
<h3 className="text-lg font-bold mb-4">隐私政策</h3>
|
||||
<div className="text-sm text-gray-600 leading-relaxed">
|
||||
<p className="mb-4">
|
||||
【概述】<br/>
|
||||
我们非常重视您的个人信息和隐私保护。本政策旨在帮助您了解我们如何收集、使用、存储和保护您的信息。
|
||||
</p>
|
||||
<p className="mb-4">
|
||||
一、信息收集<br/>
|
||||
我们收集的信息包括:您主动提供的信息(如手机号)、使用服务时自动收集的信息(如设备信息、访问日志)。
|
||||
</p>
|
||||
<p className="mb-4">
|
||||
二、信息使用<br/>
|
||||
我们使用收集的信息用于:提供和改进服务、处理您的请求、账户管理和安全保障。
|
||||
</p>
|
||||
<p className="mb-4">
|
||||
三、信息共享<br/>
|
||||
未经您的同意,我们不会与任何第三方共享您的个人信息,法律另有规定除外。
|
||||
</p>
|
||||
<p className="mb-4">
|
||||
四、信息存储<br/>
|
||||
您的信息将存储在安全的服务器上,我们采取合理的安全措施保护您的信息。
|
||||
</p>
|
||||
<p className="mb-4">
|
||||
五、您的权利<br/>
|
||||
您有权访问、更正、删除您的个人信息。如有问题,可联系我们。
|
||||
</p>
|
||||
<p>
|
||||
六、联系我们<br/>
|
||||
如有隐私相关问题,请通过官方渠道联系我们。
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<Button type="primary" onClick={onClose}>我已阅读</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
);
|
||||
|
||||
const PhoneAuthLogin = () => {
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [agreed, setAgreed] = useState<boolean>(false)
|
||||
const [showAgreement, setShowAgreement] = useState<boolean>(false)
|
||||
const [showPrivacy, setShowPrivacy] = useState<boolean>(false)
|
||||
|
||||
// 获取 redirect 参数(扫码登录场景)
|
||||
const redirectUrl = router.params.redirect || '';
|
||||
|
||||
useEffect(() => {
|
||||
Taro.hideTabBar()
|
||||
}, [])
|
||||
|
||||
// 处理微信手机号授权
|
||||
const handleGetPhoneNumber = ({ detail }: GetPhoneNumberEvent) => {
|
||||
const { code, encryptedData, iv, errMsg } = detail
|
||||
|
||||
// 用户拒绝授权
|
||||
if (errMsg && errMsg.includes('fail')) {
|
||||
Taro.showToast({
|
||||
title: '需要授权手机号才能登录',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 未同意协议
|
||||
if (!agreed) {
|
||||
Taro.showToast({
|
||||
title: '请先阅读并同意服务协议和隐私政策',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
Taro.showToast({
|
||||
title: '获取授权信息失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 执行登录
|
||||
handleLogin(code, encryptedData, iv)
|
||||
}
|
||||
|
||||
// 执行登录
|
||||
const handleLogin = async (phoneCode: string, encryptedData?: string, iv?: string) => {
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
const res = await Taro.request<LoginResponse>({
|
||||
url: `${SERVER_API_URL}/wx-login/loginByMpWxPhone`,
|
||||
method: 'POST',
|
||||
data: {
|
||||
code: phoneCode,
|
||||
encryptedData,
|
||||
iv,
|
||||
tenantId: TenantId
|
||||
},
|
||||
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)
|
||||
}
|
||||
|
||||
// 登录成功后,检查是否存在待处理的邀请关系并尝试绑定
|
||||
if (hasPendingInvite()) {
|
||||
try {
|
||||
await checkAndHandleInviteRelation()
|
||||
} catch (e) {
|
||||
console.error('授权登录后处理邀请关系失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
Taro.showToast({
|
||||
title: '授权成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 延迟跳转
|
||||
setTimeout(() => {
|
||||
if (redirectUrl) {
|
||||
// 扫码登录场景:返回扫码确认页面
|
||||
Taro.navigateBack();
|
||||
} else {
|
||||
// 普通场景:跳转到首页
|
||||
Taro.reLaunch({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
}
|
||||
}, 1500)
|
||||
|
||||
} catch (error: any) {
|
||||
Taro.showToast({
|
||||
title: error.message || '授权失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col items-center justify-center min-h-screen bg-gradient-to-b from-blue-50 to-white px-6">
|
||||
{/* Logo/Icon 区域 */}
|
||||
<div className="mb-10">
|
||||
<div className="w-20 h-20 bg-gradient-to-br from-blue-500 to-blue-600 rounded-2xl flex items-center justify-center shadow-lg shadow-blue-200">
|
||||
<svg width="40" height="40" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M20 21V19C20 17.9391 19.5786 16.9217 18.8284 16.1716C18.0783 15.4214 17.0609 15 16 15H8C6.93913 15 5.92172 15.4214 5.17157 16.1716C4.42143 16.9217 4 17.9391 4 19V21" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M12 11C14.2091 11 16 9.20914 16 7C16 4.79086 14.2091 3 12 3C9.79086 3 8 4.79086 8 7C8 9.20914 9.79086 11 12 11Z" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 标题 */}
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-2xl font-bold text-gray-800 mb-2">手机号授权登录</h1>
|
||||
<p className="text-sm text-gray-500">一键授权,快速安全登录</p>
|
||||
</div>
|
||||
|
||||
{/* 微信授权按钮 */}
|
||||
<div className="w-full mb-8">
|
||||
<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"
|
||||
open-type="getPhoneNumber"
|
||||
onGetPhoneNumber={handleGetPhoneNumber}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||
<span>登录中...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="white">
|
||||
<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>
|
||||
<span>微信手机号授权登录</span>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 协议勾选 */}
|
||||
<div className="flex items-start mb-6 px-2">
|
||||
<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 ${
|
||||
agreed ? 'bg-blue-500 border-blue-500' : 'border-gray-300 bg-white hover:border-blue-400'
|
||||
}`}
|
||||
onClick={() => setAgreed(!agreed)}
|
||||
>
|
||||
{agreed && (
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
|
||||
<path d="M2 6L5 9L10 3" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
我已阅读并同意
|
||||
<span
|
||||
className="text-blue-500 cursor-pointer hover:text-blue-600"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowAgreement(true);
|
||||
}}
|
||||
>
|
||||
《服务协议》
|
||||
</span>
|
||||
和
|
||||
<span
|
||||
className="text-blue-500 cursor-pointer hover:text-blue-600"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowPrivacy(true);
|
||||
}}
|
||||
>
|
||||
《隐私政策》
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 底部提示 */}
|
||||
<div className="text-center text-xs text-gray-400 mt-auto pb-8">
|
||||
<p>登录即表示您同意以上协议条款</p>
|
||||
<p className="mt-1">您的手机号将用于账号安全和身份验证</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 服务协议弹窗 */}
|
||||
<AgreementPopup
|
||||
visible={showAgreement}
|
||||
onClose={() => setShowAgreement(false)}
|
||||
/>
|
||||
|
||||
{/* 隐私政策弹窗 */}
|
||||
<PrivacyPopup
|
||||
visible={showPrivacy}
|
||||
onClose={() => setShowPrivacy(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default PhoneAuthLogin;
|
||||
@@ -126,19 +126,19 @@ const QRConfirmPage: React.FC = () => {
|
||||
// 调用确认登录
|
||||
await handleConfirmLogin(loginToken, wxLoginResult.data.user);
|
||||
} else {
|
||||
// 用户未注册,跳转到用户页引导注册
|
||||
console.log('[QRConfirm] 用户未注册,跳转到用户页引导注册');
|
||||
// 用户未注册,跳转到手机号授权登录页面
|
||||
console.log('[QRConfirm] 用户未注册,跳转到手机号授权登录页面');
|
||||
|
||||
Taro.showToast({
|
||||
title: '请先注册/登录小程序',
|
||||
title: '请先授权登录小程序',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
// 跳转到用户中心,在那里可以完成注册和登录
|
||||
Taro.switchTab({
|
||||
url: '/pages/user/user'
|
||||
// 跳转到手机号授权登录页面,登录/注册成功后返回扫码确认页面
|
||||
Taro.navigateTo({
|
||||
url: '/passport/phone-auth/index?redirect=/passport/qr-confirm'
|
||||
});
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import Taro from '@tarojs/taro'
|
||||
import Taro, { useRouter } from '@tarojs/taro'
|
||||
import {Input, Button} from '@nutui/nutui-react-taro'
|
||||
import {loginBySms, sendSmsCaptcha} from "@/api/passport/login";
|
||||
import {LoginParam} from "@/api/passport/login/model";
|
||||
import {checkAndHandleInviteRelation, hasPendingInvite} from "@/utils/invite";
|
||||
|
||||
const SmsLogin = () => {
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [sendingCode, setSendingCode] = useState<boolean>(false)
|
||||
const [countdown, setCountdown] = useState<number>(0)
|
||||
@@ -14,6 +15,9 @@ const SmsLogin = () => {
|
||||
code: ''
|
||||
})
|
||||
|
||||
// 获取 redirect 参数(扫码登录场景)
|
||||
const redirectUrl = router.params.redirect || '';
|
||||
|
||||
const reload = () => {
|
||||
Taro.hideTabBar()
|
||||
}
|
||||
@@ -146,11 +150,29 @@ const SmsLogin = () => {
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 延迟跳转到首页
|
||||
// 延迟跳转
|
||||
setTimeout(() => {
|
||||
Taro.reLaunch({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
if (redirectUrl) {
|
||||
// 扫码登录场景:返回扫码确认页面
|
||||
// 构建带原参数的完整 URL
|
||||
const currentPages = Taro.getCurrentPages();
|
||||
const prevPage = currentPages[currentPages.length - 2];
|
||||
|
||||
if (prevPage && prevPage.route && prevPage.route.includes('qr-confirm')) {
|
||||
// 如果上一页是扫码确认页面,直接返回
|
||||
Taro.navigateBack();
|
||||
} else {
|
||||
// 否则跳转到指定页面
|
||||
Taro.redirectTo({
|
||||
url: redirectUrl
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 普通场景:跳转到首页
|
||||
Taro.reLaunch({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
}
|
||||
}, 1500)
|
||||
|
||||
} catch (error: any) {
|
||||
|
||||
Reference in New Issue
Block a user