feat(passport): 重构短信登录页并提供手机号授权降级入口

- 修复购物车数据库缺失 spec_info 字段问题,新增迁移脚本
- 登录页处理手机号授权失败,按错误类型弹窗引导用户改用短信登录
- 登录页新增长期显示的「使用短信验证码登录」入口,提升用户降级体验
- 注册页同步改造,去除微信小程序环境下短信按钮隐藏问题
- 短信登录页整体 UI 重构,弃用 NutUI 组件,采用原生组件实现
- 短信登录页新增渐变背景、浮动光圈、圆形 logo、卡片输入区设计
- 实现手机号输入、验证码发送、倒计时及登录校验逻辑全覆盖
- 短信登录页新增返回微信快捷登录链接,避免用户被困登录流程
- 新增配套样式文件,实现多重动效及分阶段入场动画
- 保证类型检查和编译无误,增加设计稿 HTML 预览文件
This commit is contained in:
2026-07-15 18:55:12 +08:00
parent fad7c5f58e
commit b47ea6bd90
8 changed files with 891 additions and 57 deletions

View File

@@ -1,10 +1,10 @@
import {useEffect, useState} from "react";
import Taro from '@tarojs/taro'
import { View } from '@tarojs/components'
import {Input, Button} from '@nutui/nutui-react-taro'
import { View, Text, Input } from '@tarojs/components'
import {loginBySms, sendSmsCaptcha} from "@/api/passport/login";
import {LoginParam} from "@/api/passport/login/model";
import {checkAndHandleInviteRelation, hasPendingInvite, parseInviteParams, saveInviteParams, trackInviteSource} from "@/utils/invite";
import './sms-login.scss'
const SmsLogin = () => {
const [loading, setLoading] = useState<boolean>(false)
@@ -55,12 +55,8 @@ const SmsLogin = () => {
await Taro.redirectTo({ url: redirectUrl })
}
const reload = () => {
// sms-login 页面不是 tabBar 页面,无需调用 hideTabBar
}
useEffect(() => {
reload()
// sms-login 页面不是 tabBar 页面,无需调用 hideTabBar
}, [])
// 如果从分享/二维码链接进入短信登录页,先暂存邀请信息
@@ -78,7 +74,7 @@ const SmsLogin = () => {
// 倒计时效果
useEffect(() => {
let timer: NodeJS.Timeout
let timer: ReturnType<typeof setTimeout> | undefined
if (countdown > 0) {
timer = setTimeout(() => {
setCountdown(countdown - 1)
@@ -218,52 +214,114 @@ const SmsLogin = () => {
}
}
const phoneValid = validatePhone(formData.phone)
const codeValid = formData.code.length === 6
const canSubmit = phoneValid && codeValid && !loading
const sendDisabled = !phoneValid || sendingCode || countdown > 0
const sendBtnText = countdown > 0
? `${countdown}s 后重试`
: sendingCode
? '发送中'
: '获取验证码'
return (
<>
<View className='flex flex-col justify-center px-5 pt-3'>
<View className='flex flex-col justify-between items-center my-2'>
<Input
type='number'
placeholder='请输入手机号码'
maxLength={11}
value={formData.phone}
onChange={(value) => setFormData({...formData, phone: value})}
style={{backgroundColor: '#ffffff', borderRadius: '8px'}}
/>
<View className='page-sms-login'>
{/* 渐变背景 */}
<View className='sms-login-bg'>
<View className='sms-login-bg__gradient' />
<View className='sms-login-bg__circle sms-login-bg__circle--1' />
<View className='sms-login-bg__circle sms-login-bg__circle--2' />
</View>
<View className='sms-login-content'>
{/* 头部 Logo 和标题 */}
<View className='sms-login-header'>
<View className='sms-login-logo'>
<Text className='sms-login-logo__icon'>📱</Text>
</View>
<Text className='sms-login-title'></Text>
<Text className='sms-login-subtitle'></Text>
</View>
<View className='flex justify-between items-center bg-white rounded-lg my-2 pr-2'>
<Input
type='number'
placeholder='请输入6位验证码'
maxLength={6}
value={formData.code}
onChange={(value) => setFormData({...formData, code: value})}
style={{ backgroundColor: '#ffffff', borderRadius: '8px'}}
/>
<Button
size='small'
type={countdown > 0 ? "default" : "primary"}
loading={sendingCode}
disabled={sendingCode || countdown > 0}
onClick={handleSendCode}
>
{countdown > 0 ? `${countdown}s` : sendingCode ? '发送中...' : '获取验证码'}
</Button>
{/* 表单卡片 */}
<View className='sms-login-card'>
{/* 手机号 */}
<View className='sms-login-field'>
<Text className='sms-login-field__icon'>📞</Text>
<Input
className='sms-login-field__input'
type='number'
placeholder='请输入手机号码'
placeholderClass='sms-login-field__placeholder'
maxLength={11}
value={formData.phone}
onInput={(e: any) => setFormData({ ...formData, phone: e.detail.value })}
/>
</View>
<View className='sms-login-divider' />
{/* 验证码 */}
<View className='sms-login-field sms-login-field--code'>
<View className='sms-login-field__icon-wrap'>
<Text className='sms-login-field__icon'>🔐</Text>
<Input
className='sms-login-field__input'
type='number'
placeholder='请输入6位验证码'
placeholderClass='sms-login-field__placeholder'
maxLength={6}
value={formData.code}
onInput={(e: any) => setFormData({ ...formData, code: e.detail.value })}
/>
</View>
<View
className={`sms-login-send ${sendDisabled ? 'sms-login-send--disabled' : ''}`}
onClick={handleSendCode}
>
{sendBtnText}
</View>
</View>
</View>
<View className='flex justify-center my-5'>
<Button
type='info'
size='large'
className='w-full rounded-lg p-2'
loading={loading}
disabled={loading}
onClick={handleLogin}
{/* 登录按钮 */}
<View
className={`sms-login-submit ${canSubmit ? '' : 'sms-login-submit--disabled'}`}
hoverClass={canSubmit ? 'sms-login-submit--hover' : 'none'}
onClick={canSubmit ? handleLogin : undefined}
>
{loading ? '登录中...' : '登录 / 注册'}
</View>
{/* 返回微信登录 */}
<View
className='sms-login-back'
onClick={() => Taro.navigateBack({ delta: 1 })}
>
<Text className='sms-login-back__icon'></Text>
<Text className='sms-login-back__text'></Text>
</View>
{/* 协议 */}
<View className='sms-login-tips'>
<Text className='sms-login-tips__text'></Text>
<Text
className='sms-login-tips__link'
onClick={() => Taro.navigateTo({ url: '/passport/agreement?type=terms' })}
>
{loading ? '登录中...' : '登录'}
</Button>
</Text>
<Text className='sms-login-tips__text'></Text>
<Text
className='sms-login-tips__link'
onClick={() => Taro.navigateTo({ url: '/passport/agreement?type=privacy' })}
>
</Text>
</View>
</View>
</>
</View>
)
}
export default SmsLogin