feat(admin): 后台改为左侧导航 + 右侧内容
- 原来的横向 Tabs 换成库里的 Menu,左侧固定导航、右侧面板 - 首页管理收成一组:侧边栏 / 内容卡片 / 页头 三个子项,页头从顶层移入 - 页头、资料仍需手动保存,保存/撤销按钮留在各自面板底部 - 窄屏(<=860px)导航折到内容上方 - 后台内容区宽度不变(1340),导航列 190px + 24px 间距
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user