后端: - User 模型新增 Preferences JSON 字段 (theme + login_style) - 新增 GET/PUT /api/v1/user/preferences (auth) 个人偏好读写 - 新增 GET /api/v1/public/login-style (public) 登录页风格公开端点 - SQLite users 表手动添加 preferences 列 前端: - AppearanceSettings 从后端加载偏好 + 保存按钮 + dirty 状态提示 - LoginPage 先从后端公开接口获取 login_style (跨浏览器一致) - useLogin 登录成功后从后端加载偏好并应用主题 - 偏好同时存 localStorage(即时) + 后端DB(跨设备)
55 lines
2.0 KiB
TypeScript
55 lines
2.0 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
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: '极致简约,纯暗色风格' },
|
|
];
|
|
|
|
export default function LoginPage() {
|
|
const [style, setStyle] = useState<LoginStyle>('split');
|
|
const [ready, setReady] = useState(false);
|
|
|
|
useEffect(() => {
|
|
// Try backend first (cross-browser), fallback to localStorage
|
|
fetch('/api/v1/public/login-style')
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
const s = data?.login_style as LoginStyle;
|
|
if (s && s in LOGIN_STYLES) {
|
|
setStyle(s);
|
|
} else {
|
|
const stored = localStorage.getItem('login-style') as LoginStyle | null;
|
|
if (stored && stored in LOGIN_STYLES) setStyle(stored);
|
|
}
|
|
})
|
|
.catch(() => {
|
|
const stored = localStorage.getItem('login-style') as LoginStyle | null;
|
|
if (stored && stored in LOGIN_STYLES) setStyle(stored);
|
|
})
|
|
.finally(() => setReady(true));
|
|
}, []);
|
|
|
|
if (!ready) return null; // avoid flash
|
|
|
|
const Component = LOGIN_STYLES[style] || SplitView;
|
|
return <Component />;
|
|
}
|