- 将原来 switchTab 跳转“订单”页改为 navigateTo,以适配非 tabBar 页面 - 统一更新多个文件中关于订单页的跳转方式共计 6 处 - 更新 tabBar 配置,从包含“订单”页改为实际的四个 tabBar 页面 - 修正 isTabBarUrl 相关判断,去除不存在或非 tabBar 路径 - 调整多个 tabBar 判断列表,保证逻辑一致性与正确性 - 修改购物车页图片压缩宽度加入限制,优化资源加载 - 更新订单列表页注释,去除“tabBar 页”描述 - 备注遗留问题,部分积分页跳转仍用错误方式,待进一步修复
392 lines
12 KiB
TypeScript
392 lines
12 KiB
TypeScript
import { useEffect, useState } from 'react'
|
||
import Taro from '@tarojs/taro'
|
||
import { View, Image, Text, Button } from '@tarojs/components'
|
||
import { TenantId } from '@/config/app'
|
||
import { getWxOpenId } from '@/api/layout'
|
||
import { getUserInfo } from '@/api/layout'
|
||
import { saveStorageByLoginUser } from '@/utils/server'
|
||
import { isUserDisabled } from '@/utils/auth'
|
||
import {
|
||
checkAndHandleInviteRelation,
|
||
hasPendingInvite,
|
||
parseInviteParams,
|
||
saveInviteParams,
|
||
trackInviteSource,
|
||
} from '@/utils/invite'
|
||
import './login.scss'
|
||
|
||
interface GetPhoneNumberDetail {
|
||
code?: string
|
||
encryptedData?: string
|
||
iv?: string
|
||
errMsg?: string
|
||
}
|
||
|
||
interface GetPhoneNumberEvent {
|
||
detail: GetPhoneNumberDetail
|
||
}
|
||
|
||
/** 同步判断当前是否是微信小程序环境 */
|
||
function detectIsWeapp(): boolean {
|
||
try {
|
||
return Taro.getEnv() === Taro.ENV_TYPE.WEAPP
|
||
} catch {
|
||
return process.env.TARO_ENV === 'weapp'
|
||
}
|
||
}
|
||
|
||
const IS_WEAPP = detectIsWeapp()
|
||
|
||
/** 小程序登录获取 code */
|
||
async function getWeappLoginCode(): Promise<string | undefined> {
|
||
try {
|
||
const res = await new Promise<{ code?: string }>((resolve, reject) => {
|
||
Taro.login({ success: (r) => resolve(r), fail: reject })
|
||
})
|
||
return res?.code
|
||
} catch {
|
||
return undefined
|
||
}
|
||
}
|
||
|
||
/** 确保微信 openid 已保存到服务端 */
|
||
async function ensureWxOpenIdSaved() {
|
||
try {
|
||
if (Taro.getEnv() !== Taro.ENV_TYPE.WEAPP) return
|
||
} catch {
|
||
if (process.env.TARO_ENV !== 'weapp') return
|
||
}
|
||
|
||
const code = await getWeappLoginCode()
|
||
if (!code) return
|
||
|
||
try {
|
||
await getWxOpenId({ code })
|
||
const freshUser = await getUserInfo()
|
||
if (freshUser) {
|
||
const token = Taro.getStorageSync('access_token')
|
||
saveStorageByLoginUser(token, freshUser)
|
||
}
|
||
} catch (e) {
|
||
console.error('登录后绑定 openid 失败:', e)
|
||
}
|
||
}
|
||
|
||
/** 获取用户信息 */
|
||
async function fetchUserInfo(token: string) {
|
||
try {
|
||
const serverUrl = 'https://server.websoft.top'
|
||
const res: any = await Taro.request({
|
||
url: `${serverUrl}/api/auth/user`,
|
||
method: 'GET',
|
||
header: {
|
||
'Authorization': `Bearer ${token}`,
|
||
'content-type': 'application/json',
|
||
TenantId: String(TenantId),
|
||
},
|
||
})
|
||
|
||
if (res.data?.code === 0 && res.data?.data) {
|
||
return res.data.data
|
||
}
|
||
return null
|
||
} catch (e) {
|
||
console.error('获取用户信息失败:', e)
|
||
return null
|
||
}
|
||
}
|
||
|
||
const Login = () => {
|
||
const [isAgree, setIsAgree] = useState(false)
|
||
const [loading, setLoading] = useState(false)
|
||
const [showContent, setShowContent] = useState(false)
|
||
|
||
const router = Taro.getCurrentInstance().router
|
||
const isWeapp = IS_WEAPP
|
||
|
||
/** 页面加载动画 */
|
||
useEffect(() => {
|
||
setTimeout(() => setShowContent(true), 100)
|
||
}, [])
|
||
|
||
/** 解析 redirect 参数 */
|
||
const redirectUrl = (() => {
|
||
const raw = (router?.params as Record<string, string> | undefined)?.redirect
|
||
if (!raw) return ''
|
||
try {
|
||
const decoded = decodeURIComponent(raw)
|
||
return decoded.startsWith('/') ? decoded : `/${decoded}`
|
||
} catch {
|
||
return raw.startsWith('/') ? raw : `/${raw}`
|
||
}
|
||
})()
|
||
|
||
/** 处理邀请参数 */
|
||
useEffect(() => {
|
||
try {
|
||
const inviteParams = parseInviteParams({ query: router?.params })
|
||
if (inviteParams?.inviter) {
|
||
saveInviteParams(inviteParams)
|
||
trackInviteSource(inviteParams.source || 'share', parseInt(inviteParams.inviter, 10))
|
||
}
|
||
} catch (e) {
|
||
console.error('登录页处理邀请参数失败:', e)
|
||
}
|
||
}, [router?.params])
|
||
|
||
/** 登录成功后跳转 */
|
||
const navigateAfterLogin = async () => {
|
||
if (!redirectUrl) {
|
||
await Taro.reLaunch({ url: '/pages/index/index' })
|
||
return
|
||
}
|
||
|
||
const tabBarUrls = [
|
||
'/pages/index/index',
|
||
'/pages/shop/index',
|
||
'/pages/user/user',
|
||
'/pages/shop/cart',
|
||
]
|
||
const pure = redirectUrl.split('?')[0]
|
||
|
||
if (tabBarUrls.includes(pure)) {
|
||
await Taro.switchTab({ url: pure })
|
||
return
|
||
}
|
||
|
||
await Taro.redirectTo({ url: redirectUrl })
|
||
}
|
||
|
||
/** 跳转到短信验证码登录(降级方案) */
|
||
const goSmsLogin = () => {
|
||
const params: Record<string, string> = {}
|
||
if (redirectUrl) params.redirect = redirectUrl
|
||
// 透传邀请参数,避免从登录页切到短信登录时丢失 inviter
|
||
const inviteParams = parseInviteParams({ query: router?.params })
|
||
if (inviteParams?.inviter) params.inviter = inviteParams.inviter
|
||
if (inviteParams?.source) params.source = inviteParams.source
|
||
if (inviteParams?.t) params.t = inviteParams.t
|
||
|
||
const qs = Object.entries(params)
|
||
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)
|
||
.join('&')
|
||
Taro.navigateTo({ url: `/passport/sms-login${qs ? `?${qs}` : ''}` })
|
||
}
|
||
|
||
/** 手机号授权失败时弹窗引导走短信登录(区分原因) */
|
||
const showPhoneAuthFailedModal = (errMsg?: string) => {
|
||
let content = '请尝试使用短信验证码登录'
|
||
if (errMsg?.includes('user deny')) {
|
||
content = '您已取消微信手机号授权,可改用短信验证码登录'
|
||
} else if (errMsg?.includes('privacy')) {
|
||
content = '微信未授权获取手机号,可改用短信验证码登录'
|
||
} else if (errMsg?.includes('no permission')) {
|
||
content = '小程序暂未获得手机号授权,可改用短信验证码登录'
|
||
}
|
||
Taro.showModal({
|
||
title: '未获取到手机号',
|
||
content,
|
||
confirmText: '短信登录',
|
||
cancelText: '我知道了',
|
||
confirmColor: '#07c160',
|
||
success: (res) => {
|
||
if (res.confirm) goSmsLogin()
|
||
},
|
||
})
|
||
}
|
||
|
||
/** 手机号快捷登录 */
|
||
const handleGetPhoneNumber = async ({ detail }: GetPhoneNumberEvent) => {
|
||
if (!isAgree) {
|
||
Taro.showToast({ title: '请先勾选同意协议', icon: 'none' })
|
||
return
|
||
}
|
||
if (loading) return
|
||
|
||
const { code: phoneCode, errMsg } = detail || {}
|
||
if (!phoneCode || (errMsg && errMsg.includes('fail'))) {
|
||
showPhoneAuthFailedModal(errMsg)
|
||
return
|
||
}
|
||
|
||
try {
|
||
setLoading(true)
|
||
|
||
const inviteParams = parseInviteParams({ query: router?.params })
|
||
const refereeId = inviteParams?.inviter ? parseInt(inviteParams.inviter, 10) : 0
|
||
|
||
const serverUrl = 'https://server.websoft.top'
|
||
const res: any = await Taro.request({
|
||
url: `${serverUrl}/api/wx-login/loginByMpWxPhone`,
|
||
method: 'POST',
|
||
data: {
|
||
code: phoneCode,
|
||
notVerifyPhone: true,
|
||
refereeId,
|
||
sceneType: 'save_referee',
|
||
tenantId: Number(TenantId),
|
||
},
|
||
header: { 'content-type': 'application/json', TenantId: String(TenantId) },
|
||
})
|
||
|
||
if (res.data?.code === 0 && res.data?.data?.access_token) {
|
||
const token = res.data.data.access_token
|
||
let user = res.data.data.user
|
||
|
||
// 获取最新的用户信息
|
||
const freshUserInfo = await fetchUserInfo(token)
|
||
if (freshUserInfo) {
|
||
user = freshUserInfo
|
||
}
|
||
|
||
// 检查用户是否被禁用
|
||
if (isUserDisabled(user)) {
|
||
Taro.showModal({
|
||
title: '账号已被禁用',
|
||
content: '您的账号已被管理员禁用,暂时无法使用小程序。如有疑问请联系门店客服。',
|
||
showCancel: false,
|
||
confirmText: '我知道了',
|
||
confirmColor: '#ee0a24',
|
||
})
|
||
return
|
||
}
|
||
|
||
saveStorageByLoginUser(token, user)
|
||
|
||
// 绑定 openid + 处理邀请关系
|
||
await ensureWxOpenIdSaved()
|
||
if (hasPendingInvite()) {
|
||
try { await checkAndHandleInviteRelation() } catch (e) { console.error(e) }
|
||
}
|
||
|
||
Taro.showToast({ title: '登录成功', icon: 'success' })
|
||
setTimeout(() => navigateAfterLogin(), 800)
|
||
} else {
|
||
Taro.showToast({ title: res.data?.message || '登录失败', icon: 'none' })
|
||
}
|
||
} catch (e: any) {
|
||
console.error('微信登录失败:', e)
|
||
Taro.showToast({ title: e?.message || '登录失败', icon: 'none' })
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<View className={`page-login ${showContent ? 'page-login--show' : ''}`}>
|
||
{/* 渐变背景 */}
|
||
<View className='login-bg'>
|
||
<View className='login-bg__gradient' />
|
||
<View className='login-bg__circle login-bg__circle--1' />
|
||
<View className='login-bg__circle login-bg__circle--2' />
|
||
<View className='login-bg__circle login-bg__circle--3' />
|
||
</View>
|
||
|
||
{/* 主要内容区域 */}
|
||
<View className='login-content'>
|
||
{/* Logo 和标题 */}
|
||
<View className='login-header'>
|
||
<Text className='login-title'>欢迎来到鑫龙家电</Text>
|
||
<Text className='login-subtitle'>登录后享受更多精彩功能</Text>
|
||
</View>
|
||
|
||
{/* 登录按钮区域 */}
|
||
<View className='login-body'>
|
||
{isWeapp && (
|
||
<View className='login-methods'>
|
||
|
||
<Button
|
||
className='login-btn'
|
||
style={{
|
||
background: 'linear-gradient(135deg, #07c160, #06ad56)',
|
||
border: 'none',
|
||
borderRadius: '48px',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
color: '#ffffff',
|
||
fontWeight: '600',
|
||
textAlign: 'center',
|
||
opacity: (!isAgree || loading) ? 0.5 : 1,
|
||
margin: 0,
|
||
padding: 0,
|
||
}}
|
||
openType='getPhoneNumber'
|
||
onGetPhoneNumber={handleGetPhoneNumber}
|
||
disabled={!isAgree || loading}
|
||
loading={loading}
|
||
>
|
||
手机号快捷登录
|
||
</Button>
|
||
|
||
{/*<View className='login-methods__tip text-gray-200'>*/}
|
||
{/* <Text>点击上方按钮,快速登录</Text>*/}
|
||
{/*</View>*/}
|
||
|
||
<View className='login-methods__alt'>
|
||
<Text
|
||
className='login-methods__alt-link'
|
||
onClick={() => goSmsLogin()}
|
||
>
|
||
使用短信验证码登录
|
||
</Text>
|
||
</View>
|
||
|
||
<View className='login-methods__divider'>
|
||
<View className='divider-line' />
|
||
<Text className='divider-text'>安全快捷</Text>
|
||
<View className='divider-line' />
|
||
</View>
|
||
|
||
<View className='login-features'>
|
||
<View className='feature-item'>
|
||
<Text className='feature-icon'>🔒</Text>
|
||
<Text className='feature-text'>安全可靠</Text>
|
||
</View>
|
||
<View className='feature-item'>
|
||
<Text className='feature-icon'>⚡</Text>
|
||
<Text className='feature-text'>一键登录</Text>
|
||
</View>
|
||
<View className='feature-item'>
|
||
<Text className='feature-icon'>🎁</Text>
|
||
<Text className='feature-text'>专属福利</Text>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
)}
|
||
|
||
{/* 非微信小程序环境提示 */}
|
||
{!isWeapp && (
|
||
<View className='login-non-weapp'>
|
||
<View className='non-weapp-icon'>💻</View>
|
||
<Text className='non-weapp-title'>请在微信小程序中打开</Text>
|
||
<Text className='non-weapp-desc'>当前环境不支持手机号快捷登录,请在微信小程序中使用完整功能</Text>
|
||
</View>
|
||
)}
|
||
</View>
|
||
|
||
{/* 协议勾选 - 自定义实现 */}
|
||
<View className='login-footer'>
|
||
<View className='login-agreement' onClick={() => setIsAgree(!isAgree)}>
|
||
<View className={`login-agreement__checkbox ${isAgree ? 'login-agreement__checkbox--checked' : ''}`}>
|
||
{isAgree && <Text className='login-agreement__check'>✓</Text>}
|
||
</View>
|
||
<Text className='login-agreement__text'>
|
||
{'我已阅读并同意'}
|
||
<Text className='link' onClick={(e) => { e.stopPropagation(); Taro.navigateTo({ url: '/passport/agreement?type=terms' }) }}>
|
||
{'《用户协议》'}
|
||
</Text>
|
||
{'和'}
|
||
<Text className='link' onClick={(e) => { e.stopPropagation(); Taro.navigateTo({ url: '/passport/agreement?type=privacy' }) }}>
|
||
{'《隐私政策》'}
|
||
</Text>
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
)
|
||
}
|
||
|
||
export default Login
|