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:
logan committed 2026-05-26 21:16:36 +08:00
1 parent f2e93a09c0
commit 14e394fb09
12 files changed
+1147 -639

No files matched your search

+33 -131
View File
@@ -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 />;
}