From b6de05ffe4ea4b530bd14ce47fa415b0ad1c17b3 Mon Sep 17 00:00:00 2001 From: oomeow Date: Fri, 28 Aug 2026 08:17:02 +0000 Subject: [PATCH] refactor: migrate to @dnd-kit/react and simplify sortable components (#7819) * refactor: migrate to @dnd-kit/react and simplify sortable components * chore: enable css formatting in biome.json * refactor(profile): simplify icon rendering logic and add alt text * style(profiles): add overflow hidden to profile grid container * refactor(profile): add radix for parseInt and improve type safety in profiles * docs: update changelog with drag-and-drop styling improvements * refactor(profile): move hasValidName function out of ProxiesEditorViewer * refactor(proxy): refactor proxy chain into sortable components * feat(proxy): add drop state styling and arrow animations to proxy chain * style(profiles): reduce horizontal padding in profile grid * feat(profile): add vertical margin to editor viewer items * chore: refresh pnpm lockfile * fix: preserve drag and drop behavior --------- Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com> --- Changelog.md | 1 + package.json | 7 +- pnpm-lock.yaml | 103 +++--- src/assets/styles/index.scss | 14 +- src/components/base/index.ts | 1 + src/components/base/sortable-item.tsx | 60 ++++ .../connection/connection-column-manager.tsx | 121 ++++---- src/components/home/test-card.tsx | 117 +++---- src/components/layout/layout-item.tsx | 56 +--- src/components/profile/group-item.tsx | 38 +-- .../profile/groups-editor-viewer.tsx | 274 +++++++++------- src/components/profile/profile-item.tsx | 30 +- .../profile/proxies-editor-viewer.tsx | 283 +++++++++-------- src/components/profile/proxy-item.tsx | 29 +- src/components/profile/rule-item.tsx | 34 +- .../profile/rules-editor-viewer.tsx | 233 ++++++++------ .../profile/sortable-profile-item.tsx | 55 ---- src/components/proxy/proxy-chain.tsx | 292 ++++++++++-------- src/components/test/test-item.tsx | 37 +-- src/pages/_layout.tsx | 123 +++----- src/pages/_layout/hooks/use-nav-menu-order.ts | 30 +- src/pages/profiles.tsx | 282 ++++++++--------- 22 files changed, 1105 insertions(+), 1115 deletions(-) create mode 100644 src/components/base/sortable-item.tsx delete mode 100644 src/components/profile/sortable-profile-item.tsx 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) + } + }} + /> + + - - +