增加“验证码登录”模式

This commit is contained in:
yml2213
2026-08-20 14:46:53 +08:00
parent 757eee9742
commit 555a7d43a7
4 changed files with 352 additions and 45 deletions
@@ -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>
)}
</>
),
},
+24
View File
@@ -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')
}