145 lines
5.4 KiB
TypeScript
145 lines
5.4 KiB
TypeScript
import { useState } from 'react'
|
||
import { View, Text, Button } from '@tarojs/components'
|
||
import { useDidShow } from '@tarojs/taro'
|
||
import { useAppDispatch, useAppSelector } from '@/store/hooks'
|
||
import { setAuth } from '@/store/authSlice'
|
||
import {
|
||
performWxLogin,
|
||
completeUserInfoLogin,
|
||
completeMobileLogin,
|
||
getLoginSubState,
|
||
type LoginSubState
|
||
} from './wxLoginFlow'
|
||
import { useToast } from '@/hooks/useToast'
|
||
import { ROUTES } from '@/constants/routes'
|
||
import { reLaunch } from '@/utils/navigation'
|
||
import { storage } from '@/utils/storage'
|
||
import { WX_OPENID_STORAGE_KEY } from '@/constants/auth'
|
||
import styles from './index.module.scss'
|
||
|
||
export default function Login() {
|
||
const dispatch = useAppDispatch()
|
||
const toast = useToast()
|
||
const token = useAppSelector((s) => s.auth.token)
|
||
const [loading, setLoading] = useState(false)
|
||
// 注册中间态:needUserInfo(待头像昵称授权)→ noMobile(待手机号授权)→ null(初始/完成)
|
||
const [subState, setSubState] = useState<LoginSubState | null>(null)
|
||
const [openId, setOpenId] = useState('')
|
||
|
||
// 已有 token 直接进首页(本期不做 checkLogin 校验,spec D5)
|
||
useDidShow(() => {
|
||
if (token && !loading) reLaunch(ROUTES.HOME)
|
||
})
|
||
|
||
// 完整登录态校验与落地;未完成时推进 subState
|
||
const applyResult = async (result: Awaited<ReturnType<typeof performWxLogin>>) => {
|
||
const state = getLoginSubState(result)
|
||
if (state !== 'complete') {
|
||
console.log('[login-page] 注册未完成,推进到', state)
|
||
// openId 兜底链:noMobile 阶段后端把 openId 放在 bindKey 字段返回(联调实测);
|
||
// 其余阶段从本地缓存(needUserInfo 时已写入)兜底
|
||
const openIdNext =
|
||
result.openId ?? (state === 'noMobile' ? result.bindKey : undefined) ??
|
||
storage.get<string>(WX_OPENID_STORAGE_KEY) ?? openId
|
||
if (!openIdNext) throw new Error('响应缺少 openId,无法继续授权')
|
||
setOpenId(openIdNext)
|
||
setSubState(state)
|
||
setLoading(false)
|
||
return
|
||
}
|
||
if (!result.code || !result.userInfo) throw new Error('登录响应缺少凭证')
|
||
console.log('[login-page] 登录完成,token 已落地')
|
||
setSubState(null)
|
||
dispatch(setAuth({ token: result.code, userInfo: result.userInfo }))
|
||
await reLaunch(ROUTES.HOME)
|
||
}
|
||
|
||
const handleWxLogin = async () => {
|
||
if (loading) return
|
||
setLoading(true)
|
||
try {
|
||
applyResult(await performWxLogin())
|
||
} catch (e) {
|
||
// code 失效/已用等场景:toast 后端 Message,用户重按按钮重新 Taro.login(code 一次性,重试安全)
|
||
console.error('[login-page] performWxLogin 失败', e)
|
||
toast.error(e instanceof Error && e.message ? e.message : '登录失败')
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
// needUserInfo:getUserProfile 授权头像昵称
|
||
const handleAuthUserInfo = async () => {
|
||
if (loading || !openId) return
|
||
setLoading(true)
|
||
try {
|
||
applyResult(await completeUserInfoLogin(openId))
|
||
} catch (e) {
|
||
// 授权失败/取消:保留待授权态,用户可重试(getUserProfile 每次都需用户确认)
|
||
console.error('[login-page] completeUserInfoLogin 失败', e)
|
||
toast.error(e instanceof Error && e.message ? e.message : '授权失败')
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
// noMobile:手机号授权按钮回调(Button open-type=getPhoneNumber)
|
||
const handleGetPhoneNumber = async (e: {
|
||
detail: { iv: string; encryptedData: string; code?: string; errMsg?: string }
|
||
}) => {
|
||
console.log('[login-page] 手机号授权回调', {
|
||
errMsg: e.detail.errMsg,
|
||
// 两种手机号回传形态:新版 code(动态令牌)或旧版 iv/encryptedData(加密数据)
|
||
hasCode: Boolean(e.detail.code),
|
||
hasEncrypted: Boolean(e.detail.encryptedData)
|
||
})
|
||
if (loading || !openId) return
|
||
setLoading(true)
|
||
try {
|
||
applyResult(await completeMobileLogin(openId, e.detail))
|
||
} catch (err) {
|
||
console.error('[login-page] completeMobileLogin 失败', err)
|
||
toast.error(err instanceof Error && err.message ? err.message : '授权失败')
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
const submitText = () => {
|
||
if (loading) {
|
||
return subState === 'noMobile' ? '绑定中...' : subState === 'needUserInfo' ? '授权中...' : '登录中...'
|
||
}
|
||
if (subState === 'needUserInfo') return '授权头像昵称,继续'
|
||
if (subState === 'noMobile') return '授权手机号,完成注册'
|
||
return '微信一键登录'
|
||
}
|
||
|
||
return (
|
||
<View className={styles.page}>
|
||
<View className={styles.hero}>
|
||
<View className={styles.logo}>
|
||
<Text className={styles.logoText}>智</Text>
|
||
</View>
|
||
<Text className={styles.productName}>智慧养老</Text>
|
||
<Text className={styles.slogan}>一起守护家人</Text>
|
||
</View>
|
||
<View className={styles.form}>
|
||
{subState === 'noMobile' ? (
|
||
// 手机号授权必须用原生 Button open-type,微信弹官方授权框
|
||
<Button
|
||
className={styles.submitBtn}
|
||
openType="getPhoneNumber"
|
||
onGetPhoneNumber={handleGetPhoneNumber}
|
||
>
|
||
{submitText()}
|
||
</Button>
|
||
) : (
|
||
<View
|
||
className={styles.submit}
|
||
onClick={subState === 'needUserInfo' ? handleAuthUserInfo : handleWxLogin}
|
||
>
|
||
<Text className={styles.submitText}>{submitText()}</Text>
|
||
</View>
|
||
)}
|
||
</View>
|
||
</View>
|
||
)
|
||
}
|