This commit is contained in:
魔方熊成祥
2026-04-29 21:38:18 +08:00
commit ba0458abe2
56 changed files with 10300 additions and 0 deletions

276
src/pages/login/index.tsx Normal file
View 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>
)
}