feat(admin): 登录改页内弹窗 + P1 站点设置(页头/侧栏)

- 登录:LoginDialog(Modal)替代 /login 页面;/login 重定向回首页
- 设置接口:GET /api/settings 公开读、PATCH 需登录 + version 乐观锁(409/400)
- setting 表存 site JSON:页头(启用/速度/条目+标签+色调)、侧栏(hidden/order)
- /admin:Tabs 页头/侧栏,条目增删与上下移,保存后首页生效
- 首页应用设置:ticker 文本占位符与速度、侧栏顺序与显隐
- 测试 42 passed(settings 5 例)
This commit is contained in:
stash
2026-10-29 12:52:29 +08:00
parent dea7781b86
commit 9ae5a455c5
14 changed files with 1205 additions and 195 deletions
+42 -6
View File
@@ -29,6 +29,13 @@ import { useProjects } from './useProjects'
import { useCatalog } from './useCatalog'
import { useViewRoute } from './useViewRoute'
import { fetchSession, logout as logoutRequest, type SessionState } from './auth'
import {
applySidebarSettings,
defaultSettings,
fetchSettings,
type SettingsDocument,
} from './settings'
import { LoginDialog } from './components/auth/LoginDialog'
import { parseViewPath, viewPath, type View } from './data/view'
import './App.css'
@@ -40,6 +47,11 @@ export default function App() {
// 当前视图由地址栏决定:刷新 / 收藏 / 前进后退都回到同一页(见 useViewRoute.ts)
const { view, go } = useViewRoute()
const [session, setSession] = useState<SessionState | null>(null)
const [settings, setSettings] = useState<SettingsDocument>({
version: 0,
settings: defaultSettings(),
})
const [loginOpen, setLoginOpen] = useState(false)
const [scrolled, setScrolled] = useState(false)
const searchRef = useRef<HTMLInputElement>(null)
@@ -49,6 +61,9 @@ export default function App() {
fetchSession().then((next) => {
if (alive) setSession(next)
})
fetchSettings().then((next) => {
if (alive) setSettings(next)
})
return () => {
alive = false
}
@@ -63,13 +78,17 @@ export default function App() {
[categories, keyword],
)
// 侧栏不显示「本地服务」,也不显示被标记 show_in_sidebar = 0 的分区
// 侧栏不显示「本地服务」,也不显示被标记 show_in_sidebar = 0 的分区;
// 再按 /admin 里的顺序与显隐设置重排一遍
const sidebarCategories = useMemo(
() =>
categories.filter(
(category) => category.id !== 'local' && category.showInSidebar !== false,
applySidebarSettings(
categories.filter(
(category) => category.id !== 'local' && category.showInSidebar !== false,
),
settings.settings.sidebar,
),
[categories],
[categories, settings.settings.sidebar],
)
const detailView = view.type === 'detail' ? view : undefined
@@ -258,11 +277,24 @@ export default function App() {
}
const handleLogout = () => {
void logoutRequest().then(() => window.location.assign('/'))
void logoutRequest().then(() => {
setSession({ authenticated: false, has_admin: true })
})
}
return (
<>
{/* 登录弹窗:成功后直接把会话状态写回,不刷新页面 */}
<LoginDialog
open={loginOpen}
hasAdmin={session?.has_admin ?? false}
onClose={() => setLoginOpen(false)}
onSuccess={(next) => {
setSession(next)
setLoginOpen(false)
}}
/>
{/* 主题的顶部背景图:固定 65vh,底部渐隐到页面底色 */}
<div className="site-background" aria-hidden="true" />
@@ -287,11 +319,15 @@ export default function App() {
projectCount={projects.length}
sampleProject={projects[0]?.name}
live={live}
headerItems={settings.settings.header.items}
headerEnabled={settings.settings.header.enabled}
headerSpeed={settings.settings.header.speed}
authenticated={session?.authenticated === true}
username={session?.username ?? undefined}
onFocusSearch={() => searchRef.current?.focus()}
onLogin={() => window.location.assign('/login')}
onLogin={() => setLoginOpen(true)}
onLogout={handleLogout}
onSettings={() => window.location.assign('/admin')}
/>
}
sider={