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