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:
1 parent
ed582e30e6
commit
46054c8b2f
6 files changed
+901
-292
No files matched your search
@@ -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>
|
||||
|
||||
|
||||
Reference in new issue
Block a user