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);
}
/* 后台:左侧导航 + 右侧内容 */
.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,这里只管外框 */
.brand-preview {
display: flex;
+82 -62
View File
@@ -7,13 +7,13 @@ import {
ICON_NAMES,
IconButton,
Input,
Menu,
Modal,
Notice,
PasswordInput,
Select,
Space,
Switch,
Tabs,
Tag,
Text,
Textarea,
@@ -21,6 +21,7 @@ import {
Toaster,
toast,
type IconName,
type MenuItem,
} from '@stash/ui'
import { useEffect, useMemo, useRef, useState } from 'react'
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 = [
{ value: 'apps', label: '应用' },
@@ -276,33 +301,28 @@ export function AdminNext() {
)}
{authenticated && draft !== null && (
<>
<Tabs
className="admin-tabs"
items={[
{ key: 'groups', label: '分组管理' },
{ key: 'content', label: '数据管理' },
{ key: 'home', label: '首页管理' },
{ key: 'header', label: '页头' },
{ key: 'profile', label: '资料' },
{ key: 'system', label: '系统' },
]}
activeKey={tab}
onChange={(key) => setTab(key as TabKey)}
/>
{tab === 'groups' && (
<GroupsPanel
groups={groups}
onReload={load}
onDelete={(key, title) => setPending({ kind: 'group', id: key, title })}
<div className="admin-shell">
<nav className="admin-nav">
<Menu
items={ADMIN_NAV}
activeKey={tab}
onSelect={(key) => setTab(key as TabKey)}
itemHeight={40}
/>
)}
</nav>
{tab === 'content' && <ContentPanel groups={groups} onReload={load} />}
<div className="admin-panel">
{tab === 'groups' && (
<GroupsPanel
groups={groups}
onReload={load}
onDelete={(key, title) => setPending({ kind: 'group', id: key, title })}
/>
)}
{tab === 'home' && (
<div className="admin-home">
{tab === 'content' && <ContentPanel groups={groups} onReload={load} />}
{tab === 'home-sidebar' && (
<section className="admin-home-section">
<Title level={4}>侧边栏</Title>
<SidebarPanel
@@ -320,9 +340,9 @@ export function AdminNext() {
}}
/>
</section>
)}
<div className="sidebar-divider" />
{tab === 'home-cards' && (
<section className="admin-home-section">
<Title level={4}>内容卡片</Title>
<Text size="sm" tone="muted">
@@ -337,44 +357,44 @@ export function AdminNext() {
onDelete={(id, title) => setPending({ kind: 'card', id: String(id), title })}
/>
</section>
</div>
)}
)}
{tab === 'header' && (
<HeaderPanel
value={draft.header}
onChange={(header) => setDraft({ ...draft, header })}
site={draft.site}
onChangeSite={(site) => setDraft({ ...draft, site })}
/>
)}
{tab === 'home-header' && (
<HeaderPanel
value={draft.header}
onChange={(header) => setDraft({ ...draft, header })}
site={draft.site}
onChangeSite={(site) => setDraft({ ...draft, site })}
/>
)}
{tab === 'profile' && (
<ProfilePanel
value={draft.profile}
onChange={(profile) => setDraft({ ...draft, profile })}
/>
)}
{tab === 'profile' && (
<ProfilePanel
value={draft.profile}
onChange={(profile) => setDraft({ ...draft, profile })}
/>
)}
{tab === 'system' && <SystemPanel />}
{tab === 'system' && <SystemPanel />}
{(tab === 'header' || tab === 'profile') && (
<Space gap="sm" justify="end" className="admin-actions">
<Button
variant="secondary"
onClick={() => {
setDraft(clone(draft))
void load()
}}
>
撤销改动
</Button>
<Button loading={saving} icon={<Icon name="check_line" />} onClick={save}>
保存
</Button>
</Space>
)}
</>
{(tab === 'home-header' || tab === 'profile') && (
<Space gap="sm" justify="end" className="admin-actions">
<Button
variant="secondary"
onClick={() => {
setDraft(clone(draft))
void load()
}}
>
撤销改动
</Button>
<Button loading={saving} icon={<Icon name="check_line" />} onClick={save}>
保存
</Button>
</Space>
)}
</div>
</div>
)}
</Card>
</main>