feat: 全站 PageShell 设计系统与专业 UI 统一
- 新增 PageShell/PageTabs/ContentPanel/PageLoading/PageEmpty 页面骨架 - index.css 设计令牌文档化,布局令牌与纯色主按钮(去除渐变粉彩) - 全站路由页接入 PageShell,云账单三 Tab 为标杆 - 主题体系:跟随系统 + 主流/开发者分组,新增 Dracula/Tokyo Night,One Dark 暖灰差异化 - 登录页/侧栏/顶栏去渐变光球,企业克制风 - 修复 AIAlertAnalyzer JSX 与构建依赖导入问题
This commit is contained in:
1 parent
1acb0c49af
commit
e363ded445
213 files changed
+7148
-6818
No files matched your search
@@ -1,10 +1,11 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { User, Lock, Palette, Check, Loader2, AlertCircle, Eye, EyeOff, Languages } from 'lucide-react';
|
||||
import { User, Lock, Palette, Check, Loader2, AlertCircle, Eye, EyeOff, Languages, Activity } from 'lucide-react';
|
||||
import { useUserStore, useLanguageStore } from '@/store';
|
||||
import { authApi } from '@/api';
|
||||
import { useTheme, THEME_OPTIONS } from '@/hooks/useTheme';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { ThemePicker } from '@/components/layout/ThemePicker';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { SUPPORTED_LANGUAGES, type AppLanguage } from '@/i18n';
|
||||
import {
|
||||
@@ -16,58 +17,7 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
|
||||
/* ---------- CSS Animations ---------- */
|
||||
const ANIMATIONS = `
|
||||
@keyframes login-orb-1 {
|
||||
0%, 100% { transform: translate(0, 0) scale(1); }
|
||||
33% { transform: translate(40px, -50px) scale(1.12); }
|
||||
66% { transform: translate(-30px, 35px) scale(0.92); }
|
||||
}
|
||||
@keyframes login-orb-2 {
|
||||
0%, 100% { transform: translate(0, 0) scale(1); }
|
||||
50% { transform: translate(-50px, 25px) scale(1.18); }
|
||||
}
|
||||
@keyframes login-orb-3 {
|
||||
0%, 100% { transform: translate(0, 0) scale(1); }
|
||||
50% { transform: translate(30px, 45px) scale(0.88); }
|
||||
}
|
||||
@keyframes login-orb-4 {
|
||||
0%, 100% { transform: translate(0, 0) scale(1); }
|
||||
50% { transform: translate(25px, -30px) scale(1.1); }
|
||||
}
|
||||
@keyframes login-card-enter {
|
||||
from { opacity: 0; transform: translateY(24px) scale(0.97); }
|
||||
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||
}
|
||||
@keyframes login-fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
@keyframes login-input-focus {
|
||||
from { box-shadow: 0 0 0 0px var(--dg-input-focus-ring); }
|
||||
to { box-shadow: 0 0 0 4px var(--dg-input-focus-ring); }
|
||||
}
|
||||
`;
|
||||
|
||||
/* ---------- Floating background orbs ---------- */
|
||||
const ORBS = [
|
||||
{ top: '-10%', left: '-5%', size: 520, color: 'var(--dg-blue)', opacity: 0.18, blur: 90, animation: 'login-orb-1 18s ease-in-out infinite' },
|
||||
{ bottom: '-15%', right: '-8%', size: 480, color: 'var(--dg-purple)', opacity: 0.16, blur: 100, animation: 'login-orb-2 22s ease-in-out infinite' },
|
||||
{ top: '30%', right: '10%', size: 320, color: 'var(--dg-cyan)', opacity: 0.12, blur: 70, animation: 'login-orb-3 16s ease-in-out infinite' },
|
||||
{ bottom: '10%', left: '8%', size: 380, color: 'var(--dg-gradient-start)', opacity: 0.14, blur: 80, animation: 'login-orb-4 20s ease-in-out infinite' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Login - 优化后的登录页面
|
||||
*
|
||||
* 优化点:
|
||||
* 1. 使用 glass 效果替代简单的半透明背景
|
||||
* 2. 输入框使用 dg-input 工具类
|
||||
* 3. 密码显示/隐藏使用图标按钮
|
||||
* 4. 主题切换更精致
|
||||
* 5. 错误提示动画更流畅
|
||||
* 6. 背景光球动画更协调
|
||||
*/
|
||||
/** 登录页 — 克制企业风,无渐变/粉彩色光球 // Logan.Li */
|
||||
export function Login() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
@@ -78,18 +28,13 @@ export function Login() {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await authApi.login(username, password) as { user: any; token: string; permissions?: string[] };
|
||||
const result = await authApi.login(username, password) as { user: { id: number; username: string }; token: string; permissions?: string[] };
|
||||
useUserStore.getState().login(result.user, result.token, result.permissions ?? null);
|
||||
localStorage.setItem('token', result.token);
|
||||
navigate('/', { replace: true });
|
||||
@@ -102,297 +47,166 @@ export function Login() {
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>{ANIMATIONS}</style>
|
||||
|
||||
<div className="relative min-h-screen flex items-center justify-center px-4 bg-[var(--surface-canvas)]">
|
||||
{/* 极弱品牌氛围 — 单色径向,非彩色光球 // Logan.Li */}
|
||||
<div
|
||||
className="relative min-h-screen flex items-center justify-center px-4 overflow-hidden"
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-0"
|
||||
style={{
|
||||
background: `radial-gradient(ellipse at 50% 0%, ${getComputedStyle(document.documentElement).getPropertyValue('--dg-bg-secondary')} 0%, ${getComputedStyle(document.documentElement).getPropertyValue('--dg-bg-primary')} 100%)`,
|
||||
background: 'radial-gradient(ellipse 80% 50% at 50% -10%, color-mix(in srgb, var(--brand) 6%, transparent), transparent)',
|
||||
}}
|
||||
>
|
||||
{/* Floating light spots */}
|
||||
{ORBS.map((orb, i) => (
|
||||
<div
|
||||
key={`orb-${i}`}
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute"
|
||||
style={{
|
||||
top: orb.top,
|
||||
left: orb.left,
|
||||
bottom: orb.bottom,
|
||||
right: orb.right,
|
||||
width: orb.size,
|
||||
height: orb.size,
|
||||
borderRadius: '50%',
|
||||
background: `radial-gradient(circle, ${orb.color} 0%, transparent 70%)`,
|
||||
opacity: mounted ? orb.opacity : 0,
|
||||
filter: `blur(${orb.blur}px)`,
|
||||
animation: orb.animation,
|
||||
willChange: 'transform',
|
||||
transition: 'opacity 1s ease-out',
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
/>
|
||||
|
||||
{/* Theme + Language switcher */}
|
||||
<div
|
||||
className="fixed top-4 right-4 z-50 flex items-center gap-2"
|
||||
style={{ animation: 'login-fade-in 0.6s ease-out' }}
|
||||
>
|
||||
{/* Language switcher */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('header.language')}
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center size-10 rounded-full',
|
||||
'border border-[var(--dg-border)] transition-all duration-200',
|
||||
'hover:scale-105 active:scale-95 hover:border-[var(--dg-border-hover)]',
|
||||
'glass'
|
||||
)}
|
||||
style={{ color: 'var(--dg-text-secondary)' }}
|
||||
>
|
||||
<Languages className="size-5" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
className="w-40 bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]"
|
||||
style={{ boxShadow: 'var(--shadow-dropdown)' }}
|
||||
>
|
||||
<DropdownMenuLabel className="text-[var(--dg-text-secondary)]">{t('header.languageSwitch')}</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator className="bg-[var(--dg-border)]" />
|
||||
{SUPPORTED_LANGUAGES.map((lang) => (
|
||||
<DropdownMenuItem
|
||||
key={lang.code}
|
||||
className={cn(
|
||||
'flex items-center gap-2.5 cursor-pointer transition-colors',
|
||||
'focus:bg-[var(--dg-bg-hover)] focus:text-[var(--dg-text-primary)]',
|
||||
language === lang.code ? 'text-[var(--dg-text-primary)]' : 'text-[var(--dg-text-secondary)]',
|
||||
)}
|
||||
onClick={() => setLanguage(lang.code as AppLanguage)}
|
||||
>
|
||||
<span className="text-base">{lang.flag}</span>
|
||||
<span className="flex-1 text-sm">{lang.label}</span>
|
||||
{language === lang.code && <Check className="size-4 shrink-0 text-[var(--dg-blue)]" />}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* Theme switcher */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('header.theme')}
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center size-10 rounded-full',
|
||||
'border border-[var(--dg-border)] transition-all duration-200',
|
||||
'hover:scale-105 active:scale-95 hover:border-[var(--dg-border-hover)]',
|
||||
'glass'
|
||||
)}
|
||||
style={{ color: 'var(--dg-text-secondary)' }}
|
||||
>
|
||||
<Palette className="size-5" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
className="w-60 bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]"
|
||||
style={{ boxShadow: 'var(--shadow-dropdown)' }}
|
||||
>
|
||||
<DropdownMenuLabel className="text-[var(--dg-text-secondary)]">{t('header.themeSwitch')}</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator className="bg-[var(--dg-border)]" />
|
||||
{THEME_OPTIONS.map((option) => (
|
||||
<DropdownMenuItem
|
||||
key={option.id}
|
||||
className={cn(
|
||||
'flex items-center gap-2.5 cursor-pointer transition-colors',
|
||||
'focus:bg-[var(--dg-bg-hover)] focus:text-[var(--dg-text-primary)]',
|
||||
theme === option.id ? 'text-[var(--dg-text-primary)]' : 'text-[var(--dg-text-secondary)]',
|
||||
)}
|
||||
onClick={() => setTheme(option.id)}
|
||||
>
|
||||
<span
|
||||
className="size-3.5 rounded-full shrink-0 ring-2"
|
||||
style={{
|
||||
backgroundColor: option.primaryColor,
|
||||
boxShadow: '0 0 0 2px var(--dg-bg-primary)',
|
||||
}}
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="text-sm font-medium block">{option.name}</span>
|
||||
<span className="text-xs opacity-60 block truncate">{option.description}</span>
|
||||
</div>
|
||||
{theme === option.id && <Check className="size-4 shrink-0 text-[var(--dg-blue)]" />}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
{/* Login card */}
|
||||
<div
|
||||
className="relative z-10 w-full max-w-md"
|
||||
style={{
|
||||
background: 'color-mix(in srgb, var(--dg-bg-secondary) 80%, transparent)',
|
||||
backdropFilter: 'blur(24px)',
|
||||
WebkitBackdropFilter: 'blur(24px)',
|
||||
border: '1px solid var(--dg-border)',
|
||||
borderRadius: 24,
|
||||
boxShadow: 'var(--shadow-card-hover)',
|
||||
padding: '44px 40px',
|
||||
animation: 'login-card-enter 0.6s cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
}}
|
||||
>
|
||||
{/* Brand */}
|
||||
<div className="text-center mb-8">
|
||||
<div
|
||||
className="inline-flex items-center justify-center size-16 rounded-2xl mb-5"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, var(--dg-gradient-start), var(--dg-gradient-end))',
|
||||
boxShadow: '0 8px 32px color-mix(in srgb, var(--dg-gradient-start) 30%, transparent)',
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
className="size-8 text-white"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.8}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M3 12h4l3-8 4 16 3-8h4" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold dg-gradient-text tracking-tight">
|
||||
AIOps
|
||||
</h1>
|
||||
<p className="mt-2.5 text-sm text-[var(--dg-text-secondary)]">
|
||||
{t('login.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div
|
||||
className="flex items-start gap-2.5 rounded-xl px-4 py-3 text-sm animate-fade-in"
|
||||
style={{
|
||||
background: 'color-mix(in srgb, var(--dg-red) 10%, transparent)',
|
||||
border: '1px solid color-mix(in srgb, var(--dg-red) 25%, transparent)',
|
||||
color: 'var(--dg-red)',
|
||||
}}
|
||||
>
|
||||
<AlertCircle className="size-4 shrink-0 mt-0.5" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Username */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-sm font-medium text-[var(--dg-text-secondary)]">
|
||||
{t('login.username')}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<User
|
||||
className="absolute left-3.5 top-1/2 -translate-y-1/2 size-4 pointer-events-none text-[var(--dg-text-muted)]"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
placeholder={t('login.usernamePlaceholder')}
|
||||
autoComplete="username"
|
||||
autoFocus
|
||||
className="dg-input w-full py-2.5 pl-10 pr-4 text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Password */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-sm font-medium text-[var(--dg-text-secondary)]">
|
||||
{t('login.password')}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Lock
|
||||
className="absolute left-3.5 top-1/2 -translate-y-1/2 size-4 pointer-events-none text-[var(--dg-text-muted)]"
|
||||
/>
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder={t('login.passwordPlaceholder')}
|
||||
autoComplete="current-password"
|
||||
className="dg-input w-full py-2.5 pl-10 pr-12 text-sm"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword((v) => !v)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-[var(--dg-text-muted)] hover:text-[var(--dg-text-secondary)] transition-colors"
|
||||
tabIndex={-1}
|
||||
>
|
||||
{showPassword ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submit */}
|
||||
{/* 语言 / 主题 */}
|
||||
<div className="fixed top-4 right-4 z-50 flex items-center gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !username || !password}
|
||||
className={cn(
|
||||
'dg-gradient-bg w-full rounded-xl py-3 mt-2 text-white font-semibold text-[15px]',
|
||||
'transition-all duration-200 active:scale-[0.98] disabled:cursor-not-allowed',
|
||||
)}
|
||||
style={{
|
||||
opacity: loading || !username || !password ? 0.6 : 1,
|
||||
boxShadow: loading || !username || !password
|
||||
? 'none'
|
||||
: '0 6px 24px color-mix(in srgb, var(--dg-gradient-start) 30%, transparent)',
|
||||
}}
|
||||
type="button"
|
||||
aria-label={t('header.language')}
|
||||
className="dg-btn-secondary h-9 w-9 p-0 rounded-lg"
|
||||
>
|
||||
{loading ? (
|
||||
<span className="inline-flex items-center justify-center gap-2">
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
{t('login.loggingIn')}
|
||||
</span>
|
||||
) : (
|
||||
t('login.loginButton')
|
||||
)}
|
||||
<Languages className="size-4" />
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{/* Hints */}
|
||||
<div
|
||||
className="mt-6 pt-5 text-center text-xs space-y-1"
|
||||
style={{
|
||||
borderTop: '1px solid var(--dg-border-light)',
|
||||
color: 'var(--dg-text-muted)',
|
||||
}}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
className="w-40 bg-[var(--surface-raised)] border-[var(--border-default)]"
|
||||
style={{ boxShadow: 'var(--shadow-dropdown)' }}
|
||||
>
|
||||
<p>
|
||||
默认管理员账号{' '}
|
||||
<span className="font-medium text-[var(--dg-text-secondary)]">admin / 123456</span>
|
||||
</p>
|
||||
<p>LDAP 用户可直接使用域账号登录</p>
|
||||
<DropdownMenuLabel className="text-[var(--text-secondary)]">{t('header.languageSwitch')}</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator className="bg-[var(--border-default)]" />
|
||||
{SUPPORTED_LANGUAGES.map((lang) => (
|
||||
<DropdownMenuItem
|
||||
key={lang.code}
|
||||
className={cn(
|
||||
'flex items-center gap-2.5 cursor-pointer',
|
||||
language === lang.code ? 'text-[var(--text-primary)]' : 'dg-text-secondary',
|
||||
)}
|
||||
onClick={() => setLanguage(lang.code as AppLanguage)}
|
||||
>
|
||||
<span className="text-base">{lang.flag}</span>
|
||||
<span className="flex-1 text-sm">{lang.label}</span>
|
||||
{language === lang.code && <Check className="size-4 shrink-0 text-[var(--brand)]" />}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('header.theme')}
|
||||
className="dg-btn-secondary h-9 w-9 p-0 rounded-lg"
|
||||
>
|
||||
<Palette className="size-4" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
className="w-60 bg-[var(--surface-raised)] border-[var(--border-default)]"
|
||||
style={{ boxShadow: 'var(--shadow-dropdown)' }}
|
||||
>
|
||||
<DropdownMenuLabel className="text-[var(--text-secondary)]">{t('header.themeSwitch')}</DropdownMenuLabel>
|
||||
<ThemePicker value={theme} onChange={setTheme} />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
{/* 登录卡片 */}
|
||||
<div className="relative z-10 w-full max-w-md dg-card rounded-xl p-10 animate-fade-in">
|
||||
<div className="text-center mb-8">
|
||||
<div className="dg-icon-box w-14 h-14 rounded-xl mx-auto mb-4">
|
||||
<Activity className="size-7" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-[var(--text-primary)] tracking-tight">
|
||||
AIOps
|
||||
</h1>
|
||||
<p className="mt-2 text-sm dg-text-secondary">
|
||||
{t('login.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<p
|
||||
className="absolute bottom-4 left-0 right-0 text-center text-xs"
|
||||
style={{ color: 'var(--dg-text-muted)', opacity: 0.5 }}
|
||||
>
|
||||
© AIOps 智能运维平台
|
||||
</p>
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||
{error && (
|
||||
<div className="flex items-start gap-2.5 rounded-lg px-4 py-3 text-sm dg-alert-banner border-[var(--alert-error-border)] bg-[var(--alert-error-bg)] text-[var(--alert-error-text)]">
|
||||
<AlertCircle className="size-4 shrink-0 mt-0.5" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-sm font-medium dg-text-secondary">
|
||||
{t('login.username')}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<User className="absolute left-3.5 top-1/2 -translate-y-1/2 size-4 pointer-events-none dg-text-muted" />
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
placeholder={t('login.usernamePlaceholder')}
|
||||
autoComplete="username"
|
||||
autoFocus
|
||||
className="dg-input w-full py-2.5 pl-10 pr-4 text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-sm font-medium dg-text-secondary">
|
||||
{t('login.password')}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-3.5 top-1/2 -translate-y-1/2 size-4 pointer-events-none dg-text-muted" />
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder={t('login.passwordPlaceholder')}
|
||||
autoComplete="current-password"
|
||||
className="dg-input w-full py-2.5 pl-10 pr-12 text-sm"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword((v) => !v)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 dg-text-muted hover:dg-text-secondary transition-colors"
|
||||
tabIndex={-1}
|
||||
>
|
||||
{showPassword ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !username || !password}
|
||||
className="dg-btn-primary w-full rounded-lg py-2.5 mt-1 text-[15px] font-semibold disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? (
|
||||
<span className="inline-flex items-center justify-center gap-2">
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
{t('login.loggingIn')}
|
||||
</span>
|
||||
) : (
|
||||
t('login.loginButton')
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 pt-5 text-center text-xs space-y-1 border-t border-[var(--border-subtle)] dg-text-muted">
|
||||
<p>
|
||||
默认管理员账号{' '}
|
||||
<span className="font-medium dg-text-secondary">admin / 123456</span>
|
||||
</p>
|
||||
<p>LDAP 用户可直接使用域账号登录</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
<p className="absolute bottom-4 left-0 right-0 text-center text-xs dg-text-muted opacity-60">
|
||||
© AIOps 智能运维平台
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user