fix: 修复拖拽排序持久化 + 恢复 package.json catalog 引用

- 引入 @dnd-kit/helpers,使用 move() 替代手工 splice 排序逻辑
- 恢复 apps/server/package.json 中所有依赖的 catalog: 引用
- 简化 ORPC client,移除 experimental_defaults,改用 MutationCache
- route loaders 改用 fetchQuery 确保数据刷新
This commit is contained in:
2026-03-31 17:01:47 +08:00
parent ba8224e81e
commit 46e4486d7d
9 changed files with 108 additions and 157 deletions
+1
View File
@@ -25,6 +25,7 @@
"dependencies": {
"@base-ui/react": "catalog:",
"@dnd-kit/dom": "catalog:",
"@dnd-kit/helpers": "catalog:",
"@dnd-kit/react": "catalog:",
"@fontsource-variable/geist": "catalog:",
"@orpc/client": "catalog:",
+1 -66
View File
@@ -24,69 +24,4 @@ const getORPCClient = createIsomorphicFn()
const client: RouterClient = getORPCClient()
export const orpc = createTanstackQueryUtils(client, {
experimental_defaults: {
bookmarks: {
category: {
create: {
mutationOptions: {
onSuccess: (_, __, ___, ctx) => {
ctx.client.invalidateQueries({ queryKey: orpc.bookmarks.category.list.key() })
},
},
},
update: {
mutationOptions: {
onSuccess: (_, __, ___, ctx) => {
ctx.client.invalidateQueries({ queryKey: orpc.bookmarks.category.list.key() })
},
},
},
remove: {
mutationOptions: {
onSuccess: (_, __, ___, ctx) => {
ctx.client.invalidateQueries({ queryKey: orpc.bookmarks.category.list.key() })
},
},
},
reorder: {
mutationOptions: {
onSuccess: (_, __, ___, ctx) => {
ctx.client.invalidateQueries({ queryKey: orpc.bookmarks.category.list.key() })
},
},
},
},
bookmark: {
create: {
mutationOptions: {
onSuccess: (_, __, ___, ctx) => {
ctx.client.invalidateQueries({ queryKey: orpc.bookmarks.category.list.key() })
},
},
},
update: {
mutationOptions: {
onSuccess: (_, __, ___, ctx) => {
ctx.client.invalidateQueries({ queryKey: orpc.bookmarks.category.list.key() })
},
},
},
remove: {
mutationOptions: {
onSuccess: (_, __, ___, ctx) => {
ctx.client.invalidateQueries({ queryKey: orpc.bookmarks.category.list.key() })
},
},
},
reorder: {
mutationOptions: {
onSuccess: (_, __, ___, ctx) => {
ctx.client.invalidateQueries({ queryKey: orpc.bookmarks.category.list.key() })
},
},
},
},
},
},
})
export const orpc = createTanstackQueryUtils(client)
@@ -1,6 +1,7 @@
import { move } from '@dnd-kit/helpers'
import { DragDropProvider } from '@dnd-kit/react'
import { useSortable } from '@dnd-kit/react/sortable'
import { useMutation } from '@tanstack/react-query'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import * as icons from 'lucide-react'
import { ExternalLink, GripVertical, Pencil, Plus, Trash2 } from 'lucide-react'
import { useEffect, useState } from 'react'
@@ -114,45 +115,33 @@ const SortableBookmarkItem = ({
export const BookmarkManager = ({ category }: BookmarkManagerProps) => {
const [items, setItems] = useState(category.bookmarks)
const queryClient = useQueryClient()
const reorderBookmarks = useMutation(orpc.bookmarks.bookmark.reorder.mutationOptions())
useEffect(() => {
if (!reorderBookmarks.isPending) {
setItems(category.bookmarks)
}
}, [category.bookmarks, reorderBookmarks.isPending])
setItems(category.bookmarks)
}, [category.bookmarks])
const handleDragEnd: NonNullable<React.ComponentProps<typeof DragDropProvider>['onDragEnd']> = (event) => {
if (event.canceled) {
return
}
if (event.canceled) return
const sourceId = event.operation.source?.id
const targetId = event.operation.target?.id
if (!sourceId || !targetId || sourceId === targetId) {
return
}
const sourceIndex = items.findIndex((item) => item.id === sourceId)
const targetIndex = items.findIndex((item) => item.id === targetId)
if (sourceIndex === -1 || targetIndex === -1) {
return
}
const reordered = [...items]
const [moved] = reordered.splice(sourceIndex, 1)
if (!moved) {
return
}
reordered.splice(targetIndex, 0, moved)
const reordered = move(items, event)
const previousItems = items
setItems(reordered)
reorderBookmarks.mutate(
reordered.map((item, index) => ({ id: item.id, orderId: index })),
{
onSuccess: () => toast.success('书签顺序已更新'),
onError: () => toast.error('操作失败'),
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: orpc.bookmarks.category.list.queryOptions().queryKey,
})
toast.success('书签顺序已更新')
},
onError: () => {
setItems(previousItems)
toast.error('操作失败')
},
},
)
}
@@ -1,6 +1,7 @@
import { move } from '@dnd-kit/helpers'
import { DragDropProvider } from '@dnd-kit/react'
import { useSortable } from '@dnd-kit/react/sortable'
import { useMutation } from '@tanstack/react-query'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { GripVertical, MoreHorizontal, Pencil, Plus, Trash2 } from 'lucide-react'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
@@ -111,45 +112,33 @@ const SortableCategoryItem = ({
export const CategoryManager = ({ categories, selectedCategoryId, onSelectCategory }: CategoryManagerProps) => {
const [items, setItems] = useState(categories)
const queryClient = useQueryClient()
const reorderCategories = useMutation(orpc.bookmarks.category.reorder.mutationOptions())
useEffect(() => {
if (!reorderCategories.isPending) {
setItems(categories)
}
}, [categories, reorderCategories.isPending])
setItems(categories)
}, [categories])
const handleDragEnd: NonNullable<React.ComponentProps<typeof DragDropProvider>['onDragEnd']> = (event) => {
if (event.canceled) {
return
}
if (event.canceled) return
const sourceId = event.operation.source?.id
const targetId = event.operation.target?.id
if (!sourceId || !targetId || sourceId === targetId) {
return
}
const sourceIndex = items.findIndex((item) => item.id === sourceId)
const targetIndex = items.findIndex((item) => item.id === targetId)
if (sourceIndex === -1 || targetIndex === -1) {
return
}
const reordered = [...items]
const [moved] = reordered.splice(sourceIndex, 1)
if (!moved) {
return
}
reordered.splice(targetIndex, 0, moved)
const reordered = move(items, event)
const previousItems = items
setItems(reordered)
reorderCategories.mutate(
reordered.map((item, index) => ({ id: item.id, orderId: index })),
{
onSuccess: () => toast.success('分类顺序已更新'),
onError: () => toast.error('操作失败'),
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: orpc.bookmarks.category.list.queryOptions().queryKey,
})
toast.success('分类顺序已更新')
},
onError: () => {
setItems(previousItems)
toast.error('操作失败')
},
},
)
}
+15 -1
View File
@@ -1,4 +1,4 @@
import { QueryClient } from '@tanstack/react-query'
import { MutationCache, QueryClient } from '@tanstack/react-query'
import { createRouter } from '@tanstack/react-router'
import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query'
import type { RouterContext } from './routes/__root'
@@ -6,6 +6,20 @@ import { routeTree } from './routeTree.gen'
export const getRouter = () => {
const queryClient = new QueryClient({
mutationCache: new MutationCache({
onSuccess: (_data, _variables, _context, mutation) => {
const key = mutation.options.mutationKey
if (Array.isArray(key) && Array.isArray(key[0]) && key[0].length > 0) {
const module = key[0][0]
queryClient.invalidateQueries({
predicate: (query) => {
const qk = query.queryKey
return Array.isArray(qk) && Array.isArray(qk[0]) && qk[0][0] === module
},
})
}
},
}),
defaultOptions: {
queries: {
staleTime: 30 * 1000,
@@ -8,7 +8,7 @@ import { CategoryManager } from '@/modules/bookmarks/components/CategoryManager'
export const Route = createFileRoute('/_protected/admin/bookmarks' as never)({
loader: async ({ context }: { context: { queryClient: QueryClient } }) => {
await context.queryClient.ensureQueryData(orpc.bookmarks.category.list.queryOptions())
await context.queryClient.fetchQuery(orpc.bookmarks.category.list.queryOptions())
},
component: BookmarksAdmin,
})
+1 -1
View File
@@ -9,7 +9,7 @@ import { SearchBar } from '@/modules/bookmarks/components/SearchBar'
export const Route = createFileRoute('/_protected/' as never)({
loader: async ({ context }: { context: { queryClient: QueryClient } }) => {
await context.queryClient.ensureQueryData(orpc.bookmarks.category.list.queryOptions())
await context.queryClient.fetchQuery(orpc.bookmarks.category.list.queryOptions())
},
component: DashboardPage,
})