forked from gxwebsoft/mp-10550
feat(auth): 添加统一认证工具和优化登录流程
- 新增 auth 工具模块,包含 isLoggedIn、goToRegister、ensureLoggedIn 方法 - 将硬编码的服务器URL更新为 glt-server 域名 - 重构多个页面的登录检查逻辑,使用统一的认证工具 - 在用户注册/登录流程中集成邀请关系处理 - 更新注册页面配置和实现,支持跳转参数传递 - 优化分销商二维码页面的加载状态和错误处理 - 在水票使用页面添加无票时的购买引导 - 统一文件上传和API请求的服务器地址 - 添加加密库类型定义文件
This commit is contained in:
295
src/passport/register.tsx
Normal file
295
src/passport/register.tsx
Normal file
@@ -0,0 +1,295 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { Button, Radio } from '@nutui/nutui-react-taro'
|
||||
import { TenantId } from '@/config/app'
|
||||
import { getUserInfo, getWxOpenId } from '@/api/layout'
|
||||
import { saveStorageByLoginUser } from '@/utils/server'
|
||||
import {
|
||||
getStoredInviteParams,
|
||||
parseInviteParams,
|
||||
saveInviteParams,
|
||||
trackInviteSource,
|
||||
checkAndHandleInviteRelation,
|
||||
} from '@/utils/invite'
|
||||
|
||||
interface GetPhoneNumberDetail {
|
||||
code?: string
|
||||
encryptedData?: string
|
||||
iv?: string
|
||||
errMsg?: string
|
||||
}
|
||||
|
||||
interface GetPhoneNumberEvent {
|
||||
detail: GetPhoneNumberDetail
|
||||
}
|
||||
|
||||
interface LoginResponse {
|
||||
data: {
|
||||
code?: number
|
||||
message?: string
|
||||
data?: {
|
||||
access_token: string
|
||||
user: any
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getWeappLoginCode(): Promise<string | undefined> {
|
||||
try {
|
||||
const res = await new Promise<{ code?: string }>((resolve, reject) => {
|
||||
Taro.login({
|
||||
success: (r) => resolve(r as any),
|
||||
fail: (e) => reject(e),
|
||||
})
|
||||
})
|
||||
return res?.code
|
||||
} catch (_e) {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureWxOpenIdSaved(opts: { user?: any; wxLoginCode?: string }) {
|
||||
// JSAPI 微信支付必须有 openid;注册/登录后立刻补齐,避免后续创建支付单失败。
|
||||
try {
|
||||
if (Taro.getEnv() !== Taro.ENV_TYPE.WEAPP) return
|
||||
} catch (_e) {
|
||||
if (process.env.TARO_ENV !== 'weapp') return
|
||||
}
|
||||
|
||||
if (opts.user?.openid) return
|
||||
|
||||
const code = opts.wxLoginCode || (await getWeappLoginCode())
|
||||
if (!code) return
|
||||
|
||||
// 该接口一般会在服务端把 openid 绑定到当前登录用户;返回值并不一定包含 openid。
|
||||
await getWxOpenId({ code })
|
||||
|
||||
// 同步本地 User(让后续页面/逻辑能直接读到 openid)
|
||||
try {
|
||||
const fresh = await getUserInfo()
|
||||
if (fresh) Taro.setStorageSync('User', fresh)
|
||||
} catch (_e) {
|
||||
// ignore: openid 已在服务端绑定,本地不同步也不影响后端创建支付订单
|
||||
}
|
||||
}
|
||||
|
||||
function safeDecodeMaybeEncoded(input?: string): string {
|
||||
if (!input) return ''
|
||||
try {
|
||||
// Taro 路由参数通常是 URL 编码过的字符串
|
||||
return decodeURIComponent(input)
|
||||
} catch (_e) {
|
||||
return input
|
||||
}
|
||||
}
|
||||
|
||||
function isTabBarUrl(url: string) {
|
||||
const pure = url.split('?')[0]
|
||||
return (
|
||||
pure === '/pages/index/index' ||
|
||||
pure === '/pages/cart/cart' ||
|
||||
pure === '/pages/user/user' ||
|
||||
pure === '/pages/category/index'
|
||||
)
|
||||
}
|
||||
|
||||
const Register = () => {
|
||||
const [isAgree, setIsAgree] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
// 短信验证码登录仅在非微信小程序端展示
|
||||
const isWeapp = useMemo(() => {
|
||||
try {
|
||||
return Taro.getEnv() === Taro.ENV_TYPE.WEAPP
|
||||
} catch (_e) {
|
||||
return process.env.TARO_ENV === 'weapp'
|
||||
}
|
||||
}, [])
|
||||
|
||||
const router = Taro.getCurrentInstance().router
|
||||
|
||||
useEffect(() => {
|
||||
// 注册/登录页不需要展示 tabBar
|
||||
Taro.hideTabBar()
|
||||
}, [])
|
||||
|
||||
const redirectUrl = useMemo(() => {
|
||||
const raw = (router?.params as any)?.redirect as string | undefined
|
||||
const decoded = safeDecodeMaybeEncoded(raw)
|
||||
if (!decoded) return ''
|
||||
return decoded.startsWith('/') ? decoded : `/${decoded}`
|
||||
}, [router?.params])
|
||||
|
||||
// 如果从分享/二维码直接进入注册页(携带 inviter/source/t),先暂存邀请信息
|
||||
useEffect(() => {
|
||||
try {
|
||||
const inviteParams = parseInviteParams({ query: router?.params })
|
||||
if (inviteParams?.inviter) {
|
||||
saveInviteParams(inviteParams)
|
||||
trackInviteSource(inviteParams.source || 'qrcode', parseInt(inviteParams.inviter, 10))
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('注册页处理邀请参数失败:', e)
|
||||
}
|
||||
}, [router?.params])
|
||||
|
||||
const navigateAfterLogin = async () => {
|
||||
if (!redirectUrl) {
|
||||
await Taro.reLaunch({ url: '/pages/index/index' })
|
||||
return
|
||||
}
|
||||
|
||||
if (isTabBarUrl(redirectUrl)) {
|
||||
// switchTab 不支持携带 query,这里按纯路径跳转
|
||||
await Taro.switchTab({ url: redirectUrl.split('?')[0] })
|
||||
return
|
||||
}
|
||||
|
||||
// 替换当前注册页,避免返回栈里再回到注册页
|
||||
await Taro.redirectTo({ url: redirectUrl })
|
||||
}
|
||||
|
||||
const handleGetPhoneNumber = async ({ detail }: GetPhoneNumberEvent) => {
|
||||
if (!isAgree) {
|
||||
Taro.showToast({ title: '请先勾选同意协议', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (loading) return
|
||||
|
||||
const { code: phoneCode, encryptedData, iv, errMsg } = detail || {}
|
||||
if (!phoneCode || (errMsg && errMsg.includes('fail'))) {
|
||||
Taro.showToast({ title: '未授权手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
// 获取存储的邀请参数(推荐人ID)
|
||||
const inviteParams = getStoredInviteParams()
|
||||
const refereeId = inviteParams?.inviter ? parseInt(inviteParams.inviter, 10) : 0
|
||||
|
||||
// 获取小程序登录 code(用于后续绑定 openid)
|
||||
const wxLoginCode = await getWeappLoginCode()
|
||||
|
||||
const res = (await Taro.request({
|
||||
url: 'https://glt-server.websoft.top/api/wx-login/loginByMpWxPhone',
|
||||
method: 'POST',
|
||||
data: {
|
||||
code: phoneCode,
|
||||
encryptedData,
|
||||
iv,
|
||||
notVerifyPhone: true,
|
||||
refereeId: refereeId,
|
||||
sceneType: 'save_referee',
|
||||
tenantId: TenantId,
|
||||
},
|
||||
header: {
|
||||
'content-type': 'application/json',
|
||||
TenantId,
|
||||
},
|
||||
})) as unknown as LoginResponse
|
||||
|
||||
if ((res as any)?.data?.code === 1) {
|
||||
Taro.showToast({ title: res.data.message || '登录失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const token = res?.data?.data?.access_token
|
||||
const user = res?.data?.data?.user
|
||||
if (!token || !user?.userId) {
|
||||
Taro.showToast({ title: '登录失败,请重试', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
saveStorageByLoginUser(token, user)
|
||||
|
||||
// 注册/登录成功后,立即补齐 openid(JSAPI 支付必需)
|
||||
try {
|
||||
await ensureWxOpenIdSaved({ user, wxLoginCode })
|
||||
} catch (e) {
|
||||
console.error('注册页绑定 openid 失败:', e)
|
||||
}
|
||||
|
||||
// 登录成功后尝试绑定推荐关系(如果有待处理 inviter,会自动处理并清理参数)
|
||||
try {
|
||||
await checkAndHandleInviteRelation()
|
||||
} catch (e) {
|
||||
console.error('注册页登录后处理邀请关系失败:', e)
|
||||
}
|
||||
|
||||
Taro.showToast({ title: '登录成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
navigateAfterLogin().catch((e) => console.error('登录后跳转失败:', e))
|
||||
}, 800)
|
||||
} catch (e: any) {
|
||||
console.error('注册/登录失败:', e)
|
||||
Taro.showToast({ title: e?.message || '登录失败', icon: 'none' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const goSmsLogin = () => {
|
||||
const inviteParams = getStoredInviteParams()
|
||||
const inviter = inviteParams?.inviter
|
||||
const source = inviteParams?.source
|
||||
const t = inviteParams?.t
|
||||
|
||||
const params: Record<string, any> = {}
|
||||
if (redirectUrl) params.redirect = redirectUrl
|
||||
// 兜底:把 inviter 带过去,避免“先点注册再进入”时丢失
|
||||
if (inviter) params.inviter = inviter
|
||||
if (source) params.source = source
|
||||
if (t) params.t = t
|
||||
|
||||
const qs = Object.entries(params)
|
||||
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)
|
||||
.join('&')
|
||||
Taro.navigateTo({ url: `/passport/sms-login${qs ? `?${qs}` : ''}` })
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'flex flex-col justify-center px-5 pt-5'}>
|
||||
<div className={'text-3xl text-center py-5 font-normal my-6'}>注册/登录</div>
|
||||
|
||||
<div className={'flex flex-col gap-3 bg-green-600 py-1'} style={{ borderRadius: '100px' }}>
|
||||
<Button
|
||||
type="primary"
|
||||
fill="solid"
|
||||
color="#07c160"
|
||||
block
|
||||
loading={loading}
|
||||
disabled={!isAgree || loading}
|
||||
open-type="getPhoneNumber"
|
||||
onGetPhoneNumber={handleGetPhoneNumber}
|
||||
>
|
||||
手机号一键注册/登录
|
||||
</Button>
|
||||
|
||||
{!isWeapp && (
|
||||
<Button type="default" block disabled={!isAgree || loading} onClick={goSmsLogin}>
|
||||
短信验证码注册/登录
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={'mt-6 flex text-sm items-center px-1'}>
|
||||
<Radio style={{ color: '#333333' }} checked={isAgree} onClick={() => setIsAgree(!isAgree)} />
|
||||
<span className={'text-gray-400'} onClick={() => setIsAgree(!isAgree)}>
|
||||
勾选表示您已阅读并同意
|
||||
</span>
|
||||
<a
|
||||
onClick={() => Taro.navigateTo({ url: '/passport/agreement' })}
|
||||
className={'text-blue-600'}
|
||||
>
|
||||
《服务协议及隐私政策》
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Register
|
||||
Reference in New Issue
Block a user