feat: 外观设置后端持久化 + 保存按钮 + 跨浏览器同步

后端:
- 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(跨设备)
This commit is contained in:
logan committed 2026-05-26 22:09:47 +08:00
1 parent 8469df359f
commit 9055cc77ec
7 files changed
+369 -119

No files matched your search

+26 -8
View File
@@ -1,3 +1,4 @@
import { useState, useEffect } from 'react';
import SplitView from '@/components/login/SplitView';
import CenteredCard from '@/components/login/CenteredCard';
import SidePanel from '@/components/login/SidePanel';
@@ -22,15 +23,32 @@ export const LOGIN_STYLE_OPTIONS: { id: LoginStyle; name: string; desc: string }
{ 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 style = getLoginStyle();
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 />;
}