277 lines
8.8 KiB
TypeScript
277 lines
8.8 KiB
TypeScript
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>
|
|
)
|
|
}
|