import { useEffect, useRef, useState } from 'react' import { useNavigate } from 'react-router-dom' import { Alert, Button, Divider, Form, Input, Space, Typography, message } from 'antd' import { LockOutlined, RightOutlined, SafetyCertificateOutlined, UserOutlined } from '@ant-design/icons' import { authApi } from '../api' import type { PreparedDeviceLogin } from '../types' import { useAuth } from '../store/authContext' type LoginForm = { username: string password: string smsCode: string } export default function Login() { const { login, applyLoginResult } = useAuth() const navigate = useNavigate() const [form] = Form.useForm() const [loading, setLoading] = useState(false) const [sending, setSending] = useState(false) const [countdown, setCountdown] = useState(0) const [waiting, setWaiting] = useState(false) const [phoneMasked, setPhoneMasked] = useState('') const timerRef = useRef(null) const submittingRef = useRef(false) useEffect(() => { return () => { if (timerRef.current) window.clearInterval(timerRef.current) } }, []) const startCountdown = (seconds: number) => { setCountdown(seconds) if (timerRef.current) window.clearInterval(timerRef.current) timerRef.current = window.setInterval(() => { setCountdown((current) => { if (current <= 1) { if (timerRef.current) window.clearInterval(timerRef.current) return 0 } return current - 1 }) }, 1000) } const sendCode = async () => { try { const values = await form.validateFields(['username', 'password']) setSending(true) const result = await authApi.sendLoginSmsCode(values.username.trim(), values.password) setPhoneMasked(result.phoneMasked) startCountdown(60) message.success(`验证码已发送至 ${result.phoneMasked}`) } catch (error) { const text = error instanceof Error ? error.message : '验证码发送失败' if (text.includes('未绑定手机号')) { message.warning(`${text},可使用下方「旧账号仅密码登录」`) } else { message.error(text) } } finally { setSending(false) } } const onFinish = async (values: LoginForm) => { if (submittingRef.current) return submittingRef.current = true setLoading(true) setWaiting(false) try { const prepared = await authApi.prepareLogin({ username: values.username.trim(), password: values.password, smsCode: values.smsCode.trim(), apiBase: window.location.origin, }) setPhoneMasked(prepared.phoneMasked) setWaiting(true) message.info('已发起本机验证,请在弹出的 DCAuth 窗口中确认') launchProtocol(prepared.launchUrl) const result = await waitForDevice(prepared) applyLoginResult(result) message.success('登录成功,欢迎回来!') navigate('/', { replace: true }) } catch (error) { const text = error instanceof Error ? error.message : '登录失败,请检查账号密码' if (text.includes('未绑定手机号')) { message.warning(`${text},可使用下方「旧账号仅密码登录」`) } else if (text.includes('绑定设备不一致')) { message.error('当前设备与账号绑定设备不一致,请联系管理员重置设备') } else { message.error(text) } } finally { submittingRef.current = false setLoading(false) setWaiting(false) } } const submitLegacyLogin = async () => { if (submittingRef.current) return let values: LoginForm try { values = await form.validateFields(['username', 'password']) } catch { return } submittingRef.current = true setLoading(true) try { await login(values.username.trim(), values.password) message.success('登录成功,欢迎回来!') navigate('/', { replace: true }) } catch (error) { message.error(error instanceof Error ? error.message : '账号密码登录失败') } finally { submittingRef.current = false setLoading(false) } } return (
S
Skin Hub 游戏皮肤分销平台 账号密码 + 短信验证码 + DCAuth 本机设备签名
{waiting ? ( ) : null}
} placeholder="用户名(支持中文 / 英文)" size="large" autoComplete="username" style={{ borderRadius: 8 }} /> } placeholder="密码" size="large" autoComplete="current-password" style={{ borderRadius: 8 }} /> } placeholder={`6 位短信验证码${phoneMasked ? `(${phoneMasked})` : ''}`} size="large" maxLength={6} autoComplete="one-time-code" style={{ borderRadius: 8 }} addonAfter={ } />
未绑定手机号的历史账号 已绑定手机号的账号必须通过短信验证码与 DCAuth 本机签名登录
) } async function waitForDevice(prepared: PreparedDeviceLogin) { for (let attempt = 0; attempt < 120; attempt += 1) { await sleep(1000) const status = await authApi.deviceChallengeStatus(prepared.challengeId, prepared.pollToken) if (status.status === 'authorized' && status.deviceTicket) { return authApi.completeLogin({ challengeId: prepared.challengeId, pollToken: prepared.pollToken, deviceTicket: status.deviceTicket, }) } if (status.status === 'expired' || status.status === 'rejected') { throw new Error(status.message || '本机验证已过期,请重新登录') } if (attempt === 4) { message.info('如果浏览器没有弹出 DCAuth,请先安装并运行一次 DCAuth 客户端') } } throw new Error('等待本机验证超时,请确认 DCAuth 已安装并允许浏览器唤起') } function launchProtocol(url: string) { const anchor = document.createElement('a') anchor.href = url anchor.style.display = 'none' document.body.appendChild(anchor) anchor.click() window.setTimeout(() => anchor.remove(), 1000) } function sleep(ms: number) { return new Promise((resolve) => window.setTimeout(resolve, ms)) }