feat: 侧栏卡片显隐、本地服务卡片恢复、页面同步滚动
- 侧边栏设置新增「侧栏卡片」:本地管理员/本地服务显隐,即时保存 show_profile/show_service - 首页固定第一位恢复「本地服务」卡片(运行时项目网格),移除卡片三点菜单 - 后台首页卡片新增只读「本地服务」表,标记不可删除 - 前台改为整页滚动,左右两栏同步 - 验证:显隐落库、三点为 0、本地表无删除、滚动两栏同步 300px
This commit is contained in:
@@ -1299,6 +1299,11 @@ a.entry-href:hover {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sidebar-divider {
|
||||
margin: var(--su-space-2) 0;
|
||||
border-top: 1px dashed var(--su-border-strong);
|
||||
}
|
||||
|
||||
/* 首页卡片:左侧卡片列表 + 右侧分组表格 */
|
||||
.cards-manager {
|
||||
display: grid;
|
||||
|
||||
+36
-11
@@ -208,6 +208,8 @@ export default function AppNext() {
|
||||
const site: SiteSettings = settings?.settings ?? {
|
||||
header: { enabled: true, speed: 24, items: [] },
|
||||
sidebar: {
|
||||
show_profile: true,
|
||||
show_service: true,
|
||||
hidden: [],
|
||||
order: [],
|
||||
hidden_groups: [],
|
||||
@@ -309,6 +311,30 @@ export default function AppNext() {
|
||||
)
|
||||
}
|
||||
|
||||
function renderLocalService() {
|
||||
const items: AppItem[] = projects.map((project) => ({
|
||||
id: project.name,
|
||||
name: project.name,
|
||||
desc: project.route_path,
|
||||
icon: project.name.slice(0, 1).toUpperCase(),
|
||||
href: project.route_path,
|
||||
badge: '本地项目',
|
||||
comment: '本地静态站点,由 Stash 直接托管。',
|
||||
views: 0,
|
||||
likes: 0,
|
||||
}))
|
||||
|
||||
return (
|
||||
<SectionCard title="本地服务" count={items.length} barColor="#3858f6">
|
||||
{items.length > 0 ? (
|
||||
<AppGrid items={items} />
|
||||
) : (
|
||||
<Empty text="还没有项目:在托管目录下建一个子目录并放入 index.html,刷新后就会出现在这里。" />
|
||||
)}
|
||||
</SectionCard>
|
||||
)
|
||||
}
|
||||
|
||||
function renderCard(card: CardItem) {
|
||||
const resolved = card.groups
|
||||
.map((ref) => ({ ref, group: findGroup(manage.groups, ref.groupKey) }))
|
||||
@@ -327,7 +353,6 @@ export default function AppNext() {
|
||||
title={card.title}
|
||||
count={total}
|
||||
barColor={first?.barColor}
|
||||
onMore={() => first !== undefined && navigate(`/g/${encodeURIComponent(first.key)}`)}
|
||||
>
|
||||
{resolved.map(({ group, ref }) =>
|
||||
renderGroupBlock(group, ref.previewLimit ?? defaultLimit(group), resolved.length > 1),
|
||||
@@ -342,6 +367,7 @@ export default function AppNext() {
|
||||
<div className="home-view">
|
||||
{loading && <Empty text="正在读取内容库…" />}
|
||||
{!loading && cards.length === 0 && <Empty text="后台还没有创建首页卡片" />}
|
||||
{renderLocalService()}
|
||||
{cards.map(renderCard)}
|
||||
{keywordLower !== '' && cards.length > 0 && (
|
||||
<Notice title={`搜索结果:${keyword.trim()}`}>在卡片与分组内按标题过滤。</Notice>
|
||||
@@ -521,9 +547,6 @@ export default function AppNext() {
|
||||
siderBordered={false}
|
||||
contentPadding="none"
|
||||
contentOffset={88}
|
||||
height="screen"
|
||||
scrollable
|
||||
hideScrollbar
|
||||
contentId="content-scroll"
|
||||
className="app"
|
||||
header={
|
||||
@@ -546,13 +569,15 @@ export default function AppNext() {
|
||||
}
|
||||
sider={
|
||||
<div className="sidebar">
|
||||
<ProfileCard profile={site.profile} />
|
||||
<ServicePanel
|
||||
projects={projects}
|
||||
live={live}
|
||||
refreshing={refreshing}
|
||||
onRefresh={refresh}
|
||||
/>
|
||||
{site.sidebar.show_profile !== false && <ProfileCard profile={site.profile} />}
|
||||
{site.sidebar.show_service !== false && (
|
||||
<ServicePanel
|
||||
projects={projects}
|
||||
live={live}
|
||||
refreshing={refreshing}
|
||||
onRefresh={refresh}
|
||||
/>
|
||||
)}
|
||||
<div className="nav-card">
|
||||
<Menu
|
||||
items={sidebarItems}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Space,
|
||||
Switch,
|
||||
Tabs,
|
||||
Tag,
|
||||
Text,
|
||||
Textarea,
|
||||
Title,
|
||||
@@ -47,6 +48,8 @@ import {
|
||||
type GroupNode,
|
||||
} from '../../manage'
|
||||
import { createItem, deleteItem, fetchItem, saveItem } from '../../items'
|
||||
import { useProjects } from '../../useProjects'
|
||||
import type { Project } from '../../api'
|
||||
|
||||
/**
|
||||
* 新版后台:原子模型。
|
||||
@@ -96,6 +99,7 @@ export function AdminNext() {
|
||||
const [cards, setCards] = useState<CardItem[]>([])
|
||||
const [tab, setTab] = useState<TabKey>('groups')
|
||||
const [saving, setSaving] = useState(false)
|
||||
const { projects } = useProjects()
|
||||
const [pending, setPending] = useState<{
|
||||
kind: 'group' | 'card'
|
||||
id: string
|
||||
@@ -257,6 +261,14 @@ export function AdminNext() {
|
||||
else hidden.add(key)
|
||||
void saveSidebar({ ...draft.sidebar, tree_hidden: [...hidden] })
|
||||
}}
|
||||
onToggleCard={(card, visible) => {
|
||||
void saveSidebar({
|
||||
...draft.sidebar,
|
||||
...(card === 'profile'
|
||||
? { show_profile: visible }
|
||||
: { show_service: visible }),
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -264,6 +276,7 @@ export function AdminNext() {
|
||||
<CardsPanel
|
||||
groups={groups}
|
||||
cards={cards}
|
||||
projects={projects}
|
||||
onReload={load}
|
||||
onDelete={(id, title) => setPending({ kind: 'card', id: String(id), title })}
|
||||
/>
|
||||
@@ -1190,11 +1203,13 @@ function SidebarPanel({
|
||||
groups,
|
||||
value,
|
||||
onToggle,
|
||||
onToggleCard,
|
||||
saving,
|
||||
}: {
|
||||
groups: GroupNode[]
|
||||
value: SiteSettings['sidebar']
|
||||
onToggle: (key: string, visible: boolean) => void
|
||||
onToggleCard: (card: 'profile' | 'service', visible: boolean) => void
|
||||
saving: boolean
|
||||
}) {
|
||||
const hidden = new Set(value.tree_hidden)
|
||||
@@ -1219,7 +1234,38 @@ function SidebarPanel({
|
||||
return (
|
||||
<Space direction="column" gap="md" align="stretch" className="admin-form">
|
||||
<Text size="sm" tone="muted">
|
||||
勾选要在侧边栏显示的分组;子分组可以单独勾选,父分组不勾选也不影响子项。开关修改后立即保存。
|
||||
侧栏卡片与分组导航分开控制;开关修改后立即保存。
|
||||
</Text>
|
||||
|
||||
<Text size="sm" strong>
|
||||
侧栏卡片
|
||||
</Text>
|
||||
<div className="sidebar-toggle-row">
|
||||
<Text className="admin-item-title">本地管理员</Text>
|
||||
<Switch
|
||||
checked={value.show_profile !== false}
|
||||
onChange={(visible) => onToggleCard('profile', visible)}
|
||||
label="显示本地管理员卡片"
|
||||
disabled={saving}
|
||||
/>
|
||||
</div>
|
||||
<div className="sidebar-toggle-row">
|
||||
<Text className="admin-item-title">本地服务</Text>
|
||||
<Switch
|
||||
checked={value.show_service !== false}
|
||||
onChange={(visible) => onToggleCard('service', visible)}
|
||||
label="显示本地服务卡片"
|
||||
disabled={saving}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="sidebar-divider" />
|
||||
|
||||
<Text size="sm" strong>
|
||||
分组导航
|
||||
</Text>
|
||||
<Text size="sm" tone="muted">
|
||||
勾选要在侧边栏显示的分组;子分组可以单独勾选,父分组不勾选也不影响子项。
|
||||
</Text>
|
||||
{render(groups, 0)}
|
||||
</Space>
|
||||
@@ -1231,16 +1277,19 @@ function SidebarPanel({
|
||||
function CardsPanel({
|
||||
groups,
|
||||
cards,
|
||||
projects,
|
||||
onReload,
|
||||
onDelete,
|
||||
}: {
|
||||
groups: GroupNode[]
|
||||
cards: CardItem[]
|
||||
projects: Project[]
|
||||
onReload: () => void
|
||||
onDelete: (id: number, title: string) => void
|
||||
}) {
|
||||
const options = useMemo(() => flattenCardsGroups(groups), [groups])
|
||||
const [activeId, setActiveId] = useState<number | null>(cards[0]?.id ?? null)
|
||||
const [view, setView] = useState<'local' | 'card'>('local')
|
||||
const [pendingId, setPendingId] = useState<number | null>(null)
|
||||
const [newOpen, setNewOpen] = useState(false)
|
||||
const [newTitle, setNewTitle] = useState('')
|
||||
@@ -1256,6 +1305,7 @@ function CardsPanel({
|
||||
return
|
||||
}
|
||||
if (pendingId !== null && cards.some((card) => card.id === pendingId)) {
|
||||
setView('card')
|
||||
setActiveId(pendingId)
|
||||
setPendingId(null)
|
||||
return
|
||||
@@ -1328,6 +1378,15 @@ function CardsPanel({
|
||||
</div>
|
||||
|
||||
<div className="cards-side-list">
|
||||
<button
|
||||
type="button"
|
||||
className={`cards-side-item${view === 'local' ? ' is-active' : ''}`}
|
||||
onClick={() => setView('local')}
|
||||
>
|
||||
<span className="cards-side-title">本地服务</span>
|
||||
<span className="cards-side-count">{projects.length}</span>
|
||||
</button>
|
||||
|
||||
{cards.map((card) => (
|
||||
<button
|
||||
key={card.id}
|
||||
@@ -1335,13 +1394,16 @@ function CardsPanel({
|
||||
draggable={!reordering}
|
||||
className={[
|
||||
'cards-side-item',
|
||||
active?.id === card.id ? 'is-active' : '',
|
||||
view === 'card' && active?.id === card.id ? 'is-active' : '',
|
||||
dragId === card.id ? 'is-dragging' : '',
|
||||
overId === card.id && dragId !== null && dragId !== card.id ? 'is-over' : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
onClick={() => setActiveId(card.id)}
|
||||
onClick={() => {
|
||||
setView('card')
|
||||
setActiveId(card.id)
|
||||
}}
|
||||
onDragStart={(event) => {
|
||||
setDragId(card.id)
|
||||
event.dataTransfer.effectAllowed = 'move'
|
||||
@@ -1382,7 +1444,9 @@ function CardsPanel({
|
||||
</aside>
|
||||
|
||||
<div className="cards-main">
|
||||
{active === undefined ? (
|
||||
{view === 'local' ? (
|
||||
<LocalServiceTable projects={projects} />
|
||||
) : active === undefined ? (
|
||||
<Text tone="muted">新建一张卡片开始配置。</Text>
|
||||
) : (
|
||||
<CardTable
|
||||
@@ -1435,6 +1499,60 @@ function CardsPanel({
|
||||
)
|
||||
}
|
||||
|
||||
function LocalServiceTable({ projects }: { projects: Project[] }) {
|
||||
return (
|
||||
<div>
|
||||
<div className="cards-table-head">
|
||||
<div className="admin-node-title">
|
||||
<Input value="本地服务" disabled aria-label="本地服务卡片" />
|
||||
</div>
|
||||
<Tag tone="neutral">不可删除</Tag>
|
||||
</div>
|
||||
|
||||
<Text size="sm" tone="muted">
|
||||
本地服务卡片由托管目录自动生成,随项目扫描更新;后台只能查看,不能删除或改名。
|
||||
</Text>
|
||||
|
||||
<div className="group-table-wrap cards-table-wrap">
|
||||
<table className="group-table cards-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>项目名称</th>
|
||||
<th className="entry-col-href">访问地址</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{projects.map((project) => (
|
||||
<tr key={project.name}>
|
||||
<td>
|
||||
<span className="group-name">{project.name}</span>
|
||||
</td>
|
||||
<td>
|
||||
<a
|
||||
className="entry-href"
|
||||
href={project.route_path}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{project.route_path}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{projects.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={2} className="group-empty">
|
||||
还没有托管项目
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTable({
|
||||
card,
|
||||
options,
|
||||
|
||||
@@ -39,6 +39,10 @@ export type SiteSettings = {
|
||||
items: HeaderItem[]
|
||||
}
|
||||
sidebar: {
|
||||
/** 是否显示「本地管理员」资料卡 */
|
||||
show_profile: boolean
|
||||
/** 是否显示「本地服务」面板 */
|
||||
show_service: boolean
|
||||
hidden: string[]
|
||||
order: string[]
|
||||
/** 隐藏的分组:`分区id/分组id` */
|
||||
@@ -84,6 +88,8 @@ export function defaultSettings(): SiteSettings {
|
||||
],
|
||||
},
|
||||
sidebar: {
|
||||
show_profile: true,
|
||||
show_service: true,
|
||||
hidden: [],
|
||||
order: [],
|
||||
hidden_groups: [],
|
||||
|
||||
Reference in New Issue
Block a user