feat: 登录页5种主流高级风格重写 + 主题/登录页设置独立

5个登录组件全面重新设计:
- SplitView: 分屏聚焦风格(Linear/Notion) — 左55%深色渐变+几何动画装饰,右45%纯净表单+底线输入框
- CenteredCard: 沉浸渐变风格(Stripe/Vercel) — 紫蓝辐射背景+毛玻璃卡片+发光效果
- SidePanel: 品牌侧栏风格(Slack/Figma) — 左38%大号标语+feature pills,右62%宽留白表单
- FullImage: 粒子网格风格(GitHub/AWS) — CSS粒子光点+网格线+发光球体+深色内嵌卡片
- MinimalDark: 极简线条风格(Apple/Linear) — 纯黑背景+16px渐变圆点logo+宽字距+底线输入框

AppearanceSettings 主题与登录页完全独立:
- 界面配色区: 5种主题配色即时预览
- 登录页风格区: 5种风格缩略图预览+参考品牌标签(Linear/Notion, Stripe/Vercel等)
- 移除登录页主题切换按钮,风格与主题彻底解耦
This commit is contained in:
logan committed 2026-05-27 03:25:58 +08:00
1 parent ed582e30e6
commit 46054c8b2f
6 files changed
+901 -292

No files matched your search

+111 -91
View File
@@ -5,6 +5,15 @@ import { Check, Save, Loader2 } from 'lucide-react';
import api from '@/api/request';
import { toast } from '@/hooks/useToast';
// Login style preview color themes (independent from app theme)
const LOGIN_PREVIEW_COLORS: Record<LoginStyle, { bg: string; accent: string; label: string }> = {
split: { bg: 'linear-gradient(135deg, #0f172a 0%, #1e293b 100%)', accent: '#6366f1', label: 'Linear/Notion' },
centered: { bg: 'radial-gradient(ellipse at 50% 40%, #0a0a1a 0%, #1a0a2e 100%)', accent: '#8b5cf6', label: 'Stripe/Vercel' },
sidepanel: { bg: 'linear-gradient(180deg, #111827 0%, #1f2937 100%)', accent: '#6366f1', label: 'Slack/Figma' },
fullimage: { bg: '#0d1117', accent: '#6366f1', label: 'GitHub/AWS' },
minimal: { bg: '#0a0a0a', accent: '#8b5cf6', label: 'Apple/Linear' },
};
export default function AppearanceSettings() {
const { theme, setTheme } = useTheme();
const [loginStyle, setLoginStyle] = useState<LoginStyle>('split');
@@ -12,12 +21,10 @@ export default function AppearanceSettings() {
const [loading, setLoading] = useState(true);
const [dirty, setDirty] = useState(false);
// Load preferences from backend on mount
useEffect(() => {
api.get<{ theme?: string; login_style?: string }>('/user/preferences')
.then(prefs => {
if (prefs?.theme) {
// Backward compat: map old values
const t = prefs.theme;
if (t === 'dark' || t === 'light') {
setTheme(t === 'dark' ? 'ocean-deep' : 'light-professional');
@@ -28,7 +35,6 @@ export default function AppearanceSettings() {
if (prefs?.login_style) setLoginStyle(prefs.login_style as LoginStyle);
})
.catch(() => {
// Fallback to localStorage
const stored = localStorage.getItem('login-style') as LoginStyle | null;
if (stored) setLoginStyle(stored);
})
@@ -71,10 +77,10 @@ export default function AppearanceSettings() {
}
return (
<div className="space-y-8">
{/* Theme Mode */}
<div className="space-y-10">
{/* ===== Theme Mode Section ===== */}
<div>
<h3 className="text-sm font-semibold mb-1">主题模式</h3>
<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">
{THEME_OPTIONS.map(opt => (
@@ -87,7 +93,7 @@ export default function AppearanceSettings() {
>
{/* Color preview swatch */}
<div
className="w-full h-6 rounded-md mb-3"
className="w-full h-8 rounded-lg mb-3"
style={{
background: opt.isDark
? `linear-gradient(135deg, ${opt.primaryColor} 0%, color-mix(in srgb, ${opt.primaryColor} 40%, #1a1a2e) 50%, #0d1117 100%)`
@@ -109,100 +115,114 @@ export default function AppearanceSettings() {
</div>
</div>
{/* Login Page Style */}
{/* ===== Login Page Style Section ===== */}
<div>
<h3 className="text-sm font-semibold mb-1">登录页风格</h3>
<p className="text-xs text-[var(--dg-text-muted)] mb-4">选择登录页面的视觉风格,保存后退出登录即可看到效果</p>
<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)]'
}`}
>
{/* Thumbnail preview */}
<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)]" />)}
{LOGIN_STYLE_OPTIONS.map(opt => {
const preview = LOGIN_PREVIEW_COLORS[opt.id];
return (
<button
key={opt.id}
onClick={() => handleLoginStyleChange(opt.id)}
className={`group relative rounded-xl p-4 text-left transition-all ${
loginStyle === opt.id
? 'ring-2 ring-indigo-500/50 bg-[var(--dg-bg-tertiary)]'
: 'bg-[var(--dg-bg-tertiary)] hover:bg-[var(--dg-bg-hover)]'
}`}
>
{/* Login style miniature preview */}
<div
className="w-full aspect-[4/3] rounded-lg mb-3 overflow-hidden relative"
style={{ background: preview.bg }}
>
{/* Abstract mini layout */}
{opt.id === 'split' && (
<>
<div className="absolute inset-0 flex">
<div className="flex-1 p-2 flex flex-col justify-center gap-1">
<div className="w-6 h-1.5 rounded-sm" style={{ background: 'rgba(255,255,255,0.3)' }} />
<div className="w-8 h-1 rounded-sm" style={{ background: 'rgba(255,255,255,0.15)' }} />
</div>
<div className="w-2/5 bg-[var(--dg-bg-secondary)] p-2 flex flex-col justify-center gap-1">
<div className="h-0.5 rounded-sm bg-[var(--dg-bg-hover)]" />
<div className="h-0.5 rounded-sm bg-[var(--dg-bg-hover)]" />
<div className="h-1 rounded-sm mt-0.5" style={{ background: preview.accent, opacity: 0.6 }} />
</div>
</div>
</>
)}
{opt.id === 'centered' && (
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-3/4 rounded-lg p-2 flex flex-col items-center gap-1" style={{ background: 'rgba(255,255,255,0.04)', backdropFilter: 'blur(4px)', border: '1px solid rgba(255,255,255,0.06)' }}>
<div className="w-4 h-1 rounded-sm" style={{ background: 'rgba(255,255,255,0.4)' }} />
<div className="w-6 h-0.5 rounded-sm" style={{ background: 'rgba(255,255,255,0.15)' }} />
<div className="w-full h-0.5 rounded-sm" style={{ background: 'rgba(255,255,255,0.08)' }} />
<div className="w-full h-0.5 rounded-sm" style={{ background: 'rgba(255,255,255,0.08)' }} />
<div className="w-full h-1 rounded-sm mt-0.5" style={{ background: preview.accent, opacity: 0.5 }} />
</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" />
)}
{opt.id === 'sidepanel' && (
<div className="absolute inset-0 flex">
<div className="w-[38%] p-2 flex flex-col justify-end gap-1" style={{ background: preview.bg }}>
<div className="flex flex-wrap gap-0.5">
{['SQL', 'DDL', 'ACL'].map(t => (
<span key={t} className="px-1 rounded text-[5px]" style={{ background: 'rgba(255,255,255,0.08)', color: 'rgba(255,255,255,0.5)' }}>{t}</span>
))}
</div>
</div>
<div className="flex-1 bg-[var(--dg-bg-primary)] p-2 flex flex-col justify-center gap-1">
<div className="h-0.5 rounded-sm bg-[var(--dg-bg-hover)]" />
<div className="h-0.5 rounded-sm bg-[var(--dg-bg-hover)]" />
<div className="h-1 rounded-sm mt-0.5" style={{ background: preview.accent, opacity: 0.5 }} />
</div>
</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" />
)}
{opt.id === 'fullimage' && (
<div className="absolute inset-0">
{/* Grid dots */}
<div className="absolute inset-0" style={{
backgroundImage: 'radial-gradient(circle, rgba(99,102,241,0.15) 0.5px, transparent 0.5px)',
backgroundSize: '12px 12px',
}} />
{/* Center card */}
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-3/4 rounded p-1.5 flex flex-col items-center gap-0.5" style={{ background: 'rgba(0,0,0,0.4)', border: '1px solid rgba(255,255,255,0.06)' }}>
<div className="w-3 h-0.5 rounded-sm" style={{ background: 'rgba(255,255,255,0.25)' }} />
<div className="w-full h-0.5 rounded-sm" style={{ background: 'rgba(255,255,255,0.06)' }} />
<div className="w-full h-0.5 rounded-sm" style={{ background: 'rgba(255,255,255,0.06)' }} />
<div className="w-full h-0.5 rounded-sm mt-0.5" style={{ background: preview.accent, opacity: 0.4 }} />
</div>
</div>
</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" />
)}
{opt.id === 'minimal' && (
<div className="absolute inset-0 flex flex-col items-center justify-center gap-1.5 p-2">
<div className="w-2 h-2 rounded-full" style={{ background: `linear-gradient(135deg, ${preview.accent}, #a855f7)` }} />
<div className="w-8 h-0.5 rounded-sm" style={{ background: 'rgba(255,255,255,0.15)' }} />
<div className="w-6 h-px my-1" style={{ background: 'rgba(255,255,255,0.1)' }} />
<div className="w-full h-0.5 rounded-sm" style={{ background: 'rgba(255,255,255,0.06)' }} />
<div className="w-full h-0.5 rounded-sm" style={{ background: 'rgba(255,255,255,0.06)' }} />
<div className="w-full h-0.5 rounded-sm mt-0.5" style={{ background: preview.accent, opacity: 0.4 }} />
</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>
{/* Active check mark */}
{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>
))}
{/* Active check mark */}
{loginStyle === opt.id && (
<div className="absolute top-2 right-2 w-5 h-5 rounded-full bg-indigo-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)] leading-snug">{opt.desc}</p>
<p className="text-[10px] text-[var(--dg-text-muted)] mt-1 opacity-60">{preview.label}</p>
</button>
);
})}
</div>
</div>