feat: 5种专业登录页风格 + 外观设置 + 文档全面更新
- 新增 5 种登录页组件: SplitView/CenteredCard/SidePanel/FullImage/MinimalDark - 提取 useLogin hook 共享登录逻辑 - LoginPage 改为风格分发器, localStorage(login-style) 控制渲染 - SettingsPage 新增外观设置 tab: 主题切换 + 登录风格选择器(含缩略图预览) - AppearanceSettings 组件: 5 种风格卡片式切换 - 文档全面更新为 Go/Gin 架构: - ARCHITECTURE.md: Go 项目结构/handler/GORM 模型/中间件/LDAP/驱动矩阵 - FEATURES.md: v0.3.1 功能清单含登录主题系统 - ROADMAP.md: Python→Go 迁移历史 + Sprint 计划
This commit is contained in:
1 parent
f2e93a09c0
commit
14e394fb09
12 files changed
+1147
-639
No files matched your search
@@ -0,0 +1,61 @@
|
||||
import { useLogin } from '@/hooks/useLogin';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { Sun, Moon } from 'lucide-react';
|
||||
|
||||
export default function CenteredCard() {
|
||||
const { username, password, loading, error, setUsername, setPassword, handleLogin } = useLogin();
|
||||
const { toggleTheme, isDark } = useTheme();
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24, position: 'relative' }}>
|
||||
{/* Gradient background */}
|
||||
<div className="dg-gradient-br" style={{ position: 'fixed', inset: 0, zIndex: 0 }} />
|
||||
|
||||
{/* Theme toggle */}
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
style={{ position: 'fixed', top: 20, right: 20, zIndex: 50, background: 'rgba(255,255,255,0.1)', border: 'none', borderRadius: 8, padding: 8, cursor: 'pointer', color: '#fff', backdropFilter: 'blur(4px)' }}
|
||||
>
|
||||
{isDark ? <Sun size={18} /> : <Moon size={18} />}
|
||||
</button>
|
||||
|
||||
{/* Card */}
|
||||
<div style={{ position: 'relative', zIndex: 10, width: '100%', maxWidth: 400, background: 'var(--dg-bg-primary)', borderRadius: 14, padding: '40px 32px', boxShadow: '0 8px 32px rgba(0,0,0,0.3)' }}>
|
||||
<div style={{ textAlign: 'center', marginBottom: 32 }}>
|
||||
<h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 6 }}>
|
||||
<span className="dg-gradient-text">SQL Audit Platform</span>
|
||||
</h1>
|
||||
<p style={{ color: 'var(--dg-text-muted)', fontSize: 14 }}>Sign in to your account</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleLogin} style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
{error && <div style={{ color: '#f87171', fontSize: 13, textAlign: 'center' }}>{error}</div>}
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
style={{ padding: '11px 14px', borderRadius: 8, background: 'var(--dg-bg-secondary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)', fontSize: 14, outline: 'none' }}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
style={{ padding: '11px 14px', borderRadius: 8, background: 'var(--dg-bg-secondary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)', fontSize: 14, outline: 'none' }}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !username || !password}
|
||||
className="dg-gradient-bg"
|
||||
style={{ padding: '11px 14px', borderRadius: 8, border: 'none', color: '#fff', fontWeight: 600, fontSize: 14, cursor: loading || !username || !password ? 'not-allowed' : 'pointer', opacity: loading || !username || !password ? 0.6 : 1, marginTop: 4 }}
|
||||
>
|
||||
{loading ? 'Signing in…' : 'Sign in'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { useLogin } from '@/hooks/useLogin';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { Sun, Moon } from 'lucide-react';
|
||||
|
||||
export default function FullImage() {
|
||||
const { username, password, loading, error, setUsername, setPassword, handleLogin } = useLogin();
|
||||
const { toggleTheme, isDark } = useTheme();
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24, position: 'relative', overflow: 'hidden' }}>
|
||||
{/* Circuit board pattern background */}
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed', inset: 0, zIndex: 0,
|
||||
background: `
|
||||
repeating-linear-gradient(0deg, transparent, transparent 48px, var(--dg-border) 48px, var(--dg-border) 49px),
|
||||
repeating-linear-gradient(90deg, transparent, transparent 48px, var(--dg-border) 48px, var(--dg-border) 49px),
|
||||
var(--dg-bg-primary)
|
||||
`,
|
||||
opacity: 0.5,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Theme toggle */}
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
style={{ position: 'fixed', top: 20, right: 20, zIndex: 50, background: 'var(--dg-bg-tertiary)', border: 'none', borderRadius: 8, padding: 8, cursor: 'pointer', color: 'var(--dg-text-secondary)', backdropFilter: 'blur(8px)' }}
|
||||
>
|
||||
{isDark ? <Sun size={18} /> : <Moon size={18} />}
|
||||
</button>
|
||||
|
||||
{/* Glass card */}
|
||||
<div
|
||||
style={{
|
||||
position: 'relative', zIndex: 10, width: '100%', maxWidth: 400,
|
||||
background: 'rgba(0,0,0,0.45)', backdropFilter: 'blur(16px)',
|
||||
borderRadius: 16, padding: '40px 32px',
|
||||
color: '#fff',
|
||||
}}
|
||||
>
|
||||
<div style={{ textAlign: 'center', marginBottom: 32 }}>
|
||||
<h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 6 }}>
|
||||
<span className="dg-gradient-text">SQL Audit Platform</span>
|
||||
</h1>
|
||||
<p style={{ color: 'rgba(255,255,255,0.6)', fontSize: 14 }}>Sign in to your workspace</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleLogin} style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
{error && <div style={{ color: '#f87171', fontSize: 13, textAlign: 'center' }}>{error}</div>}
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
style={{ padding: '11px 14px', borderRadius: 8, background: 'rgba(255,255,255,0.08)', border: '1px solid rgba(255,255,255,0.12)', color: '#fff', fontSize: 14, outline: 'none' }}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
style={{ padding: '11px 14px', borderRadius: 8, background: 'rgba(255,255,255,0.08)', border: '1px solid rgba(255,255,255,0.12)', color: '#fff', fontSize: 14, outline: 'none' }}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !username || !password}
|
||||
className="dg-gradient-bg"
|
||||
style={{ padding: '11px 14px', borderRadius: 8, border: 'none', color: '#fff', fontWeight: 600, fontSize: 14, cursor: loading || !username || !password ? 'not-allowed' : 'pointer', opacity: loading || !username || !password ? 0.6 : 1, marginTop: 4 }}
|
||||
>
|
||||
{loading ? 'Signing in…' : 'Sign in'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { useLogin } from '@/hooks/useLogin';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { Sun, Moon, Database } from 'lucide-react';
|
||||
|
||||
export default function MinimalDark() {
|
||||
const { username, password, loading, error, setUsername, setPassword, handleLogin } = useLogin();
|
||||
const { toggleTheme, isDark } = useTheme();
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: 24, background: 'var(--dg-bg-primary)', color: 'var(--dg-text-primary)' }}>
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
style={{ position: 'fixed', top: 20, right: 20, zIndex: 50, background: 'var(--dg-bg-tertiary)', border: 'none', borderRadius: 8, padding: 8, cursor: 'pointer', color: 'var(--dg-text-secondary)' }}
|
||||
>
|
||||
{isDark ? <Sun size={18} /> : <Moon size={18} />}
|
||||
</button>
|
||||
|
||||
<div style={{ width: '100%', maxWidth: 320, textAlign: 'center' }}>
|
||||
<Database size={36} className="dg-gradient-text" style={{ marginBottom: 16, display: 'inline-block' }} />
|
||||
<h1 style={{ fontSize: 22, fontWeight: 700, marginBottom: 4 }}>
|
||||
<span className="dg-gradient-text">SQL Audit Platform</span>
|
||||
</h1>
|
||||
<p style={{ color: 'var(--dg-text-muted)', fontSize: 13, marginBottom: 36 }}>Secure database auditing & compliance</p>
|
||||
|
||||
<form onSubmit={handleLogin} style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
|
||||
{error && <div style={{ color: '#f87171', fontSize: 13 }}>{error}</div>}
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
style={{ padding: '10px 14px', borderRadius: 8, background: 'var(--dg-bg-secondary)', border: 'none', color: 'var(--dg-text-primary)', fontSize: 14, outline: 'none' }}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
style={{ padding: '10px 14px', borderRadius: 8, background: 'var(--dg-bg-secondary)', border: 'none', color: 'var(--dg-text-primary)', fontSize: 14, outline: 'none' }}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !username || !password}
|
||||
className="dg-gradient-bg"
|
||||
style={{ padding: '10px 14px', borderRadius: 8, border: 'none', color: '#fff', fontWeight: 600, fontSize: 14, cursor: loading || !username || !password ? 'not-allowed' : 'pointer', opacity: loading || !username || !password ? 0.6 : 1, marginTop: 8 }}
|
||||
>
|
||||
{loading ? 'Signing in…' : 'Sign in'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { useLogin } from '@/hooks/useLogin';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { Sun, Moon, Database } from 'lucide-react';
|
||||
|
||||
export default function SidePanel() {
|
||||
const { username, password, loading, error, setUsername, setPassword, handleLogin } = useLogin();
|
||||
const { toggleTheme, isDark } = useTheme();
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', minHeight: '100vh', background: 'var(--dg-bg-primary)', color: 'var(--dg-text-primary)' }}>
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
style={{ position: 'fixed', top: 20, right: 20, zIndex: 50, background: 'var(--dg-bg-tertiary)', border: 'none', borderRadius: 8, padding: 8, cursor: 'pointer', color: 'var(--dg-text-secondary)' }}
|
||||
>
|
||||
{isDark ? <Sun size={18} /> : <Moon size={18} />}
|
||||
</button>
|
||||
|
||||
{/* Left Panel — 40% */}
|
||||
<div
|
||||
className="dg-gradient-br"
|
||||
style={{ width: '40%', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', padding: 48, position: 'relative', overflow: 'hidden' }}
|
||||
>
|
||||
{/* Decorative circles */}
|
||||
<div style={{ position: 'absolute', top: '15%', left: '10%', width: 80, height: 80, borderRadius: '50%', background: 'rgba(255,255,255,0.06)' }} />
|
||||
<div style={{ position: 'absolute', bottom: '20%', right: '15%', width: 120, height: 120, borderRadius: '50%', background: 'rgba(255,255,255,0.04)' }} />
|
||||
<div style={{ position: 'absolute', top: '55%', left: '60%', width: 40, height: 40, borderRadius: '50%', background: 'rgba(255,255,255,0.07)' }} />
|
||||
<div style={{ position: 'absolute', top: '30%', right: '30%', width: 12, height: 12, borderRadius: '50%', background: 'rgba(255,255,255,0.12)' }} />
|
||||
<div style={{ position: 'absolute', bottom: '40%', left: '25%', width: 8, height: 8, borderRadius: '50%', background: 'rgba(255,255,255,0.10)' }} />
|
||||
|
||||
<div style={{ position: 'relative', zIndex: 2, textAlign: 'center', maxWidth: 320 }}>
|
||||
<Database size={40} style={{ color: '#fff', marginBottom: 20 }} />
|
||||
<h1 style={{ fontSize: 26, fontWeight: 700, color: '#fff', marginBottom: 12 }}>SQL Audit Platform</h1>
|
||||
<p style={{ color: 'rgba(255,255,255,0.7)', fontSize: 14, lineHeight: 1.6 }}>
|
||||
Empowering teams with intelligent SQL auditing, real-time compliance monitoring, and actionable insights for database security.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Panel — Login Form */}
|
||||
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 48 }}>
|
||||
<form onSubmit={handleLogin} style={{ width: '100%', maxWidth: 360, display: 'flex', flexDirection: 'column', gap: 20 }}>
|
||||
<h2 style={{ fontSize: 22, fontWeight: 600, marginBottom: 4 }}>Welcome back</h2>
|
||||
<p style={{ color: 'var(--dg-text-muted)', fontSize: 14, marginBottom: 8 }}>Sign in to access your dashboard</p>
|
||||
|
||||
{error && <div style={{ color: '#f87171', fontSize: 13 }}>{error}</div>}
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
style={{ padding: '10px 14px', borderRadius: 8, background: 'var(--dg-bg-tertiary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)', fontSize: 14, outline: 'none' }}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
style={{ padding: '10px 14px', borderRadius: 8, background: 'var(--dg-bg-tertiary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)', fontSize: 14, outline: 'none' }}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !username || !password}
|
||||
className="dg-gradient-bg"
|
||||
style={{ padding: '10px 14px', borderRadius: 8, border: 'none', color: '#fff', fontWeight: 600, fontSize: 14, cursor: loading || !username || !password ? 'not-allowed' : 'pointer', opacity: loading || !username || !password ? 0.6 : 1 }}
|
||||
>
|
||||
{loading ? 'Signing in…' : 'Sign in'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { useLogin } from '@/hooks/useLogin';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { Sun, Moon, Database, Code, GitBranch, Bell, History, UserCog } from 'lucide-react';
|
||||
|
||||
const features = [
|
||||
{ icon: Database, label: 'SQL Auditing', desc: 'Real-time query analysis' },
|
||||
{ icon: Code, label: 'Smart Rules', desc: 'Custom compliance rules' },
|
||||
{ icon: GitBranch, label: 'Version Control', desc: 'Track schema changes' },
|
||||
{ icon: Bell, label: 'Alerts', desc: 'Instant notifications' },
|
||||
{ icon: History, label: 'History', desc: 'Full audit trail' },
|
||||
{ icon: UserCog, label: 'Access Control', desc: 'Role-based permissions' },
|
||||
];
|
||||
|
||||
export default function SplitView() {
|
||||
const { username, password, loading, error, setUsername, setPassword, handleLogin } = useLogin();
|
||||
const { theme, toggleTheme, isDark } = useTheme();
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', minHeight: '100vh', background: 'var(--dg-bg-primary)', color: 'var(--dg-text-primary)' }}>
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
style={{ position: 'fixed', top: 20, right: 20, zIndex: 50, background: 'var(--dg-bg-tertiary)', border: 'none', borderRadius: 8, padding: 8, cursor: 'pointer', color: 'var(--dg-text-secondary)' }}
|
||||
>
|
||||
{isDark ? <Sun size={18} /> : <Moon size={18} />}
|
||||
</button>
|
||||
|
||||
{/* Left Panel */}
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'center', padding: '48px 40px', background: 'var(--dg-bg-secondary)' }}>
|
||||
<div style={{ maxWidth: 440 }}>
|
||||
<h1 style={{ fontSize: 28, fontWeight: 700, marginBottom: 8 }}>
|
||||
<span className="dg-gradient-text">SQL Audit Platform</span>
|
||||
</h1>
|
||||
<p style={{ color: 'var(--dg-text-muted)', fontSize: 15, marginBottom: 36, lineHeight: 1.5 }}>
|
||||
Enterprise-grade SQL auditing and compliance management.
|
||||
</p>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
|
||||
{features.map((f) => (
|
||||
<div key={f.label} style={{ padding: 16, borderRadius: 10, background: 'var(--dg-bg-tertiary)' }}>
|
||||
<f.icon size={20} className="dg-gradient-text" />
|
||||
<div style={{ fontWeight: 600, fontSize: 13, marginTop: 8 }}>{f.label}</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--dg-text-muted)', marginTop: 2 }}>{f.desc}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Panel — Login Form */}
|
||||
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 48 }}>
|
||||
<form
|
||||
onSubmit={handleLogin}
|
||||
style={{ width: '100%', maxWidth: 360, display: 'flex', flexDirection: 'column', gap: 20 }}
|
||||
>
|
||||
<h2 style={{ fontSize: 22, fontWeight: 600, marginBottom: 4 }}>Sign in</h2>
|
||||
<p style={{ color: 'var(--dg-text-muted)', fontSize: 14, marginBottom: 8 }}>Enter your credentials to continue</p>
|
||||
|
||||
{error && <div style={{ color: '#f87171', fontSize: 13 }}>{error}</div>}
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
style={{ padding: '10px 14px', borderRadius: 8, background: 'var(--dg-bg-tertiary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)', fontSize: 14, outline: 'none' }}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
style={{ padding: '10px 14px', borderRadius: 8, background: 'var(--dg-bg-tertiary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)', fontSize: 14, outline: 'none' }}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !username || !password}
|
||||
className="dg-gradient-bg"
|
||||
style={{ padding: '10px 14px', borderRadius: 8, border: 'none', color: '#fff', fontWeight: 600, fontSize: 14, cursor: loading || !username || !password ? 'not-allowed' : 'pointer', opacity: loading || !username || !password ? 0.6 : 1 }}
|
||||
>
|
||||
{loading ? 'Signing in…' : 'Sign in'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
|
||||
export interface LoginState {
|
||||
username: string;
|
||||
password: string;
|
||||
loading: boolean;
|
||||
error: string;
|
||||
setUsername: (v: string) => void;
|
||||
setPassword: (v: string) => void;
|
||||
handleLogin: (e: React.FormEvent) => Promise<void>;
|
||||
}
|
||||
|
||||
export function useLogin(): LoginState {
|
||||
const navigate = useNavigate();
|
||||
const { login } = useAuth();
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleLogin = useCallback(async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!username || !password) return;
|
||||
setLoading(true);
|
||||
setError('');
|
||||
const ok = await login(username, password);
|
||||
setLoading(false);
|
||||
if (ok) {
|
||||
navigate('/dashboard');
|
||||
} else {
|
||||
setError('用户名或密码错误');
|
||||
}
|
||||
}, [username, password, login, navigate]);
|
||||
|
||||
return { username, password, loading, error, setUsername, setPassword, handleLogin };
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { LOGIN_STYLE_OPTIONS, type LoginStyle } from '@/pages/LoginPage';
|
||||
import { Sun, Moon, Monitor, Check } from 'lucide-react';
|
||||
|
||||
export default function AppearanceSettings() {
|
||||
const { theme, setTheme, isDark } = useTheme();
|
||||
const [loginStyle, setLoginStyle] = useState<LoginStyle>('split');
|
||||
|
||||
useEffect(() => {
|
||||
const stored = localStorage.getItem('login-style') as LoginStyle | null;
|
||||
if (stored) setLoginStyle(stored);
|
||||
}, []);
|
||||
|
||||
const handleThemeChange = (t: 'dark' | 'light') => {
|
||||
setTheme(t);
|
||||
};
|
||||
|
||||
const handleLoginStyleChange = (id: LoginStyle) => {
|
||||
setLoginStyle(id);
|
||||
localStorage.setItem('login-style', id);
|
||||
};
|
||||
|
||||
const themeOptions = [
|
||||
{ id: 'dark' as const, icon: Moon, label: '深色模式' },
|
||||
{ id: 'light' as const, icon: Sun, label: '浅色模式' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* 主题模式 */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold mb-3">主题模式</h3>
|
||||
<div className="flex gap-3">
|
||||
{themeOptions.map(opt => (
|
||||
<button
|
||||
key={opt.id}
|
||||
onClick={() => handleThemeChange(opt.id)}
|
||||
className={`flex items-center gap-2 px-5 py-3 rounded-xl text-sm font-medium transition-all ${
|
||||
theme === opt.id
|
||||
? 'bg-blue-500/15 text-blue-400 ring-1 ring-blue-500/30'
|
||||
: 'bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)]'
|
||||
}`}
|
||||
>
|
||||
<opt.icon className="w-4 h-4" />
|
||||
{opt.label}
|
||||
{theme === opt.id && <Check className="w-3.5 h-3.5 ml-1" />}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 登录页风格 */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold mb-1">登录页风格</h3>
|
||||
<p className="text-xs text-[var(--dg-text-muted)] mb-4">选择登录页面的视觉风格,退出登录后生效</p>
|
||||
<div className="grid grid-cols-5 gap-3">
|
||||
{LOGIN_STYLE_OPTIONS.map(opt => (
|
||||
<button
|
||||
key={opt.id}
|
||||
onClick={() => handleLoginStyleChange(opt.id)}
|
||||
className={`group relative rounded-xl p-4 text-left transition-all ${
|
||||
loginStyle === opt.id
|
||||
? 'bg-blue-500/10 ring-1 ring-blue-500/30'
|
||||
: 'bg-[var(--dg-bg-tertiary)] hover:bg-[var(--dg-bg-hover)]'
|
||||
}`}
|
||||
>
|
||||
{/* 预览缩略图 */}
|
||||
<div className={`w-full aspect-[4/3] rounded-lg mb-3 overflow-hidden ${
|
||||
opt.id === 'split' ? 'flex' :
|
||||
opt.id === 'centered' ? 'flex items-center justify-center dg-gradient-br' :
|
||||
opt.id === 'sidepanel' ? 'flex' :
|
||||
opt.id === 'fullimage' ? 'flex items-center justify-center' :
|
||||
'flex items-center justify-center bg-[#0a0a0a]'
|
||||
}`}>
|
||||
{opt.id === 'split' && (
|
||||
<>
|
||||
<div className="flex-1 bg-[var(--dg-bg-primary)] p-1.5">
|
||||
<div className="w-3 h-1 rounded-sm bg-blue-500/30 mb-1" />
|
||||
<div className="grid grid-cols-2 gap-0.5">
|
||||
{[0,1,2,3].map(i => <div key={i} className="h-1.5 rounded-sm bg-[var(--dg-bg-tertiary)]" />)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-2/5 bg-[var(--dg-bg-secondary)] p-1.5 flex flex-col gap-0.5 justify-center">
|
||||
<div className="h-1 rounded-sm bg-[var(--dg-bg-tertiary)]" />
|
||||
<div className="h-1 rounded-sm bg-[var(--dg-bg-tertiary)]" />
|
||||
<div className="h-1.5 rounded-sm dg-gradient-bg opacity-60" />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{opt.id === 'centered' && (
|
||||
<div className="w-3/4 bg-white/10 backdrop-blur rounded-md p-1.5 flex flex-col gap-0.5 items-center">
|
||||
<div className="w-4 h-1 rounded-sm bg-white/40" />
|
||||
<div className="w-6 h-1 rounded-sm bg-white/20" />
|
||||
<div className="w-full h-1 rounded-sm bg-white/15" />
|
||||
<div className="w-full h-1 rounded-sm bg-white/15" />
|
||||
<div className="w-full h-1.5 rounded-sm bg-white/30" />
|
||||
</div>
|
||||
)}
|
||||
{opt.id === 'sidepanel' && (
|
||||
<>
|
||||
<div className="w-2/5 dg-gradient-br p-1.5 flex flex-col justify-center items-center gap-0.5">
|
||||
<div className="w-3 h-3 rounded-full bg-white/20" />
|
||||
<div className="w-4 h-0.5 rounded-sm bg-white/15" />
|
||||
</div>
|
||||
<div className="flex-1 bg-[var(--dg-bg-primary)] p-1.5 flex flex-col gap-0.5 justify-center">
|
||||
<div className="h-1 rounded-sm bg-[var(--dg-bg-tertiary)]" />
|
||||
<div className="h-1 rounded-sm bg-[var(--dg-bg-tertiary)]" />
|
||||
<div className="h-1.5 rounded-sm dg-gradient-bg opacity-60" />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{opt.id === 'fullimage' && (
|
||||
<div className="w-full h-full" style={{
|
||||
background: 'repeating-linear-gradient(0deg, transparent, transparent 8px, rgba(59,130,246,0.15) 8px, rgba(59,130,246,0.15) 9px), repeating-linear-gradient(90deg, transparent, transparent 8px, rgba(59,130,246,0.15) 8px, rgba(59,130,246,0.15) 9px)',
|
||||
}}>
|
||||
<div className="w-3/4 mx-auto mt-3 bg-black/40 backdrop-blur rounded-md p-1.5 flex flex-col gap-0.5 items-center">
|
||||
<div className="w-3 h-0.5 rounded-sm bg-white/30" />
|
||||
<div className="w-full h-0.5 rounded-sm bg-white/10" />
|
||||
<div className="w-full h-0.5 rounded-sm bg-white/10" />
|
||||
<div className="w-full h-1 rounded-sm bg-blue-500/40" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{opt.id === 'minimal' && (
|
||||
<div className="w-full h-full flex flex-col items-center justify-center gap-1 p-2">
|
||||
<div className="w-4 h-1 rounded-sm bg-white/20" />
|
||||
<div className="w-6 h-0.5 rounded-sm bg-white/10" />
|
||||
<div className="w-full h-1 rounded-sm bg-white/8" />
|
||||
<div className="w-full h-1 rounded-sm bg-white/8" />
|
||||
<div className="w-full h-1.5 rounded-sm bg-white/15" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 选中标记 */}
|
||||
{loginStyle === opt.id && (
|
||||
<div className="absolute top-2 right-2 w-5 h-5 rounded-full bg-blue-500 flex items-center justify-center">
|
||||
<Check className="w-3 h-3 text-white" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h4 className="text-sm font-medium mb-0.5">{opt.name}</h4>
|
||||
<p className="text-xs text-[var(--dg-text-muted)]">{opt.desc}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,134 +1,36 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Shield, Database, Code, GitBranch, Bell, History, UserCog, Sparkles, Sun, Moon } from 'lucide-react';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import SplitView from '@/components/login/SplitView';
|
||||
import CenteredCard from '@/components/login/CenteredCard';
|
||||
import SidePanel from '@/components/login/SidePanel';
|
||||
import FullImage from '@/components/login/FullImage';
|
||||
import MinimalDark from '@/components/login/MinimalDark';
|
||||
|
||||
export type LoginStyle = 'split' | 'centered' | 'sidepanel' | 'fullimage' | 'minimal';
|
||||
|
||||
const LOGIN_STYLES: Record<LoginStyle, React.ComponentType> = {
|
||||
split: SplitView,
|
||||
centered: CenteredCard,
|
||||
sidepanel: SidePanel,
|
||||
fullimage: FullImage,
|
||||
minimal: MinimalDark,
|
||||
};
|
||||
|
||||
export const LOGIN_STYLE_OPTIONS: { id: LoginStyle; name: string; desc: string }[] = [
|
||||
{ id: 'split', name: '经典分屏', desc: '左右分屏布局,功能特性展示' },
|
||||
{ id: 'centered', name: '居中卡片', desc: '渐变背景,居中登录卡片' },
|
||||
{ id: 'sidepanel', name: '侧边面板', desc: '左侧品牌面板 + 右侧表单' },
|
||||
{ id: 'fullimage', name: '全屏背景', desc: '科技背景,毛玻璃卡片' },
|
||||
{ id: 'minimal', name: '极简暗色', desc: '极致简约,纯暗色风格' },
|
||||
];
|
||||
|
||||
function getLoginStyle(): LoginStyle {
|
||||
if (typeof window === 'undefined') return 'split';
|
||||
const stored = localStorage.getItem('login-style') as LoginStyle | null;
|
||||
if (stored && stored in LOGIN_STYLES) return stored;
|
||||
return 'split';
|
||||
}
|
||||
|
||||
export default function LoginPage() {
|
||||
const navigate = useNavigate();
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
const { login } = useAuth();
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!username || !password) return;
|
||||
setLoading(true);
|
||||
setError('');
|
||||
const ok = await login(username, password);
|
||||
setLoading(false);
|
||||
if (ok) {
|
||||
navigate('/dashboard');
|
||||
} else {
|
||||
setError('用户名或密码错误');
|
||||
}
|
||||
};
|
||||
|
||||
const features = [
|
||||
{ icon: Database, title: '多数据库支持', desc: 'MySQL、PostgreSQL、Redis、MongoDB' },
|
||||
{ icon: Code, title: '智能 SQL 审核', desc: '语法检查、规则校验、影响分析、自动评分' },
|
||||
{ icon: GitBranch, title: '多级审批流程', desc: '会签、或签、条件审批灵活配置' },
|
||||
{ icon: Bell, title: '多渠道通知', desc: '飞书、钉钉、企微、TG 即时推送' },
|
||||
{ icon: History, title: '完整审计追踪', desc: '所有操作可追溯,支持 SQL 回滚' },
|
||||
{ icon: UserCog, title: '细粒度权限', desc: 'RBAC 角色权限,字段级/行级访问控制' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex bg-[var(--dg-bg-primary)] relative overflow-hidden">
|
||||
{/* 背景装饰 */}
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
<div className="absolute top-0 left-0 w-full h-full bg-[radial-gradient(ellipse_at_20%_20%,rgba(88,166,255,0.08)_0%,transparent_50%),radial-gradient(ellipse_at_80%_80%,rgba(137,87,229,0.08)_0%,transparent_50%)]" />
|
||||
</div>
|
||||
{/* 主题切换 */}
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="absolute top-5 right-5 z-20 w-10 h-10 rounded-xl bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] flex items-center justify-center text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)] transition-colors"
|
||||
title={theme === 'dark' ? '切换到浅色模式' : '切换到深色模式'}
|
||||
>
|
||||
{theme === 'dark' ? <Sun className="w-5 h-5" /> : <Moon className="w-5 h-5" />}
|
||||
</button>
|
||||
|
||||
{/* 左侧 */}
|
||||
<div className="flex-1 flex flex-col justify-center px-16 relative z-10">
|
||||
<div className="mb-10">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-12 h-12 rounded-xl dg-gradient-br flex items-center justify-center">
|
||||
<Shield className="w-7 h-7 text-white" />
|
||||
</div>
|
||||
<h1 className="text-5xl font-bold dg-gradient-text">
|
||||
SQL-Platform
|
||||
</h1>
|
||||
</div>
|
||||
<p className="text-lg text-[var(--dg-text-secondary)] leading-relaxed">
|
||||
企业级数据审核与变更管理平台<br />
|
||||
支持多数据库类型,提供完善的 SQL 审核、审批流程和审计追踪
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{features.map((f, i) => (
|
||||
<div key={i} className="flex items-start gap-3 p-4 rounded-xl bg-[var(--dg-bg-secondary)]/60 border border-[var(--dg-border)] hover:border-blue-500/50 hover:bg-[var(--dg-bg-secondary)] transition-all group">
|
||||
<f.icon className="w-6 h-6 text-blue-400 mt-0.5 flex-shrink-0 group-hover:scale-110 transition-transform" />
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold mb-1">{f.title}</h4>
|
||||
<p className="text-xs text-[var(--dg-text-secondary)]">{f.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧 */}
|
||||
<div className="w-[480px] flex flex-col justify-center px-16 bg-[var(--dg-bg-secondary)]/80 backdrop-blur-xl border-l border-[var(--dg-border)] relative z-10">
|
||||
<div className="w-full max-w-sm mx-auto">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Sparkles className="w-5 h-5 text-blue-400" />
|
||||
<h2 className="text-2xl font-bold">欢迎回来</h2>
|
||||
</div>
|
||||
<p className="text-sm text-[var(--dg-text-secondary)] mb-8">请登录您的账户以继续</p>
|
||||
|
||||
<form onSubmit={handleLogin} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">用户名</label>
|
||||
<div className="relative">
|
||||
<UserCog className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--dg-text-muted)]" />
|
||||
<input
|
||||
className="w-full pl-10 pr-4 py-3 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none focus:ring-1 focus:ring-blue-400/20 transition-all"
|
||||
placeholder="请输入用户名"
|
||||
value={username}
|
||||
onChange={e => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">密码</label>
|
||||
<div className="relative">
|
||||
<Shield className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--dg-text-muted)]" />
|
||||
<input
|
||||
type="password"
|
||||
className="w-full pl-10 pr-4 py-3 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none focus:ring-1 focus:ring-blue-400/20 transition-all"
|
||||
placeholder="请输入密码"
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !username || !password}
|
||||
className="w-full py-3 dg-gradient-bg rounded-lg text-sm font-semibold text-white hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed transition-all flex items-center justify-center gap-2"
|
||||
>
|
||||
{loading ? (
|
||||
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||
) : '登录'}
|
||||
</button>
|
||||
{error && <p className="text-xs text-red-400 text-center mt-2">{error}</p>}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const style = getLoginStyle();
|
||||
const Component = LOGIN_STYLES[style] || SplitView;
|
||||
return <Component />;
|
||||
}
|
||||
@@ -4,11 +4,12 @@ import AuditRulesPage from './AuditRulesPage';
|
||||
import AnnouncementPage from './AnnouncementPage';
|
||||
import NotificationPage from './NotificationPage';
|
||||
import LdapConfigPage from './LdapConfigPage';
|
||||
import AppearanceSettings from './AppearanceSettings';
|
||||
|
||||
const tabs = [
|
||||
{ id: 'appearance', label: '外观设置', icon: '🎨' },
|
||||
{ id: 'platform', label: '平台配置', icon: '⚙️' },
|
||||
{ id: 'rules', label: '审核规则', icon: '🛡️' },
|
||||
{ id: 'announcement', label: '公告管理', icon: '📢' },
|
||||
{ id: 'notification', label: '消息推送', icon: '🔔' },
|
||||
{ id: 'ldap', label: 'LDAP', icon: '🔐' },
|
||||
] as const;
|
||||
@@ -16,13 +17,13 @@ const tabs = [
|
||||
type TabId = typeof tabs[number]['id'];
|
||||
|
||||
export default function SettingsPage() {
|
||||
const [activeTab, setActiveTab] = useState<TabId>('platform');
|
||||
const [activeTab, setActiveTab] = useState<TabId>('appearance');
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold">系统设置</h1>
|
||||
<p className="text-sm text-[var(--dg-text-muted)] mt-1">管理平台配置、审核规则、消息推送和 LDAP 认证</p>
|
||||
<p className="text-sm text-[var(--dg-text-muted)] mt-1">管理平台外观、配置、审核规则、消息推送和 LDAP 认证</p>
|
||||
</div>
|
||||
|
||||
{/* Tab 导航 */}
|
||||
@@ -45,9 +46,9 @@ export default function SettingsPage() {
|
||||
|
||||
{/* 内容区 */}
|
||||
<div>
|
||||
{activeTab === 'appearance' && <AppearanceSettings />}
|
||||
{activeTab === 'platform' && <PlatformConfigPage />}
|
||||
{activeTab === 'rules' && <AuditRulesPage />}
|
||||
{activeTab === 'announcement' && <AnnouncementPage />}
|
||||
{activeTab === 'notification' && <NotificationPage />}
|
||||
{activeTab === 'ldap' && <LdapConfigPage />}
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user