feat: 搜索框下滚动、本地服务卡片排序、拖拽末位修复
- 恢复内容区内部滚动:页头/搜索框固定,滚动区域从搜索框下方开始;侧栏跟随内容滚动,侧栏滚轮驱动内容区 - v22:本地服务以 card 行 local-service 参与排序;首页按卡片顺序渲染,后台可拖动排序 - 本地服务卡片后端禁止删除/改名/配置分组 - 修复向下拖动时插到目标之后,倒数第二项可拖到最后 - 验证:搜索框位置固定、侧栏 scrollTop 跟随、本地服务与倒数第二拖末位均落库、首页顺序一致
This commit is contained in:
@@ -205,6 +205,39 @@ export default function AppNext() {
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const content = document.getElementById('content-scroll')
|
||||
const sider = document.querySelector('aside')
|
||||
if (!(content instanceof HTMLElement) || !(sider instanceof HTMLElement)) return
|
||||
|
||||
// 左右两栏共用一个滚动位置:侧栏没有独立滚动条,滚轮也驱动内容区
|
||||
sider.id = 'sidebar-scroll'
|
||||
sider.style.overflow = 'hidden'
|
||||
let frame = 0
|
||||
const sync = () => {
|
||||
if (frame !== 0) return
|
||||
frame = window.requestAnimationFrame(() => {
|
||||
frame = 0
|
||||
sider.scrollTop = content.scrollTop
|
||||
})
|
||||
}
|
||||
const onWheel = (event: WheelEvent) => {
|
||||
event.preventDefault()
|
||||
content.scrollTop += event.deltaY
|
||||
}
|
||||
|
||||
content.addEventListener('scroll', sync, { passive: true })
|
||||
sider.addEventListener('wheel', onWheel, { passive: false })
|
||||
|
||||
return () => {
|
||||
content.removeEventListener('scroll', sync)
|
||||
sider.removeEventListener('wheel', onWheel)
|
||||
if (frame !== 0) window.cancelAnimationFrame(frame)
|
||||
sider.id = ''
|
||||
sider.style.overflow = ''
|
||||
}
|
||||
}, [])
|
||||
|
||||
const site: SiteSettings = settings?.settings ?? {
|
||||
header: { enabled: true, speed: 24, items: [] },
|
||||
sidebar: {
|
||||
@@ -311,7 +344,7 @@ export default function AppNext() {
|
||||
)
|
||||
}
|
||||
|
||||
function renderLocalService() {
|
||||
function renderLocalService(card: CardItem) {
|
||||
const items: AppItem[] = projects.map((project) => ({
|
||||
id: project.name,
|
||||
name: project.name,
|
||||
@@ -325,7 +358,7 @@ export default function AppNext() {
|
||||
}))
|
||||
|
||||
return (
|
||||
<SectionCard title="本地服务" count={items.length} barColor="#3858f6">
|
||||
<SectionCard key={card.id} title="本地服务" count={items.length} barColor="#3858f6">
|
||||
{items.length > 0 ? (
|
||||
<AppGrid items={items} />
|
||||
) : (
|
||||
@@ -367,8 +400,9 @@ export default function AppNext() {
|
||||
<div className="home-view">
|
||||
{loading && <Empty text="正在读取内容库…" />}
|
||||
{!loading && cards.length === 0 && <Empty text="后台还没有创建首页卡片" />}
|
||||
{renderLocalService()}
|
||||
{cards.map(renderCard)}
|
||||
{cards.map((card) =>
|
||||
card.key === 'local-service' ? renderLocalService(card) : renderCard(card),
|
||||
)}
|
||||
{keywordLower !== '' && cards.length > 0 && (
|
||||
<Notice title={`搜索结果:${keyword.trim()}`}>在卡片与分组内按标题过滤。</Notice>
|
||||
)}
|
||||
@@ -547,6 +581,9 @@ export default function AppNext() {
|
||||
siderBordered={false}
|
||||
contentPadding="none"
|
||||
contentOffset={88}
|
||||
height="screen"
|
||||
scrollable
|
||||
hideScrollbar
|
||||
contentId="content-scroll"
|
||||
className="app"
|
||||
header={
|
||||
|
||||
@@ -1289,7 +1289,6 @@ function CardsPanel({
|
||||
}) {
|
||||
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('')
|
||||
@@ -1305,7 +1304,6 @@ function CardsPanel({
|
||||
return
|
||||
}
|
||||
if (pendingId !== null && cards.some((card) => card.id === pendingId)) {
|
||||
setView('card')
|
||||
setActiveId(pendingId)
|
||||
setPendingId(null)
|
||||
return
|
||||
@@ -1338,12 +1336,16 @@ function CardsPanel({
|
||||
setOverId(null)
|
||||
if (from === null || from === targetId) return
|
||||
|
||||
const ids = cards.map((card) => card.id)
|
||||
const start = ids.indexOf(from)
|
||||
if (start < 0) return
|
||||
ids.splice(start, 1)
|
||||
const to = ids.indexOf(targetId)
|
||||
ids.splice(to, 0, from)
|
||||
const original = cards.map((card) => card.id)
|
||||
const fromIndex = original.indexOf(from)
|
||||
const targetIndex = original.indexOf(targetId)
|
||||
if (fromIndex < 0 || targetIndex < 0) return
|
||||
|
||||
const ids = original.filter((id) => id !== from)
|
||||
const targetAfter = ids.indexOf(targetId)
|
||||
// 向下拖时插到目标之后,保证能拖到列表最后一项;向上拖时插到目标之前
|
||||
const insertAt = fromIndex < targetIndex ? targetAfter + 1 : targetAfter
|
||||
ids.splice(insertAt, 0, from)
|
||||
|
||||
setReordering(true)
|
||||
const results = await Promise.all(ids.map((id, index) => updateCard(id, { sort: index })))
|
||||
@@ -1378,15 +1380,6 @@ 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}
|
||||
@@ -1394,16 +1387,13 @@ function CardsPanel({
|
||||
draggable={!reordering}
|
||||
className={[
|
||||
'cards-side-item',
|
||||
view === 'card' && active?.id === card.id ? 'is-active' : '',
|
||||
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={() => {
|
||||
setView('card')
|
||||
setActiveId(card.id)
|
||||
}}
|
||||
onClick={() => setActiveId(card.id)}
|
||||
onDragStart={(event) => {
|
||||
setDragId(card.id)
|
||||
event.dataTransfer.effectAllowed = 'move'
|
||||
@@ -1426,7 +1416,9 @@ function CardsPanel({
|
||||
⋮⋮
|
||||
</span>
|
||||
<span className="cards-side-title">{card.title}</span>
|
||||
<span className="cards-side-count">{card.groups.length}</span>
|
||||
<span className="cards-side-count">
|
||||
{card.key === 'local-service' ? projects.length : card.groups.length}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
{cards.length === 0 && (
|
||||
@@ -1444,10 +1436,10 @@ function CardsPanel({
|
||||
</aside>
|
||||
|
||||
<div className="cards-main">
|
||||
{view === 'local' ? (
|
||||
<LocalServiceTable projects={projects} />
|
||||
) : active === undefined ? (
|
||||
{active === undefined ? (
|
||||
<Text tone="muted">新建一张卡片开始配置。</Text>
|
||||
) : active.key === 'local-service' ? (
|
||||
<LocalServiceTable projects={projects} />
|
||||
) : (
|
||||
<CardTable
|
||||
key={active.id}
|
||||
|
||||
Reference in New Issue
Block a user