diff --git a/frontend/index.html b/frontend/index.html index 0023e91..2de8b01 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,8 +2,15 @@ - + + + + + 游戏皮肤分销系统 diff --git a/frontend/public/apple-touch-icon.png b/frontend/public/apple-touch-icon.png new file mode 100644 index 0000000..50d027d Binary files /dev/null and b/frontend/public/apple-touch-icon.png differ diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 0000000..f93fad8 Binary files /dev/null and b/frontend/public/favicon.ico differ diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg index 6893eb1..a71c934 100644 --- a/frontend/public/favicon.svg +++ b/frontend/public/favicon.svg @@ -1 +1,24 @@ - \ No newline at end of file + + + + + + + + + + + + + + + + + diff --git a/frontend/public/icon-192.png b/frontend/public/icon-192.png new file mode 100644 index 0000000..2afe055 Binary files /dev/null and b/frontend/public/icon-192.png differ diff --git a/frontend/public/icon-512.png b/frontend/public/icon-512.png new file mode 100644 index 0000000..d856198 Binary files /dev/null and b/frontend/public/icon-512.png differ diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index 0e9a607..860c278 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -1,138 +1,202 @@ -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' +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 + username: string; + password: string; + smsCode: string; +}; + +/** 与站点图标同源的品牌标记:蓝紫渐变 + 白色宝石。 */ +function BrandMark({ size = 48 }: { size?: number }) { + return ( + + + + + + + + + + + + + ); } 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) + 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) - } - }, []) + if (timerRef.current) window.clearInterval(timerRef.current); + }; + }, []); const startCountdown = (seconds: number) => { - setCountdown(seconds) - if (timerRef.current) window.clearInterval(timerRef.current) + 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 + if (timerRef.current) window.clearInterval(timerRef.current); + return 0; } - return current - 1 - }) - }, 1000) - } + 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}`) + 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},可使用下方「旧账号仅密码登录」`) + const text = error instanceof Error ? error.message : "验证码发送失败"; + if (text.includes("未绑定手机号")) { + message.warning(`${text},可使用下方「旧账号仅密码登录」`); } else { - message.error(text) + message.error(text); } } finally { - setSending(false) + setSending(false); } - } + }; const onFinish = async (values: LoginForm) => { - if (submittingRef.current) return - submittingRef.current = true - setLoading(true) - setWaiting(false) + 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) + }); + setPhoneMasked(prepared.phoneMasked); + setWaiting(true); + message.info("已发起本机验证,请在弹出的 DCAuth 窗口中确认"); + launchProtocol(prepared.launchUrl); - const result = await waitForDevice(prepared) - applyLoginResult(result) - message.success('登录成功,欢迎回来!') - navigate('/', { replace: true }) + 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('当前设备与账号绑定设备不一致,请联系管理员重置设备') + const text = + error instanceof Error ? error.message : "登录失败,请检查账号密码"; + if (text.includes("未绑定手机号")) { + message.warning(`${text},可使用下方「旧账号仅密码登录」`); + } else if (text.includes("绑定设备不一致")) { + message.error("当前设备与账号绑定设备不一致,请联系管理员重置设备"); } else { - message.error(text) + message.error(text); } } finally { - submittingRef.current = false - setLoading(false) - setWaiting(false) + submittingRef.current = false; + setLoading(false); + setWaiting(false); } - } + }; const submitLegacyLogin = async () => { - if (submittingRef.current) return - let values: LoginForm + if (submittingRef.current) return; + let values: LoginForm; try { - values = await form.validateFields(['username', 'password']) + values = await form.validateFields(["username", "password"]); } catch { - return + return; } - submittingRef.current = true - setLoading(true) + submittingRef.current = true; + setLoading(true); try { - await login(values.username.trim(), values.password) - message.success('登录成功,欢迎回来!') - navigate('/', { replace: true }) + await login(values.username.trim(), values.password); + message.success("登录成功,欢迎回来!"); + navigate("/", { replace: true }); } catch (error) { - message.error(error instanceof Error ? error.message : '账号密码登录失败') + message.error( + error instanceof Error ? error.message : "账号密码登录失败", + ); } finally { - submittingRef.current = false - setLoading(false) + submittingRef.current = false; + setLoading(false); } - } + }; return (
- -
-
S
- + +
+
+ +
+ Skin Hub 游戏皮肤分销平台 - + 账号密码 + 短信验证码 + DCAuth 本机设备签名
@@ -146,14 +210,19 @@ export default function Login() { /> ) : null} -
+ } + prefix={} placeholder="用户名(支持中文 / 英文)" size="large" autoComplete="username" @@ -163,11 +232,11 @@ export default function Login() { } + prefix={} placeholder="密码" size="large" autoComplete="current-password" @@ -177,12 +246,14 @@ export default function Login() { } - placeholder={`6 位短信验证码${phoneMasked ? `(${phoneMasked})` : ''}`} + prefix={ + + } + placeholder={`6 位短信验证码${phoneMasked ? `(${phoneMasked})` : ""}`} size="large" maxLength={6} autoComplete="one-time-code" @@ -195,9 +266,9 @@ export default function Login() { loading={sending} disabled={countdown > 0} onClick={() => void sendCode()} - style={{ padding: 0, height: 'auto' }} + style={{ padding: 0, height: "auto" }} > - {countdown > 0 ? `${countdown}s 后重发` : '获取验证码'} + {countdown > 0 ? `${countdown}s 后重发` : "获取验证码"} } /> @@ -216,8 +287,9 @@ export default function Login() { borderRadius: 8, fontSize: 15, fontWeight: 600, - background: 'linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%)', - boxShadow: '0 4px 14px rgba(37, 99, 235, 0.4)', + background: + "linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%)", + boxShadow: "0 4px 14px rgba(37, 99, 235, 0.4)", }} > 安全登录 · 拉起本机验证 @@ -225,54 +297,71 @@ export default function Login() { - - 未绑定手机号的历史账号 + + + 未绑定手机号的历史账号 + - 已绑定手机号的账号必须通过短信验证码与 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) { + 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 (status.status === "expired" || status.status === "rejected") { + throw new Error(status.message || "本机验证已过期,请重新登录"); } if (attempt === 4) { - message.info('如果浏览器没有弹出 DCAuth,请先安装并运行一次 DCAuth 客户端') + message.info( + "如果浏览器没有弹出 DCAuth,请先安装并运行一次 DCAuth 客户端", + ); } } - throw new Error('等待本机验证超时,请确认 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) + 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)) + return new Promise((resolve) => window.setTimeout(resolve, ms)); }