diff --git a/web/frontend/index.html b/web/frontend/index.html index 64d1845..1fc4ee2 100644 --- a/web/frontend/index.html +++ b/web/frontend/index.html @@ -1,9 +1,16 @@ - + - + + + + + 直播运营后台 diff --git a/web/frontend/public/apple-touch-icon.png b/web/frontend/public/apple-touch-icon.png new file mode 100644 index 0000000..a373703 Binary files /dev/null and b/web/frontend/public/apple-touch-icon.png differ diff --git a/web/frontend/public/favicon.ico b/web/frontend/public/favicon.ico new file mode 100644 index 0000000..ac9eee9 Binary files /dev/null and b/web/frontend/public/favicon.ico differ diff --git a/web/frontend/public/favicon.svg b/web/frontend/public/favicon.svg index 6893eb1..3320651 100644 --- a/web/frontend/public/favicon.svg +++ b/web/frontend/public/favicon.svg @@ -1 +1,17 @@ - \ No newline at end of file + + + + + + + + + + + + + + + + + diff --git a/web/frontend/public/icon-192.png b/web/frontend/public/icon-192.png new file mode 100644 index 0000000..7fd4655 Binary files /dev/null and b/web/frontend/public/icon-192.png differ diff --git a/web/frontend/public/icon-512.png b/web/frontend/public/icon-512.png new file mode 100644 index 0000000..e3f106f Binary files /dev/null and b/web/frontend/public/icon-512.png differ diff --git a/web/frontend/src/api/client.ts b/web/frontend/src/api/client.ts index 73c368a..c1b8ed1 100644 --- a/web/frontend/src/api/client.ts +++ b/web/frontend/src/api/client.ts @@ -25,7 +25,15 @@ api.interceptors.response.use( if (typeof detail === 'string') { msg = detail; } else if (Array.isArray(detail) && detail.length > 0) { - msg = detail.map((d: any) => d?.msg || (typeof d === 'string' ? d : JSON.stringify(d))).join('; '); + msg = detail + .map((item: unknown) => { + if (typeof item === 'string') return item; + if (item && typeof item === 'object' && 'msg' in item) { + return String((item as { msg?: unknown }).msg ?? ''); + } + return JSON.stringify(item); + }) + .join('; '); } else if (detail && typeof detail === 'object') { msg = JSON.stringify(detail); } diff --git a/web/frontend/src/index.css b/web/frontend/src/index.css index 8fd9c73..11d89d0 100644 --- a/web/frontend/src/index.css +++ b/web/frontend/src/index.css @@ -34,3 +34,449 @@ html[data-theme='dark'] body { .huya-collab-home-drop .ant-upload-btn { padding: 10px 12px !important; } + +/* ---------- 登录页 ---------- */ +.login-page { + --login-card-bg: rgba(255, 255, 255, 0.92); + --login-border: rgba(15, 23, 42, 0.08); + --login-text: #0f172a; + --login-muted: #64748b; + --login-step-bg: rgba(15, 23, 42, 0.03); + position: relative; + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 40px 20px; + overflow: hidden; + background: + radial-gradient(900px 520px at 12% 8%, rgba(99, 102, 241, 0.28), transparent 62%), + radial-gradient(820px 520px at 88% 92%, rgba(6, 182, 212, 0.24), transparent 60%), + #eef2ff; +} + +.login-page.is-dark { + --login-card-bg: rgba(15, 23, 42, 0.86); + --login-border: rgba(148, 163, 184, 0.16); + --login-text: #e2e8f0; + --login-muted: #94a3b8; + --login-step-bg: rgba(148, 163, 184, 0.06); + background: + radial-gradient(900px 520px at 12% 8%, rgba(79, 70, 229, 0.32), transparent 62%), + radial-gradient(820px 520px at 88% 92%, rgba(6, 182, 212, 0.2), transparent 60%), + #070b16; +} + +.login-backdrop { + position: absolute; + inset: 0; + pointer-events: none; + background-image: + linear-gradient(rgba(148, 163, 184, 0.12) 1px, transparent 1px), + linear-gradient(90deg, rgba(148, 163, 184, 0.12) 1px, transparent 1px); + background-size: 46px 46px; + mask-image: radial-gradient(ellipse at 50% 42%, #000 26%, transparent 74%); + -webkit-mask-image: radial-gradient(ellipse at 50% 42%, #000 26%, transparent 74%); + opacity: 0.7; +} + +.login-page.is-dark .login-backdrop { + background-image: + linear-gradient(rgba(148, 163, 184, 0.16) 1px, transparent 1px), + linear-gradient(90deg, rgba(148, 163, 184, 0.16) 1px, transparent 1px); + opacity: 0.5; +} + +.login-shell { + position: relative; + display: grid; + grid-template-columns: minmax(0, 1.05fr) 440px; + width: min(1040px, 100%); + overflow: hidden; + border-radius: 28px; + border: 1px solid var(--login-border); + background: var(--login-card-bg); + box-shadow: 0 40px 90px -40px rgba(15, 23, 42, 0.55); + backdrop-filter: blur(18px); + -webkit-backdrop-filter: blur(18px); +} + +.login-brand { + position: relative; + display: flex; + flex-direction: column; + gap: 34px; + padding: 44px 46px; + color: #fff; + background: + radial-gradient(120% 120% at 0% 0%, rgba(255, 255, 255, 0.3), transparent 56%), + linear-gradient(150deg, #4338ca 0%, #2563eb 48%, #06b6d4 100%); +} + +.login-brand::before, +.login-brand::after { + content: ''; + position: absolute; + border-radius: 50%; + pointer-events: none; +} + +.login-brand::before { + width: 320px; + height: 320px; + right: -120px; + top: -90px; + background: rgba(255, 255, 255, 0.14); +} + +.login-brand::after { + width: 240px; + height: 240px; + left: -110px; + bottom: -120px; + background: rgba(7, 11, 22, 0.18); +} + +.login-brand-head { + position: relative; + display: flex; + align-items: center; + gap: 12px; +} + +.login-brand-head strong { + display: block; + font-size: 16px; + letter-spacing: 0.3px; +} + +.login-brand-head span { + display: block; + margin-top: 2px; + font-size: 11.5px; + letter-spacing: 1.2px; + text-transform: uppercase; + color: rgba(255, 255, 255, 0.66); +} + +.login-brand-body { + position: relative; + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; +} + +.login-brand-body h1 { + margin: 0 0 14px; + font-size: 34px; + line-height: 1.28; + letter-spacing: -0.6px; + font-weight: 700; +} + +.login-brand-body p { + margin: 0 0 30px; + max-width: 340px; + font-size: 14px; + line-height: 1.8; + color: rgba(255, 255, 255, 0.78); +} + +.login-features { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 16px; +} + +.login-features li { + display: flex; + align-items: center; + gap: 12px; +} + +.login-feature-icon { + display: grid; + place-items: center; + width: 38px; + height: 38px; + flex: none; + border-radius: 12px; + font-size: 17px; + color: #fff; + background: rgba(255, 255, 255, 0.16); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.login-features strong { + display: block; + font-size: 13.5px; + font-weight: 600; +} + +.login-features em { + display: block; + margin-top: 2px; + font-style: normal; + font-size: 12.5px; + color: rgba(255, 255, 255, 0.7); +} + +.login-brand-foot { + position: relative; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding-top: 18px; + font-size: 12px; + color: rgba(255, 255, 255, 0.7); + border-top: 1px solid rgba(255, 255, 255, 0.2); +} + +.login-version { + padding: 2px 8px; + border-radius: 999px; + font-variant-numeric: tabular-nums; + color: #fff; + background: rgba(255, 255, 255, 0.16); +} + +.login-card { + display: flex; + flex-direction: column; + padding: 42px 40px 30px; + min-height: 560px; +} + +.login-card-head { + display: flex; + align-items: center; + gap: 14px; + margin-bottom: 22px; +} + +.login-card-head h2 { + margin: 0; + font-size: 23px; + line-height: 1.3; + letter-spacing: -0.3px; + color: var(--login-text); +} + +.login-card-head p { + margin: 4px 0 0; + font-size: 12.5px; + color: var(--login-muted); +} + +.login-logo { + flex: none; + border-radius: 14px; + filter: drop-shadow(0 10px 18px rgba(37, 99, 235, 0.28)); +} + +.login-steps { + list-style: none; + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; + margin: 0 0 20px; + padding: 0; +} + +.login-steps li { + display: flex; + align-items: center; + gap: 6px; + padding: 9px 10px; + border-radius: 12px; + border: 1px solid var(--login-border); + background: var(--login-step-bg); + font-size: 12.5px; + color: var(--login-muted); + transition: all 0.2s ease; +} + +.login-steps li.is-active { + color: #38bdf8; + border-color: rgba(56, 189, 248, 0.45); + background: rgba(56, 189, 248, 0.12); +} + +.login-step-icon { + display: inline-flex; + font-size: 14px; +} + +.login-form .ant-form-item { + margin-bottom: 16px; +} + +.login-form .ant-form-item-label { + padding-bottom: 4px; +} + +.login-form .ant-form-item-label > label { + font-size: 12.5px; + color: var(--login-muted); +} + +.login-form .ant-input-affix-wrapper, +.login-form .ant-input { + border-radius: 11px; +} + +.login-form .ant-input-affix-wrapper .ant-input { + border-radius: 0; +} + +.login-code-btn { + height: auto !important; + padding: 0 2px !important; + font-size: 12.5px; +} + +.login-submit-item { + margin-top: 22px; + margin-bottom: 0 !important; +} + +.login-submit-item .ant-btn { + height: 46px; + border-radius: 12px; + font-weight: 600; + letter-spacing: 0.3px; +} + +.login-submit-item .ant-btn-primary { + border: none; + background: linear-gradient(135deg, #4f46e5 0%, #2563eb 55%, #06b6d4 100%); + box-shadow: 0 16px 30px -16px rgba(37, 99, 235, 0.9); +} + +.login-submit-item .ant-btn-primary:not(:disabled):hover { + background: linear-gradient(135deg, #4338ca 0%, #1d4ed8 55%, #0891b2 100%); +} + +.login-waiting { + display: flex; + gap: 12px; + margin-bottom: 16px; + padding: 14px; + border-radius: 14px; + border: 1px solid rgba(56, 189, 248, 0.35); + background: rgba(56, 189, 248, 0.1); +} + +.login-waiting-icon { + display: grid; + place-items: center; + width: 32px; + height: 32px; + flex: none; + border-radius: 50%; + font-size: 17px; + color: #38bdf8; + background: rgba(56, 189, 248, 0.16); + animation: login-pulse 1.4s ease-in-out infinite; +} + +@keyframes login-pulse { + 0%, + 100% { + transform: scale(1); + opacity: 0.85; + } + 50% { + transform: scale(1.08); + opacity: 1; + } +} + +.login-waiting-body strong { + display: block; + font-size: 13.5px; + color: var(--login-text); +} + +.login-waiting-body p { + margin: 4px 0 9px; + font-size: 12px; + line-height: 1.6; + color: var(--login-muted); +} + +.login-copy { + font-size: 12px; +} + +.login-notice { + display: flex; + align-items: flex-start; + gap: 8px; + margin-bottom: 18px; + padding: 12px 14px; + border-radius: 12px; + border: 1px solid rgba(245, 158, 11, 0.3); + background: rgba(245, 158, 11, 0.1); + color: #b45309; + font-size: 12.5px; + line-height: 1.65; +} + +.login-page.is-dark .login-notice { + color: #fbbf24; +} + +.login-switch { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + margin-top: auto; + padding-top: 16px; + border-top: 1px dashed var(--login-border); + font-size: 12.5px; + color: var(--login-muted); +} + +.login-switch-center { + justify-content: center; + border-top: none; + padding-top: 8px; +} + +@media (max-width: 980px) { + .login-shell { + grid-template-columns: 1fr; + width: min(462px, 100%); + } + + .login-brand { + display: none; + } + + .login-card { + min-height: 0; + padding: 34px 26px 26px; + } +} + +@media (max-width: 420px) { + .login-page { + padding: 16px; + } + + .login-steps li { + padding: 8px 6px; + font-size: 11.5px; + } + + .login-card-head h2 { + font-size: 21px; + } +} diff --git a/web/frontend/src/pages/LoginPage.tsx b/web/frontend/src/pages/LoginPage.tsx index 560120c..b86559c 100644 --- a/web/frontend/src/pages/LoginPage.tsx +++ b/web/frontend/src/pages/LoginPage.tsx @@ -1,11 +1,16 @@ import { useEffect, useRef, useState } from 'react'; -import { Alert, Button, Card, Divider, Form, Input, Space, Typography } from 'antd'; +import { Button, Form, Input, Space, Typography } from 'antd'; import type { InputRef } from 'antd'; import { message } from '../utils/antdMessage'; import { + ArrowLeftOutlined, + DesktopOutlined, + LoadingOutlined, LockOutlined, MobileOutlined, + ReloadOutlined, SafetyCertificateOutlined, + SafetyOutlined, UserOutlined, } from '@ant-design/icons'; import { useNavigate } from 'react-router-dom'; @@ -16,11 +21,50 @@ import { getErrorMessage } from '../utils/error'; import { useTheme } from '../store/useTheme'; import { useAppInfo } from '../hooks/useAppInfo'; -const { Title, Text } = Typography; +const { Text } = Typography; type LoginMode = 'secure' | 'legacy'; type LoginValues = { username: string; password: string; smsCode?: string }; +const FLOW_STEPS = [ + { title: '账号密码', icon: }, + { title: '短信验证码', icon: }, + { title: '本机确认', icon: }, +]; + +const BRAND_FEATURES = [ + { icon: , title: '账号密码', desc: '第一重校验,拒绝弱口令' }, + { icon: , title: '短信验证码', desc: '绑定手机接收一次性验证码' }, + { icon: , title: '本机 DCAuth', desc: 'Ed25519 设备签名,换机即失效' }, +]; + +function LogoMark({ size = 40 }: { size?: number }) { + return ( + + + + + + + + + + + + + ); +} + function errorCodeOf(error: unknown): string { return (error as { errorCode?: string } | null)?.errorCode || ''; } @@ -228,161 +272,235 @@ export default function LoginPage({ onLogin }: { onLogin?: () => void }) { } return ( -
- - - 直播运营后台 - - - 当前版本 {versionText} - +
+
); }