Files
xinlong-shop-taro/src_bak/passport/login.tsx
赵忠林 1fa58040f3 feat(user): 新增收货地址管理及售后申请页面
- 新增地址类型定义,增强前端地址数据结构
- 新增地址编辑页面,支持地址智能识别和定位选点功能
- 地址编辑支持省市区选择及默认地址设置
- 新增地址列表页面,支持地址展示、删除、编辑和选择功能
- 实现售后申请页面,支持选择售后类型和退款原因
- 售后申请支持商品选择、退款金额计算和凭证上传
- 新增售后详情页面,支持售后状态展示及申请取消
- 优化页面加载和用户交互体验,增加错误提示和权限处理
2026-07-01 12:11:56 +08:00

332 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 {
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/points/index',
'/pages/user/user',
]
const pure = redirectUrl.split('?')[0]
if (tabBarUrls.includes(pure)) {
await Taro.switchTab({ url: pure })
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, errMsg } = detail || {}
if (!phoneCode || (errMsg && errMsg.includes('fail'))) {
Taro.showToast({ title: '未授权手机号', icon: 'none' })
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
}
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__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