diff --git a/Changelog.md b/Changelog.md
index 654c7ada6..023204d14 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -53,5 +53,6 @@
- 配置变更后统一广播刷新,界面开关与托盘状态保持同步
- 重构命令层错误返回为结构化格式,保留完整诊断链
- 优化首页界面中当前节点固定状态显示
+- 优化拖动排序样式及动画效果
diff --git a/package.json b/package.json
index d1cc6ea78..5733db8ec 100644
--- a/package.json
+++ b/package.json
@@ -41,9 +41,8 @@
"knip:report": "knip --no-exit-code"
},
"dependencies": {
- "@dnd-kit/core": "^6.3.1",
- "@dnd-kit/sortable": "^10.0.0",
- "@dnd-kit/utilities": "^3.2.2",
+ "@dnd-kit/helpers": "^0.5.0",
+ "@dnd-kit/react": "^0.5.0",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@juggle/resize-observer": "^3.4.0",
@@ -132,7 +131,7 @@
"eslint --fix --max-warnings=0",
"biome format --write"
],
- "*.{css,scss,json}": [
+ "*.{css,json}": [
"biome format --write"
]
},
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6ced9c260..90da62b2e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -109,15 +109,12 @@ importers:
.:
dependencies:
- '@dnd-kit/core':
- specifier: ^6.3.1
- version: 6.3.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
- '@dnd-kit/sortable':
- specifier: ^10.0.0
- version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)
- '@dnd-kit/utilities':
- specifier: ^3.2.2
- version: 3.2.2(react@19.2.8)
+ '@dnd-kit/helpers':
+ specifier: ^0.5.0
+ version: 0.5.0
+ '@dnd-kit/react':
+ specifier: ^0.5.0
+ version: 0.5.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
'@emotion/react':
specifier: ^11.14.0
version: 11.14.0(@types/react@19.2.18)(react@19.2.8)
@@ -933,27 +930,29 @@ packages:
cpu: [x64]
os: [win32]
- '@dnd-kit/accessibility@3.1.1':
- resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==}
- peerDependencies:
- react: '>=16.8.0'
+ '@dnd-kit/abstract@0.5.0':
+ resolution: {integrity: sha512-hi13iMJgjPX/KDYVKg5VeDIhmYiV6buc9bAX+tCLYf4QdyYjPbsXjn2sPo6m7fQ6SGJBEFgHJ2PemeKDUbwBaA==}
- '@dnd-kit/core@6.3.1':
- resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==}
- peerDependencies:
- react: '>=16.8.0'
- react-dom: '>=16.8.0'
+ '@dnd-kit/collision@0.5.0':
+ resolution: {integrity: sha512-xUqRn3lS7oqLkT0AnnHS/STh/Czvwe1UapZFYiLbsUGxopMsQd4teaPCzPouOThoMdGEe+dHWjfqJl6t9iG4mQ==}
- '@dnd-kit/sortable@10.0.0':
- resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==}
- peerDependencies:
- '@dnd-kit/core': ^6.3.0
- react: '>=16.8.0'
+ '@dnd-kit/dom@0.5.0':
+ resolution: {integrity: sha512-f2xFJp5SYQ8EW/Fbtaa8iBb66hpkWc7qa8vU826KW11/tb44sH+AisZnGtwOOTWTQ0GraqBDr5ixTErww+eKXw==}
- '@dnd-kit/utilities@3.2.2':
- resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==}
+ '@dnd-kit/geometry@0.5.0':
+ resolution: {integrity: sha512-ubHQS1CiSDH8ssYH2xG5BnpwPSFP1tStXXjug7/Ba6qnQdu/EUH47l6QXKIksQnnanfVfDf0aGeevRxgZlj28A==}
+
+ '@dnd-kit/helpers@0.5.0':
+ resolution: {integrity: sha512-i4y+51/icSw+OHMr/su19qhnmNhAzh8PnBwXvapFYTd+64oodIyJRiRkB+hhfxAfnur7RYSW8qacDTrXjg2XOg==}
+
+ '@dnd-kit/react@0.5.0':
+ resolution: {integrity: sha512-abQPLI8lmfVE+v/n+pqy5WFxrw6T2Yg0UQZsL78dp5DKci7dKTVDjvLWqvass+XTFtzJmsZEjk1NdqE6xG8Jiw==}
peerDependencies:
- react: '>=16.8.0'
+ react: ^18.0.0 || ^19.0.0
+ react-dom: ^18.0.0 || ^19.0.0
+
+ '@dnd-kit/state@0.5.0':
+ resolution: {integrity: sha512-y7XbabQqjF58Lk8YmDQuR8l6QjN+Kh4qlGEjUvHuIeasLk1QP+9L5diXS98VMxQIivyMmUtX2//f+3N7qPJX4w==}
'@emnapi/core@1.10.0':
resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==}
@@ -1641,6 +1640,9 @@ packages:
'@popperjs/core@2.11.8':
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
+ '@preact/signals-core@1.14.4':
+ resolution: {integrity: sha512-HNB6HYeYKhQbJ1aKl+YRjrS4+QWHLKX6qKoUsfS/m0vqzsVaEBiZiaKbG/e+NKk2ch5ALQr/ihWaMHxiCuuWHA==}
+
'@rolldown/binding-android-arm-eabi@1.2.6':
resolution: {integrity: sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -4775,29 +4777,48 @@ snapshots:
'@biomejs/cli-win32-x64@2.5.10':
optional: true
- '@dnd-kit/accessibility@3.1.1(react@19.2.8)':
+ '@dnd-kit/abstract@0.5.0':
dependencies:
- react: 19.2.8
+ '@dnd-kit/geometry': 0.5.0
+ '@dnd-kit/state': 0.5.0
tslib: 2.8.1
- '@dnd-kit/core@6.3.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
+ '@dnd-kit/collision@0.5.0':
dependencies:
- '@dnd-kit/accessibility': 3.1.1(react@19.2.8)
- '@dnd-kit/utilities': 3.2.2(react@19.2.8)
+ '@dnd-kit/abstract': 0.5.0
+ '@dnd-kit/geometry': 0.5.0
+ tslib: 2.8.1
+
+ '@dnd-kit/dom@0.5.0':
+ dependencies:
+ '@dnd-kit/abstract': 0.5.0
+ '@dnd-kit/collision': 0.5.0
+ '@dnd-kit/geometry': 0.5.0
+ '@dnd-kit/state': 0.5.0
+ tslib: 2.8.1
+
+ '@dnd-kit/geometry@0.5.0':
+ dependencies:
+ '@dnd-kit/state': 0.5.0
+ tslib: 2.8.1
+
+ '@dnd-kit/helpers@0.5.0':
+ dependencies:
+ '@dnd-kit/abstract': 0.5.0
+ tslib: 2.8.1
+
+ '@dnd-kit/react@0.5.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
+ dependencies:
+ '@dnd-kit/abstract': 0.5.0
+ '@dnd-kit/dom': 0.5.0
+ '@dnd-kit/state': 0.5.0
react: 19.2.8
react-dom: 19.2.8(react@19.2.8)
tslib: 2.8.1
- '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)':
+ '@dnd-kit/state@0.5.0':
dependencies:
- '@dnd-kit/core': 6.3.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
- '@dnd-kit/utilities': 3.2.2(react@19.2.8)
- react: 19.2.8
- tslib: 2.8.1
-
- '@dnd-kit/utilities@3.2.2(react@19.2.8)':
- dependencies:
- react: 19.2.8
+ '@preact/signals-core': 1.14.4
tslib: 2.8.1
'@emnapi/core@1.10.0':
@@ -5441,6 +5462,8 @@ snapshots:
'@popperjs/core@2.11.8': {}
+ '@preact/signals-core@1.14.4': {}
+
'@rolldown/binding-android-arm-eabi@1.2.6':
optional: true
diff --git a/src/assets/styles/index.scss b/src/assets/styles/index.scss
index c597d7d7b..7edb14b0d 100644
--- a/src/assets/styles/index.scss
+++ b/src/assets/styles/index.scss
@@ -1,12 +1,12 @@
-@use './layout.scss';
-@use './page.scss';
-@use './font.scss';
+@use "./layout.scss";
+@use "./page.scss";
+@use "./font.scss";
body {
margin: 0;
font-family:
- -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
- 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
+ -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu",
+ "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
user-select: none;
@@ -59,3 +59,7 @@ body {
-moz-user-select: none;
-ms-user-select: none;
}
+
+[data-dnd-dragging] {
+ box-shadow: 0 6px 18px rgba(0, 0, 0, 0.32) !important;
+}
diff --git a/src/components/base/index.ts b/src/components/base/index.ts
index e1f87c20d..c2106db51 100644
--- a/src/components/base/index.ts
+++ b/src/components/base/index.ts
@@ -17,3 +17,4 @@ export {
StickyVirtualList,
type StickyVirtualListHandle,
} from './sticky-virtual-list'
+export { SortableItem } from './sortable-item'
diff --git a/src/components/base/sortable-item.tsx b/src/components/base/sortable-item.tsx
new file mode 100644
index 000000000..0fb923ba9
--- /dev/null
+++ b/src/components/base/sortable-item.tsx
@@ -0,0 +1,60 @@
+import { useSortable } from '@dnd-kit/react/sortable'
+import { useTheme } from '@mui/material'
+import { type CSSProperties, type ReactNode, useCallback } from 'react'
+
+export interface SortableItemRenderProps {
+ ref: (element: HTMLElement | null) => void
+ handleRef: (element: HTMLElement | null) => void
+ style: CSSProperties
+}
+
+interface SortableItemProps {
+ id: string
+ index: number
+ group?: string
+ disabled?: boolean
+ style?: CSSProperties
+ children: ReactNode | ((props: SortableItemRenderProps) => ReactNode)
+}
+
+export const SortableItem = (props: SortableItemProps) => {
+ const { id, index, group, disabled, children, style } = props
+ const { ref, handleRef, sortable, isDragging } = useSortable({
+ id,
+ index,
+ group,
+ disabled,
+ })
+ const theme = useTheme()
+ const setRef = useCallback(
+ (element: HTMLDivElement | null) => {
+ handleRef(element?.querySelector('[data-sortable-handle]') ?? null)
+ ref(element)
+ },
+ [handleRef, ref],
+ )
+
+ const mergedStyle: CSSProperties = {
+ position: 'relative',
+ borderRadius: 8,
+ ...style,
+ ...(sortable.transition && {
+ transition: `${sortable.transition.duration ?? 300}ms ${sortable.transition.easing ?? 'ease'} transform`,
+ }),
+ ...(isDragging && {
+ zIndex: 100,
+ // 拖拽中的元素置为不透明背景,避免半透明卡片透出下层内容。 投影由全局 [data-dnd-dragging] 规则统一施加。
+ backgroundColor: theme.palette.background.paper,
+ }),
+ }
+
+ if (typeof children === 'function') {
+ return children({ ref, handleRef, style: mergedStyle })
+ }
+
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/components/connection/connection-column-manager.tsx b/src/components/connection/connection-column-manager.tsx
index 866c06ee1..809570907 100644
--- a/src/components/connection/connection-column-manager.tsx
+++ b/src/components/connection/connection-column-manager.tsx
@@ -1,13 +1,10 @@
+import { arrayMove } from '@dnd-kit/helpers'
import {
- closestCenter,
- DndContext,
+ DragDropProvider,
PointerSensor,
- useSensor,
- useSensors,
type DragEndEvent,
-} from '@dnd-kit/core'
-import { arrayMove, SortableContext, useSortable } from '@dnd-kit/sortable'
-import { CSS } from '@dnd-kit/utilities'
+} from '@dnd-kit/react'
+import { isSortable, useSortable } from '@dnd-kit/react/sortable'
import { DragIndicatorRounded } from '@mui/icons-material'
import {
Button,
@@ -21,9 +18,13 @@ import {
ListItem,
ListItemText,
} from '@mui/material'
-import { useCallback, useMemo } from 'react'
+import { useCallback, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
+const columnPointerSensor = PointerSensor.configure({
+ activationConstraints: () => undefined,
+})
+
export interface ConnectionColumnOption {
id: string
label: string
@@ -46,14 +47,8 @@ export const ConnectionColumnManager = ({
onOrderChange,
onReset,
}: Props) => {
- const sensors = useSensors(
- useSensor(PointerSensor, {
- activationConstraint: { distance: 6 },
- }),
- )
const { t } = useTranslation()
- const items = useMemo(() => columns.map((column) => column.id), [columns])
const visibleCount = useMemo(
() => columns.filter((column) => column.visible).length,
[columns],
@@ -61,13 +56,21 @@ export const ConnectionColumnManager = ({
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
- const { active, over } = event
- if (!over || active.id === over.id) return
+ const { operation, canceled } = event
+ const { source, target } = operation
+ if (canceled || !target || !isSortable(source)) return
const order = columns.map((column) => column.id)
- const oldIndex = order.indexOf(active.id as string)
- const newIndex = order.indexOf(over.id as string)
- if (oldIndex === -1 || newIndex === -1) return
+ const { index: newIndex, initialIndex: oldIndex } = source.sortable
+ if (
+ oldIndex < 0 ||
+ newIndex < 0 ||
+ oldIndex >= order.length ||
+ newIndex >= order.length ||
+ oldIndex === newIndex
+ ) {
+ return
+ }
onOrderChange(arrayMove(order, oldIndex, newIndex))
},
@@ -80,30 +83,29 @@ export const ConnectionColumnManager = ({
{t('connections.components.columnManager.title')}
-
-
-
- {columns.map((column) => (
-
- ))}
-
-
-
+
+ {columns.map((column, index) => (
+
+ ))}
+
+
-
-
-
-
- {
- return x.uid
- })}
- >
- {profileItems.map((item) => (
-
- onSelect(item.uid, f)}
- onEdit={() => viewerRef.current?.edit(item)}
- onSave={async (prev, curr) => {
- if (prev !== curr && profiles.current === item.uid) {
- await onEnhance(false)
- }
- }}
- onDelete={() => {
- if (batchMode) {
- toggleProfileSelection(item.uid)
- } else {
- onDelete(item.uid)
- }
- }}
- batchMode={batchMode}
- isSelected={selectedProfiles.has(item.uid)}
- onSelectionChange={() => toggleProfileSelection(item.uid)}
- />
-
- ))}
-
-
+
+ {profileItems.map((item, index) => (
+ onSelect(item.uid, f)}
+ onEdit={() => viewerRef.current?.edit(item)}
+ onSave={async (prev, curr) => {
+ if (prev !== curr && profiles.current === item.uid) {
+ await onEnhance(false)
+ }
+ }}
+ onDelete={() => {
+ if (batchMode) {
+ toggleProfileSelection(item.uid)
+ } else {
+ onDelete(item.uid)
+ }
+ }}
+ batchMode={batchMode}
+ isSelected={selectedProfiles.has(item.uid)}
+ onSelectionChange={() => toggleProfileSelection(item.uid)}
+ />
+ ))}
-
-
-
-
- {
- if (prev !== curr) {
- await onEnhance(false)
- }
- }}
- />
-
-
- {
- if (prev !== curr) {
- await onEnhance(false)
- }
- }}
- />
-
+
+
+
+
+
+ {
+ if (prev !== curr) {
+ await onEnhance(false)
+ }
+ }}
+ />
-
+
+ {
+ if (prev !== curr) {
+ await onEnhance(false)
+ }
+ }}
+ />
+
+
-
-
+