feat(passport): 实现管理员短信登录功能

- 更新登录页面标题为"管理员登录"
- 移除账号密码登录方式,仅保留短信验证码登录
- 新增手机号格式验证逻辑
- 集成短信验证码发送与登录接口- 添加图形验证码验证流程
- 实现短信验证码倒计时功能
- 更新用户中心跳转链接指向新的登录页
- 优化登录页面UI布局与样式
This commit is contained in:
2025-09-29 19:37:22 +08:00
parent ce014364ba
commit df25e1a406
3 changed files with 388 additions and 33 deletions

View File

@@ -250,10 +250,10 @@ const UserCell = () => {
/> />
<Cell <Cell
className="nutui-cell-clickable" className="nutui-cell-clickable"
title="短信登录" title="管理员登录"
align="center" align="center"
extra={<ArrowRight color="#cccccc" size={18}/>} extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => navTo('/passport/sms-login', true)} onClick={() => navTo('/passport/login', true)}
/> />
<Cell <Cell
className="nutui-cell-clickable" className="nutui-cell-clickable"

View File

@@ -1,4 +1,4 @@
export default definePageConfig({ export default definePageConfig({
navigationBarTitleText: '登录', navigationBarTitleText: '管理员登录',
navigationBarTextStyle: 'black' navigationBarTextStyle: 'black'
}) })

View File

@@ -1,56 +1,411 @@
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import Taro from '@tarojs/taro' import Taro from '@tarojs/taro'
import {Input, Radio, Button} from '@nutui/nutui-react-taro' import {Input, Radio, Button} from '@nutui/nutui-react-taro'
import {loginBySms, getCaptcha, sendSmsCaptcha} from '@/api/passport/login'
const Login = () => { const Login = () => {
const [isAgree, setIsAgree] = useState(false) const [isAgree, setIsAgree] = useState(false)
// 只保留短信登录方式
const [loginType, setLoginType] = useState('sms')
// const [username, setUsername] = useState('')
// const [password, setPassword] = useState('')
const [phone, setPhone] = useState('')
const [smsCode, setSmsCode] = useState('')
const [captchaImg, setCaptchaImg] = useState('')
const [captchaCode, setCaptchaCode] = useState('')
const [showCaptchaModal, setShowCaptchaModal] = useState(false)
const [countdown, setCountdown] = useState(0) // 短信验证码倒计时
const [loading, setLoading] = useState(false)
const reload = () => { const reload = () => {
Taro.hideTabBar() Taro.hideTabBar()
} }
// 获取图形验证码
const fetchCaptcha = async () => {
try {
const res = await getCaptcha()
setCaptchaImg(res.base64)
} catch (error) {
Taro.showToast({
title: '获取验证码失败',
icon: 'error'
})
}
}
// 发送短信验证码
const handleSendSmsCode = async () => {
if (!phone) {
Taro.showToast({
title: '请输入手机号',
icon: 'error'
})
return
}
// 验证手机号格式
const phoneReg = /^1[3-9]\d{9}$/
if (!phoneReg.test(phone)) {
Taro.showToast({
title: '手机号格式不正确',
icon: 'error'
})
return
}
// 显示图形验证码弹窗
fetchCaptcha()
setShowCaptchaModal(true)
}
// 确认发送短信验证码
const confirmSendSmsCode = async () => {
if (!captchaCode) {
Taro.showToast({
title: '请输入图形验证码',
icon: 'error'
})
return
}
try {
setLoading(true)
// 发送短信验证码时需要传入手机号和图形验证码
await sendSmsCaptcha({ phone, code: captchaCode })
Taro.showToast({
title: '短信验证码已发送',
icon: 'success'
})
setShowCaptchaModal(false)
setCaptchaCode('')
// 开始倒计时
setCountdown(60)
} catch (error) {
Taro.showToast({
title: error.message || '发送失败',
icon: 'error'
})
} finally {
setLoading(false)
}
}
// 短信验证码登录
const handleSmsLogin = async () => {
if (!phone) {
Taro.showToast({
title: '请输入手机号',
icon: 'error'
})
return
}
// 验证手机号格式
const phoneReg = /^1[3-9]\d{9}$/
if (!phoneReg.test(phone)) {
Taro.showToast({
title: '手机号格式不正确',
icon: 'error'
})
return
}
if (!smsCode) {
Taro.showToast({
title: '请输入短信验证码',
icon: 'error'
})
return
}
try {
setLoading(true)
// 短信登录时传入手机号和短信验证码
const res = await loginBySms({ phone, code: smsCode })
console.log(res,'.......')
Taro.showToast({
title: '登录成功',
icon: 'success'
})
// 跳转到首页
setTimeout(() => {
Taro.switchTab({ url: '/pages/index/index' })
}, 1500)
} catch (error) {
Taro.showToast({
title: error.message || '登录失败',
icon: 'error'
})
} finally {
setLoading(false)
}
}
// 登录处理
const onLogin = async () => {
if (!isAgree) {
Taro.showToast({
title: '请先同意服务协议',
icon: 'error'
})
return
}
handleSmsLogin()
}
// 倒计时处理
useEffect(() => {
let timer: any
if (countdown > 0) {
timer = setTimeout(() => {
setCountdown(countdown - 1)
}, 1000)
}
return () => clearTimeout(timer)
}, [countdown])
useEffect(() => { useEffect(() => {
reload() reload()
}, []) }, [])
return ( return (
<> <>
<div className={'flex flex-col justify-center px-5'}> <div style={{
<div className={'text-3xl text-center py-5 font-normal my-10'}></div> display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
padding: '0 20px',
minHeight: '70vh',
backgroundColor: '#f5f5f5'
}}>
<div style={{
fontSize: '24px',
textAlign: 'center',
padding: '20px 0',
fontWeight: 'normal',
margin: '20px 0 20px 0'
}}></div>
<> {/* 登录方式切换 - 隐藏账号登录 */}
<div className={'flex flex-col justify-between items-center my-2'}> <div style={{
<Input type="text" placeholder="手机号" maxLength={11} display: 'none', // 隐藏登录方式切换
style={{backgroundColor: '#ffffff', borderRadius: '8px'}}/> justifyContent: 'center',
marginBottom: '20px'
}}>
<div
style={{
padding: '10px 20px',
borderBottom: loginType === 'account' ? '2px solid #1890ff' : 'none',
color: loginType === 'account' ? '#1890ff' : '#999',
cursor: 'pointer'
}}
onClick={() => setLoginType('account')}
>
</div> </div>
<div className={'flex flex-col justify-between items-center my-2'}> <div
<Input type="password" placeholder="密码" style={{backgroundColor: '#ffffff', borderRadius: '8px'}}/> style={{
padding: '10px 20px',
borderBottom: loginType === 'sms' ? '2px solid #1890ff' : 'none',
color: loginType === 'sms' ? '#1890ff' : '#999',
cursor: 'pointer'
}}
onClick={() => setLoginType('sms')}
>
</div> </div>
<div className={'flex justify-between my-2 text-left px-1'}>
<a href={'#'} className={'text-blue-600 text-sm'}
onClick={() => Taro.navigateTo({url: '/passport/forget'})}></a>
<a href={'#'} className={'text-blue-600 text-sm'}
onClick={() => Taro.navigateTo({url: '/passport/register'})}></a>
</div> </div>
<div className={'flex justify-center my-5'}>
<Button type="info" size={'large'} className={'w-full rounded-lg p-2'} disabled={!isAgree}></Button>
</div>
<div className={'my-2 flex fixed justify-center bottom-20 left-0 text-sm items-center text-center w-full'}>
<Button onClick={() => Taro.navigateTo({url: '/passport/setting'})}></Button>
</div>
{/*<div className={'w-full fixed bottom-20 my-2 flex justify-center text-sm items-center text-center'}>*/}
{/* 没有账号?<a href={''} onClick={() => Taro.navigateTo({url: '/passport/register'})}*/}
{/* className={'text-blue-600'}>立即注册</a>*/}
{/*</div>*/}
</>
<div className={'my-2 flex text-sm items-center px-1'}> {/* 短信验证码登录 - 始终显示 */}
<Radio style={{color: '#333333'}} checked={isAgree} onClick={() => setIsAgree(!isAgree)}></Radio> <div>
<span className={'text-gray-400'} onClick={() => setIsAgree(!isAgree)}></span><a <div style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
alignItems: 'center',
margin: '10px 0'
}}>
<Input
type="text"
placeholder="手机号"
maxLength={11}
value={phone}
onChange={(val) => setPhone(val)}
style={{
backgroundColor: '#ffffff',
borderRadius: '8px',
width: '100%',
padding: '10px'
}}
/>
</div>
<div style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
alignItems: 'center',
margin: '10px 0'
}}>
<div style={{
display: 'flex',
width: '100%',
backgroundColor: '#ffffff',
borderRadius: '8px'
}}>
<Input
type="text"
placeholder="短信验证码"
maxLength={6}
value={smsCode}
onChange={(val) => setSmsCode(val)}
style={{
flex: 1,
border: 'none',
padding: '10px'
}}
/>
<Button
type="info"
size="small"
disabled={countdown > 0}
onClick={handleSendSmsCode}
style={{
borderRadius: '0 8px 8px 0',
height: '40px'
}}
>
{countdown > 0 ? `${countdown}秒后重发` : '获取验证码'}
</Button>
</div>
</div>
</div>
<div style={{
display: 'flex',
justifyContent: 'center',
margin: '20px 0'
}}>
<Button
type="info"
size={'large'}
style={{
width: '100%',
borderRadius: '8px',
padding: '10px'
}}
disabled={!isAgree}
loading={loading}
onClick={onLogin}
>
</Button>
</div>
<div style={{
display: 'flex',
alignItems: 'center',
padding: '0 5px',
margin: '10px 0'
}}>
<Radio
style={{color: '#333333'}}
checked={isAgree}
onClick={() => setIsAgree(!isAgree)}
/>
<span style={{color: '#999', marginLeft: '5px'}} onClick={() => setIsAgree(!isAgree)}></span>
<a
onClick={() => Taro.navigateTo({url: '/passport/agreement'})} onClick={() => Taro.navigateTo({url: '/passport/agreement'})}
className={'text-blue-600'}></a> style={{color: '#1890ff'}}
>
</a>
</div> </div>
</div> </div>
{/* 图形验证码弹窗 */}
{showCaptchaModal && (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 9999
}}>
<div style={{
backgroundColor: '#fff',
borderRadius: '8px',
padding: '20px',
width: '80%'
}}>
<div style={{
textAlign: 'center',
fontWeight: 'bold',
marginBottom: '15px',
fontSize: '16px'
}}></div>
<div style={{
display: 'flex',
justifyContent: 'center',
marginBottom: '15px'
}}>
{captchaImg && (
<img
src={`data:image/png;base64,${captchaImg}`}
alt="验证码"
style={{
width: '128px',
height: '48px',
cursor: 'pointer'
}}
onClick={fetchCaptcha}
/>
)}
</div>
<Input
type="text"
placeholder="请输入验证码"
value={captchaCode}
onChange={(val) => setCaptchaCode(val)}
style={{
marginBottom: '15px'
}}
/>
<div style={{
display: 'flex',
justifyContent: 'space-between'
}}>
<Button
type="default"
onClick={() => {
setShowCaptchaModal(false)
setCaptchaCode('')
}}
>
</Button>
<Button
type="info"
loading={loading}
onClick={confirmSendSmsCode}
>
</Button>
</div>
</div>
</div>
)}
</> </>
) )
} }
export default Login export default Login