1
This commit is contained in:
276
src/pages/login/index.tsx
Normal file
276
src/pages/login/index.tsx
Normal file
@@ -0,0 +1,276 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Form,
|
||||
Input,
|
||||
Space,
|
||||
Tabs,
|
||||
} from 'antd'
|
||||
import { LockOutlined, MessageOutlined, MobileOutlined } from '@ant-design/icons'
|
||||
import { theme } from 'antd'
|
||||
import { toast } from 'sonner'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { apiGetSmsCode } from '@/apis/apisCommon'
|
||||
import { merchantProfileGet } from '@/apis/mallMerchant'
|
||||
import { apiMallMerchantLogin } from '@/apis/apisUser'
|
||||
import { useSessionStore } from '@/store/session'
|
||||
import { resolveMerchantSessionIdentity } from '@/utils/mall'
|
||||
|
||||
interface LoginFormValues {
|
||||
account: string
|
||||
password?: string
|
||||
captcha?: string
|
||||
}
|
||||
|
||||
type LoginMode = 'password' | 'sms'
|
||||
|
||||
const PHONE_REGEX = /^1\d{10}$/
|
||||
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
|
||||
const resolveLoginType = (account: string): '0' | '1' => {
|
||||
return EMAIL_REGEX.test(account) ? '1' : '0'
|
||||
}
|
||||
|
||||
export default function LoginPage() {
|
||||
const { token } = theme.useToken()
|
||||
const navigate = useNavigate()
|
||||
const setSession = useSessionStore(state => state.setSession)
|
||||
const setToken = useSessionStore(state => state.setToken)
|
||||
const setUserInfo = useSessionStore(state => state.setUserInfo)
|
||||
const existingToken = useSessionStore(state => state.token)
|
||||
const loginAccount = useSessionStore(state => state.loginAccount)
|
||||
const userInfo = useSessionStore(state => state.userInfo)
|
||||
const [mode, setMode] = useState<LoginMode>('password')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [smsSending, setSmsSending] = useState(false)
|
||||
const [smsLeft, setSmsLeft] = useState(0)
|
||||
const smsTimerRef = useRef<number | null>(null)
|
||||
const [form] = Form.useForm<LoginFormValues>()
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && existingToken?.accessToken) {
|
||||
navigate('/profile', { replace: true })
|
||||
}
|
||||
}, [existingToken, loading, navigate])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (smsTimerRef.current) {
|
||||
window.clearInterval(smsTimerRef.current)
|
||||
smsTimerRef.current = null
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const tabs = useMemo(
|
||||
() => [
|
||||
{ key: 'password', label: '密码登录' },
|
||||
{ key: 'sms', label: '短信登录' },
|
||||
],
|
||||
[]
|
||||
)
|
||||
|
||||
const startSmsCountdown = (seconds: number) => {
|
||||
setSmsLeft(seconds)
|
||||
if (smsTimerRef.current) {
|
||||
window.clearInterval(smsTimerRef.current)
|
||||
smsTimerRef.current = null
|
||||
}
|
||||
smsTimerRef.current = window.setInterval(() => {
|
||||
setSmsLeft(prev => {
|
||||
if (prev <= 1) {
|
||||
if (smsTimerRef.current) {
|
||||
window.clearInterval(smsTimerRef.current)
|
||||
smsTimerRef.current = null
|
||||
}
|
||||
return 0
|
||||
}
|
||||
return prev - 1
|
||||
})
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const onSendSms = async () => {
|
||||
try {
|
||||
const account = (form.getFieldValue('account') ?? '').toString().trim()
|
||||
if (!PHONE_REGEX.test(account)) {
|
||||
toast.error('请输入有效的手机号')
|
||||
return
|
||||
}
|
||||
setSmsSending(true)
|
||||
await apiGetSmsCode({ account, type: '1' })
|
||||
toast.success('验证码已发送')
|
||||
startSmsCountdown(60)
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : '验证码发送失败'
|
||||
toast.error(message)
|
||||
} finally {
|
||||
setSmsSending(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
style={{
|
||||
borderRadius: token.borderRadiusLG,
|
||||
border: `1px solid ${token.colorBorderSecondary}`,
|
||||
boxShadow: 'none',
|
||||
}}
|
||||
>
|
||||
<Space direction="vertical" size={20} style={{ width: '100%' }}>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
requiredMark={false}
|
||||
initialValues={{
|
||||
account: loginAccount || userInfo?.account || '',
|
||||
}}
|
||||
onFinish={async values => {
|
||||
try {
|
||||
const account = values.account.trim()
|
||||
const isPhone = PHONE_REGEX.test(account)
|
||||
const isEmail = EMAIL_REGEX.test(account)
|
||||
|
||||
if (!isPhone && !isEmail) {
|
||||
toast.error('请输入有效的手机号或邮箱')
|
||||
return
|
||||
}
|
||||
|
||||
if (mode === 'sms' && !isPhone) {
|
||||
toast.error('验证码登录仅支持手机号')
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
const payload =
|
||||
mode === 'password'
|
||||
? {
|
||||
account,
|
||||
password: values.password?.trim(),
|
||||
loginType: resolveLoginType(account),
|
||||
type: '0' as const,
|
||||
loginIp: '',
|
||||
}
|
||||
: {
|
||||
account,
|
||||
captcha: values.captcha?.trim(),
|
||||
loginType: '0' as const,
|
||||
type: '1' as const,
|
||||
loginIp: '',
|
||||
}
|
||||
|
||||
const loginResult = await apiMallMerchantLogin(payload)
|
||||
setToken(loginResult.data)
|
||||
setUserInfo(null)
|
||||
setSession({
|
||||
loginAccount: account,
|
||||
merchantId: null,
|
||||
merchantName: '',
|
||||
})
|
||||
|
||||
try {
|
||||
const profileData = await merchantProfileGet()
|
||||
setSession({
|
||||
loginAccount: account,
|
||||
...resolveMerchantSessionIdentity(profileData.merchant),
|
||||
})
|
||||
} catch (profileError) {
|
||||
const message =
|
||||
profileError instanceof Error ? profileError.message : '同步商家资料失败'
|
||||
toast.error(`登录成功,但同步商家资料失败:${message}`)
|
||||
}
|
||||
|
||||
toast.success('登录成功')
|
||||
navigate('/profile', { replace: true })
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : '登录失败'
|
||||
toast.error(message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Tabs
|
||||
activeKey={mode}
|
||||
items={tabs}
|
||||
onChange={key => {
|
||||
setMode(key as LoginMode)
|
||||
form.resetFields(['password', 'captcha'])
|
||||
}}
|
||||
/>
|
||||
<Form.Item
|
||||
name="account"
|
||||
label={mode === 'sms' ? '手机号' : '登录账号'}
|
||||
rules={[
|
||||
{
|
||||
validator: (_, value) => {
|
||||
const account = (value ?? '').toString().trim()
|
||||
|
||||
if (!account) {
|
||||
return Promise.reject(
|
||||
new Error(mode === 'sms' ? '请输入手机号' : '请输入手机号或邮箱')
|
||||
)
|
||||
}
|
||||
|
||||
if (mode === 'sms') {
|
||||
return PHONE_REGEX.test(account)
|
||||
? Promise.resolve()
|
||||
: Promise.reject(new Error('请输入有效的手机号'))
|
||||
}
|
||||
|
||||
return PHONE_REGEX.test(account) || EMAIL_REGEX.test(account)
|
||||
? Promise.resolve()
|
||||
: Promise.reject(new Error('请输入有效的手机号或邮箱'))
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
placeholder={mode === 'sms' ? '请输入手机号' : '请输入手机号或邮箱'}
|
||||
autoComplete="username"
|
||||
prefix={<MobileOutlined />}
|
||||
/>
|
||||
</Form.Item>
|
||||
{mode === 'password' ? (
|
||||
<Form.Item
|
||||
name="password"
|
||||
label="密码"
|
||||
rules={[
|
||||
{ required: true, message: '请输入密码' },
|
||||
{ min: 6, message: '密码不能少于6位' },
|
||||
]}
|
||||
>
|
||||
<Input.Password
|
||||
placeholder="请输入密码"
|
||||
autoComplete="current-password"
|
||||
prefix={<LockOutlined />}
|
||||
/>
|
||||
</Form.Item>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="captcha"
|
||||
label="短信验证码"
|
||||
rules={[
|
||||
{ required: true, message: '请输入短信验证码' },
|
||||
{ pattern: /^\d{4,6}$/, message: '请输入有效验证码' },
|
||||
]}
|
||||
>
|
||||
<Space.Compact style={{ width: '100%' }}>
|
||||
<Input placeholder="请输入短信验证码" prefix={<MessageOutlined />} />
|
||||
<Button disabled={smsLeft > 0} loading={smsSending} onClick={() => void onSendSms()}>
|
||||
{smsLeft > 0 ? `${smsLeft}s` : '获取验证码'}
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item style={{ marginBottom: 0 }}>
|
||||
<Button type="primary" htmlType="submit" block loading={loading}>
|
||||
登录并进入工作台
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Space>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user