feat: 搜索框下滚动、本地服务卡片排序、拖拽末位修复

- 恢复内容区内部滚动:页头/搜索框固定,滚动区域从搜索框下方开始;侧栏跟随内容滚动,侧栏滚轮驱动内容区
- v22:本地服务以 card 行 local-service 参与排序;首页按卡片顺序渲染,后台可拖动排序
- 本地服务卡片后端禁止删除/改名/配置分组
- 修复向下拖动时插到目标之后,倒数第二项可拖到最后
- 验证:搜索框位置固定、侧栏 scrollTop 跟随、本地服务与倒数第二拖末位均落库、首页顺序一致
This commit is contained in:
2026-10-31 15:30:09 +08:00
parent 0e6c772466
commit 57ab90a1fa
7 changed files with 116 additions and 40 deletions
+18 -26
View File
@@ -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}