增加“验证码登录”模式
This commit is contained in:
@@ -7,18 +7,32 @@ import {
|
||||
ThunderboltOutlined,
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { Alert, App, Button, Form, Input, Modal, Space, Table, Tabs, Typography } from 'antd'
|
||||
import {
|
||||
Alert,
|
||||
App,
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
Segmented,
|
||||
Space,
|
||||
Table,
|
||||
Tabs,
|
||||
Typography,
|
||||
} from 'antd'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Navigate, useNavigate } from 'react-router'
|
||||
|
||||
import {
|
||||
deleteWorkerSessionWithCredentials,
|
||||
loginWorker,
|
||||
loginWorkerBySmsCode,
|
||||
manageWorkerSessions,
|
||||
registerWorker,
|
||||
resetWorkerPassword,
|
||||
sendWorkerSmsCode,
|
||||
sendWorkerPasswordResetSmsCode,
|
||||
sendWorkerLoginSmsCode,
|
||||
} from '@/services/worker'
|
||||
import type { WorkerSessionDevice } from '@/types/worker-platform'
|
||||
import { hasWorkerSession, setWorkerSession } from '@/utils/worker-auth'
|
||||
@@ -49,9 +63,12 @@ export default function WorkerLoginPage() {
|
||||
const { message, modal } = App.useApp()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [registerForm] = Form.useForm()
|
||||
const [loginSmsForm] = Form.useForm<{ phone: string; code: string }>()
|
||||
const [resetForm] = Form.useForm<{ phone: string; code: string; password: string }>()
|
||||
const [deviceForm] = Form.useForm<{ username: string; password: string }>()
|
||||
const [countdown, setCountdown] = useState(0)
|
||||
const [loginSmsCountdown, setLoginSmsCountdown] = useState(0)
|
||||
const [loginMode, setLoginMode] = useState<'password' | 'sms'>('password')
|
||||
const [resetCountdown, setResetCountdown] = useState(0)
|
||||
const [forgotPasswordOpen, setForgotPasswordOpen] = useState(false)
|
||||
const [deviceManagerOpen, setDeviceManagerOpen] = useState(false)
|
||||
@@ -64,6 +81,12 @@ export default function WorkerLoginPage() {
|
||||
return () => window.clearInterval(timer)
|
||||
}, [countdown])
|
||||
|
||||
useEffect(() => {
|
||||
if (loginSmsCountdown <= 0) return
|
||||
const timer = window.setInterval(() => setLoginSmsCountdown((value) => value - 1), 1000)
|
||||
return () => window.clearInterval(timer)
|
||||
}, [loginSmsCountdown])
|
||||
|
||||
useEffect(() => {
|
||||
if (resetCountdown <= 0) return
|
||||
const timer = window.setInterval(() => setResetCountdown((value) => value - 1), 1000)
|
||||
@@ -105,6 +128,42 @@ export default function WorkerLoginPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function requestLoginSmsCode() {
|
||||
const phone = String(loginSmsForm.getFieldValue('phone') || '').trim()
|
||||
if (!/^1[3-9]\d{9}$/.test(phone)) {
|
||||
message.error('请先填写正确的手机号')
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await sendWorkerLoginSmsCode({ phone })
|
||||
setLoginSmsCountdown(SMS_COUNTDOWN_SECONDS)
|
||||
message.success(
|
||||
response.data.provider === 'mock'
|
||||
? '验证码已发送(开发模式,验证码见服务日志)'
|
||||
: `验证码已发送,${response.data.expiresInSeconds} 秒内有效`,
|
||||
)
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '验证码发送失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function submitSmsLogin(values: { phone: string; code: string }) {
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await loginWorkerBySmsCode(values)
|
||||
setWorkerSession(response.data.token, response.data.expiresAt, response.data.worker)
|
||||
message.success('登录成功')
|
||||
void navigate('/hall', { replace: true })
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '验证码登录失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadManagedDevices(values?: { username: string; password: string }) {
|
||||
const credentials = values || deviceForm.getFieldsValue()
|
||||
if (!credentials.username || !credentials.password) {
|
||||
@@ -275,51 +334,116 @@ export default function WorkerLoginPage() {
|
||||
<>
|
||||
<div className="auth-form-header">
|
||||
<h1>欢迎回来</h1>
|
||||
<p>使用手机号、昵称或账号登录,进入抢单大厅</p>
|
||||
<p>使用密码或短信验证码登录,进入抢单大厅</p>
|
||||
</div>
|
||||
<Form layout="vertical" onFinish={submitLogin} requiredMark={false}>
|
||||
<Form.Item
|
||||
label="手机号 / 昵称 / 账号"
|
||||
name="username"
|
||||
rules={[{ required: true, message: '请输入手机号、昵称或账号' }]}
|
||||
<Segmented
|
||||
block
|
||||
value={loginMode}
|
||||
options={[
|
||||
{ label: '密码登录', value: 'password' },
|
||||
{ label: '验证码登录', value: 'sms' },
|
||||
]}
|
||||
onChange={(value) => setLoginMode(value as 'password' | 'sms')}
|
||||
style={{ marginBottom: 20 }}
|
||||
/>
|
||||
{loginMode === 'password' ? (
|
||||
<Form layout="vertical" onFinish={submitLogin} requiredMark={false}>
|
||||
<Form.Item
|
||||
label="手机号 / 昵称 / 账号"
|
||||
name="username"
|
||||
rules={[{ required: true, message: '请输入手机号、昵称或账号' }]}
|
||||
>
|
||||
<Input
|
||||
prefix={<UserOutlined />}
|
||||
autoComplete="username"
|
||||
placeholder="手机号 / 昵称 / 账号"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="密码"
|
||||
name="password"
|
||||
rules={[{ required: true, message: '请输入密码' }]}
|
||||
>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined />}
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={loading}
|
||||
size="large"
|
||||
block
|
||||
>
|
||||
登录
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
icon={<GlobalOutlined />}
|
||||
block
|
||||
onClick={() => setDeviceManagerOpen(true)}
|
||||
>
|
||||
登录设备管理
|
||||
</Button>
|
||||
<Button type="link" block onClick={() => setForgotPasswordOpen(true)}>
|
||||
忘记密码?
|
||||
</Button>
|
||||
</Form>
|
||||
) : (
|
||||
<Form
|
||||
form={loginSmsForm}
|
||||
layout="vertical"
|
||||
onFinish={submitSmsLogin}
|
||||
requiredMark={false}
|
||||
>
|
||||
<Input
|
||||
prefix={<UserOutlined />}
|
||||
autoComplete="username"
|
||||
placeholder="手机号 / 昵称 / 账号"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="密码"
|
||||
name="password"
|
||||
rules={[{ required: true, message: '请输入密码' }]}
|
||||
>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined />}
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={loading}
|
||||
size="large"
|
||||
block
|
||||
>
|
||||
登录
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
icon={<GlobalOutlined />}
|
||||
block
|
||||
onClick={() => setDeviceManagerOpen(true)}
|
||||
>
|
||||
登录设备管理
|
||||
</Button>
|
||||
<Button type="link" block onClick={() => setForgotPasswordOpen(true)}>
|
||||
忘记密码?
|
||||
</Button>
|
||||
</Form>
|
||||
<Form.Item
|
||||
label="手机号"
|
||||
name="phone"
|
||||
rules={[
|
||||
{ required: true, message: '请输入手机号' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确' },
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
prefix={<MobileOutlined />}
|
||||
maxLength={11}
|
||||
placeholder="注册时使用的手机号"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="短信验证码"
|
||||
name="code"
|
||||
rules={[{ required: true, message: '请输入短信验证码' }]}
|
||||
>
|
||||
<Space.Compact style={{ width: '100%' }}>
|
||||
<Input
|
||||
prefix={<SafetyCertificateOutlined />}
|
||||
maxLength={6}
|
||||
placeholder="6 位数字验证码"
|
||||
/>
|
||||
<Button
|
||||
loading={loading}
|
||||
disabled={loginSmsCountdown > 0}
|
||||
onClick={requestLoginSmsCode}
|
||||
>
|
||||
{loginSmsCountdown > 0
|
||||
? `${loginSmsCountdown}s 后重发`
|
||||
: '获取验证码'}
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={loading}
|
||||
size="large"
|
||||
block
|
||||
>
|
||||
验证码登录
|
||||
</Button>
|
||||
</Form>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -64,6 +64,30 @@ export function loginWorker(payload: { username: string; password: string }) {
|
||||
})
|
||||
}
|
||||
|
||||
function buildWorkerDevicePayload() {
|
||||
const userAgent = navigator.userAgent
|
||||
const isMobile = /Android|iPhone|iPad|iPod|Mobile/i.test(userAgent)
|
||||
return {
|
||||
deviceId: getWorkerDeviceId(),
|
||||
deviceType: isMobile ? 'mobile' : 'pc',
|
||||
deviceName: isMobile ? '手机浏览器' : 'PC 浏览器',
|
||||
}
|
||||
}
|
||||
|
||||
export function sendWorkerLoginSmsCode(payload: { phone: string }) {
|
||||
return apiPost<{ sent: boolean; expiresInSeconds: number; provider: string }>(
|
||||
'/api/v1/worker/auth/login/sms-code',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function loginWorkerBySmsCode(payload: { phone: string; code: string }) {
|
||||
return apiPost<WorkerLoginResponse>('/api/v1/worker/auth/login/sms', {
|
||||
...payload,
|
||||
...buildWorkerDevicePayload(),
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchWorkerSessions() {
|
||||
return apiGet<{ maxDevices: number; items: WorkerSessionDevice[] }>('/api/v1/worker/auth/devices')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user