feat(admin): 后台改为左侧导航 + 右侧内容

- 原来的横向 Tabs 换成库里的 Menu,左侧固定导航、右侧面板
- 首页管理收成一组:侧边栏 / 内容卡片 / 页头 三个子项,页头从顶层移入
- 页头、资料仍需手动保存,保存/撤销按钮留在各自面板底部
- 窄屏(<=860px)导航折到内容上方
- 后台内容区宽度不变(1340),导航列 190px + 24px 间距
This commit is contained in:
2026-11-01 20:27:36 +08:00
parent 8271adc307
commit 10b653e30f
2 changed files with 116 additions and 62 deletions
+34
View File
@@ -53,6 +53,40 @@
padding: 0 var(--su-space-5); padding: 0 var(--su-space-5);
} }
/* 后台:左侧导航 + 右侧内容 */
.admin-shell {
display: grid;
grid-template-columns: 190px minmax(0, 1fr);
gap: var(--su-space-6);
align-items: start;
margin-top: var(--su-space-5);
}
.admin-nav {
position: sticky;
top: calc(var(--su-header-height) + var(--su-space-5));
padding-right: var(--su-space-4);
border-right: 1px solid var(--su-border);
}
.admin-panel {
min-width: 0;
}
@media (max-width: 860px) {
.admin-shell {
grid-template-columns: 1fr;
}
.admin-nav {
position: static;
padding-right: 0;
border-right: none;
border-bottom: 1px solid var(--su-border);
padding-bottom: var(--su-space-3);
}
}
/* 后台「首页 Logo」的实时预览:外观交给库里的 Brand,这里只管外框 */ /* 后台「首页 Logo」的实时预览:外观交给库里的 Brand,这里只管外框 */
.brand-preview { .brand-preview {
display: flex; display: flex;
+42 -22
View File
@@ -7,13 +7,13 @@ import {
ICON_NAMES, ICON_NAMES,
IconButton, IconButton,
Input, Input,
Menu,
Modal, Modal,
Notice, Notice,
PasswordInput, PasswordInput,
Select, Select,
Space, Space,
Switch, Switch,
Tabs,
Tag, Tag,
Text, Text,
Textarea, Textarea,
@@ -21,6 +21,7 @@ import {
Toaster, Toaster,
toast, toast,
type IconName, type IconName,
type MenuItem,
} from '@stash/ui' } from '@stash/ui'
import { useEffect, useMemo, useRef, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
import { changePassword, fetchSession, logout, type SessionState } from '../../auth' import { changePassword, fetchSession, logout, type SessionState } from '../../auth'
@@ -63,7 +64,31 @@ import type { Project } from '../../api'
* - 内容是条目,挂在分组上; * - 内容是条目,挂在分组上;
* - 侧边栏、首页卡片都只是「勾选分组」的展示组合。 * - 侧边栏、首页卡片都只是「勾选分组」的展示组合。
*/ */
type TabKey = 'groups' | 'content' | 'home' | 'header' | 'profile' | 'system' type TabKey =
| 'groups'
| 'content'
| 'home-sidebar'
| 'home-cards'
| 'home-header'
| 'profile'
| 'system'
/** 左侧导航:首页管理下面挂三个子项,页头也归到这一组 */
const ADMIN_NAV: MenuItem[] = [
{ key: 'groups', label: '分组管理' },
{ key: 'content', label: '数据管理' },
{
key: 'home',
label: '首页管理',
children: [
{ key: 'home-sidebar', label: '侧边栏' },
{ key: 'home-cards', label: '内容卡片' },
{ key: 'home-header', label: '页头' },
],
},
{ key: 'profile', label: '资料' },
{ key: 'system', label: '系统' },
]
const FORMAT_OPTIONS = [ const FORMAT_OPTIONS = [
{ value: 'apps', label: '应用' }, { value: 'apps', label: '应用' },
@@ -276,21 +301,17 @@ export function AdminNext() {
)} )}
{authenticated && draft !== null && ( {authenticated && draft !== null && (
<> <div className="admin-shell">
<Tabs <nav className="admin-nav">
className="admin-tabs" <Menu
items={[ items={ADMIN_NAV}
{ key: 'groups', label: '分组管理' },
{ key: 'content', label: '数据管理' },
{ key: 'home', label: '首页管理' },
{ key: 'header', label: '页头' },
{ key: 'profile', label: '资料' },
{ key: 'system', label: '系统' },
]}
activeKey={tab} activeKey={tab}
onChange={(key) => setTab(key as TabKey)} onSelect={(key) => setTab(key as TabKey)}
itemHeight={40}
/> />
</nav>
<div className="admin-panel">
{tab === 'groups' && ( {tab === 'groups' && (
<GroupsPanel <GroupsPanel
groups={groups} groups={groups}
@@ -301,8 +322,7 @@ export function AdminNext() {
{tab === 'content' && <ContentPanel groups={groups} onReload={load} />} {tab === 'content' && <ContentPanel groups={groups} onReload={load} />}
{tab === 'home' && ( {tab === 'home-sidebar' && (
<div className="admin-home">
<section className="admin-home-section"> <section className="admin-home-section">
<Title level={4}>侧边栏</Title> <Title level={4}>侧边栏</Title>
<SidebarPanel <SidebarPanel
@@ -320,9 +340,9 @@ export function AdminNext() {
}} }}
/> />
</section> </section>
)}
<div className="sidebar-divider" /> {tab === 'home-cards' && (
<section className="admin-home-section"> <section className="admin-home-section">
<Title level={4}>内容卡片</Title> <Title level={4}>内容卡片</Title>
<Text size="sm" tone="muted"> <Text size="sm" tone="muted">
@@ -337,10 +357,9 @@ export function AdminNext() {
onDelete={(id, title) => setPending({ kind: 'card', id: String(id), title })} onDelete={(id, title) => setPending({ kind: 'card', id: String(id), title })}
/> />
</section> </section>
</div>
)} )}
{tab === 'header' && ( {tab === 'home-header' && (
<HeaderPanel <HeaderPanel
value={draft.header} value={draft.header}
onChange={(header) => setDraft({ ...draft, header })} onChange={(header) => setDraft({ ...draft, header })}
@@ -358,7 +377,7 @@ export function AdminNext() {
{tab === 'system' && <SystemPanel />} {tab === 'system' && <SystemPanel />}
{(tab === 'header' || tab === 'profile') && ( {(tab === 'home-header' || tab === 'profile') && (
<Space gap="sm" justify="end" className="admin-actions"> <Space gap="sm" justify="end" className="admin-actions">
<Button <Button
variant="secondary" variant="secondary"
@@ -374,7 +393,8 @@ export function AdminNext() {
</Button> </Button>
</Space> </Space>
)} )}
</> </div>
</div>
)} )}
</Card> </Card>
</main> </main>