Skip to content

Comments

重构 DraggableEntry 、修正卡片高度对齐#1245

Merged
CodFrm merged 10 commits intoscriptscat:release/v1.3from
cyfung1031:layout-fix-script-card-003
Feb 18, 2026
Merged

重构 DraggableEntry 、修正卡片高度对齐#1245
CodFrm merged 10 commits intoscriptscat:release/v1.3from
cyfung1031:layout-fix-script-card-003

Conversation

@cyfung1031
Copy link
Collaborator

@cyfung1031 cyfung1031 commented Feb 14, 2026

Before

Screenshot 2026-02-14 at 14 58 06

After

Screenshot 2026-02-14 at 14 57 19

div 移除了
原来它会自己做一层 div 所以不用再做一层

重构 DraggableEntry 、修正卡片高度对齐

@cyfung1031 cyfung1031 marked this pull request as draft February 14, 2026 07:01
@cyfung1031 cyfung1031 force-pushed the layout-fix-script-card-003 branch from 31d8c2f to 37387f8 Compare February 14, 2026 07:18
@cyfung1031 cyfung1031 changed the title CSS修正:卡片高度对齐 重构 DraggableEntry 、修正卡片高度对齐 Feb 14, 2026
@cyfung1031 cyfung1031 marked this pull request as ready for review February 14, 2026 07:18
@cyfung1031 cyfung1031 added the P1 🔥 重要但是不紧急的内容 label Feb 15, 2026
@CodFrm CodFrm requested a review from Copilot February 16, 2026 08:04
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

这个 PR 重构了 ScriptCard 组件中的 DraggableEntry 实现,并修正了卡片高度对齐问题。

Changes:

  • 重构 DraggableEntry:从 forwardRef 模式改为使用 React.cloneElement 直接克隆子组件
  • 移除了 Card 外层的额外 div 包装,直接将拖拽属性应用到 Card 组件上
  • 添加了 dnd-kit 的 attributes 属性以改善无障碍支持

Comment on lines 64 to 68
{React.cloneElement(children, {
ref: setNodeRef,
style,
...attributes,
})}
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

使用 React.cloneElement 直接传递 ref 可能会覆盖子组件已有的 ref。虽然当前场景中 Card 组件没有使用 ref,但如果未来子组件发生变化或需要自己的 ref,这种实现方式会导致问题。

ScriptTable.tsx 中的 DraggableRow 使用了 composeRefs 来合并多个 ref(见 ScriptTable.tsx:107-117, 131),这是更安全的做法。建议在这里也采用同样的模式,以提高代码的健壮性和一致性。

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

某AI说不需要。现时实际使用也不需要。需要时再加回 composeRefs 吧

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

不对。小心为上。跟随一下

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

根据AI指示,还原至原有的 composeRefs / mergeRefs 做法 df0c8b1

Comment on lines 67 to 68
...attributes,
})}
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

添加了 attributes 的传递,这是一个很好的改进!attributes 包含了可访问性相关的 ARIA 属性(如 role、aria-* 等),有助于提升拖拽功能的无障碍支持。

不过需要注意,ScriptTable.tsx 中的 DraggableRow 并未使用 attributes(见 ScriptTable.tsx:124,143)。为了保持代码库的一致性,建议也在 ScriptTable.tsx 中添加 attributes 的支持。

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

不用。這PR只搞 DraggableEntry

Comment on lines 46 to 52
const style = {
// ScriptCard 移位渉及多个元件上下左右移动,DragEnd时不要使用 dnd-kit 提供的效果
...children.props.style, // Merge existing styles
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
zIndex: isDragging ? 10 : "auto",
};
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里合并了 children.props.style 和拖拽相关的样式,但需要注意对象展开的顺序。当前实现中,如果 Card 组件的 style 中定义了 transform、transition、opacity 或 zIndex,这些值会被拖拽样式覆盖。

从当前使用场景看(Card 组件没有设置这些属性),这个顺序是正确的。但如果未来 Card 的样式发生变化,需要注意可能的样式冲突。

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个顺序是正确的

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@cyfung1031 cyfung1031 marked this pull request as draft February 16, 2026 08:30
@cyfung1031 cyfung1031 marked this pull request as ready for review February 16, 2026 08:43
zIndex: isDragging ? 10 : "auto",
};

const ref = (children as any).ref;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

好奇怪的用法

Copy link
Collaborator Author

@cyfung1031 cyfung1031 Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

一般我们用的是 <div ref={setNodeRef}> ... </div>
但现在我们是直接用ArcoDesign的Card 做这个 div
所以 ArcoDesign的Card (即 children ) 的 ref function 要先抽出来,再和 setNodeRef 合并, 然后在 React.cloneElement 那边塞回合并后的 ref function.
就算 Card 没有加 ref function, 也不会有问题

const DraggableEntry = React.forwardRef<HTMLDivElement, DraggableEntryProps>(({ recordUUID, ...rest }, ref) => {
const sortable = useSortable({ id: recordUUID });
const { setNodeRef, transform, transition, listeners, setActivatorNodeRef, isDragging } = sortable;
const DraggableEntry = ({ recordUUID, children }: DraggableEntryProps) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这么重构DraggableEntry的目的是什么?

Copy link
Collaborator Author

@cyfung1031 cyfung1031 Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

修正卡片高度对齐呀
dnd-kit 加了一层 wrapper 的话布局会错误
这PR 1樓 那两个图的分别

@cyfung1031
Copy link
Collaborator Author

@CodFrm之前我有問過AI是不是用forwardRef才對。它說不是
因為我們這個是把children放進去才用CloneElement生成Dom
不是把一個已生成的DOM的ref forward去這個元件

不過我也不是很懂這些
你測試過沒問題就行

@cyfung1031
Copy link
Collaborator Author

@CodFrm之前我有問過AI是不是用forwardRef才對。它說不是 因為我們這個是把children放進去才用CloneElement生成Dom 不是把一個已生成的DOM的ref forward去這個元件

不過我也不是很懂這些 你測試過沒問題就行

不然你傳的是一個已生成的ref
而不是clone出來的ref

@cyfung1031
Copy link
Collaborator Author

重新再看了一次
把名字改成 DraggableInjector
避免你误以为是 forwardRef
这次重构用的是 CloneElement


我们来重新梳理一下 React.cloneElement 和 forwardRef 在 dnd-kit 拖拽场景里的区别,以及为什么你的代码选择 cloneElement 是正确的。

一、React.cloneElement 的核心作用

React.cloneElement 的作用非常明确:
它拿一个已存在的 React 元素,复制一份,然后用你提供的新 props 去覆盖原来的 props,最后返回这个新的元素。
原来的元素完全不受影响。

关键点在于:

  • 它可以覆盖任何 props,包括 ref
  • 这是普通方式(比如直接 {...props} 展开)做不到的,因为 ref 不是普通属性

代码示例:

const original = <div className="old-class">内容</div>;

const cloned = React.cloneElement(original, {
  className: "new-class",
  ref: someRef,
});

结果就是新的 div,className 被覆盖,ref 也被设置,原对象不变。

二、在 dnd-kit 场景中为什么必须用它

你的组件本质上是一个“行为增强器”:

  • 给 children 注入拖拽能力(attributes、style、ref)
  • 不能增加额外的 DOM 层(否则布局、flex/grid、CSS 选择器都会出问题)
  • dnd-kit 要求 ref 必须直接挂在可拖拽的真实 DOM 元素上

所以你写了:

const mergedRef = composeRefs(setNodeRef, children.ref);

return React.cloneElement(children, {
  ...attributes,
  ref: mergedRef,
  style: { ...children.props.style, ...style },
});

这三步做到了:

  1. 保留 children 原来的 ref(如果有的话)
  2. 合并 dnd-kit 的 setNodeRef
  3. 注入拖拽所需的 attributes 和 style
  4. 没有多一层 DOM,布局和结构保持原样

三、如果用
{children}
会怎样

很多人第一反应是包一层 div:

<div ref={setNodeRef} {...attributes} style={style}>
  {children}
</div>

问题立刻出现:

  • 多了一层 DOM,flex/grid 布局可能错位(子元素变成孙元素)
  • CSS 选择器(比如 :first-child、nth-child)失效
  • dnd-kit 有时会因为 ref 没挂在最外层真实元素而出问题
  • 性能和可维护性都变差

cloneElement 完美绕过了这些问题。

四、forwardRef 为什么不适合这里

forwardRef 的设计目的是让父组件能把 ref 传给子组件内部的某个 DOM

典型用法:

const MyInput = forwardRef((props, ref) => (
  <input ref={ref} {...props} />
));

它是“从外向内传递 ref”的工具。

但你的需求正好相反:
你需要从 children 身上读取它已有的 ref(如果使用者传了 ref),然后和 dnd-kit 的 ref 合并,再写回去。

forwardRef 完全读不到 children 的 ref。它只能处理从父组件传进来的 ref。

如果你把组件改成 forwardRef 包一层:

  • 你拿到的 ref 是父组件传给你的
  • children 原有的 ref 被忽略
  • dnd 的 setNodeRef 还是挂不到正确的元素上

方向完全反了。

五、ref 合并的必要性

如果使用者这样用你的组件:

<Draggable>
  <Card ref={myCardRef} />
</Draggable>

children 已经有 ref 了。
你再直接 ref={setNodeRef} 会覆盖掉 myCardRef,导致外部拿不到 Card 的实例。

所以必须用 composeRefs(或 useMergedRef)把两个 ref 合并:

const mergedRef = composeRefs(setNodeRef, children.ref);

cloneElement 能把这个 mergedRef 正确覆盖回去,这是 forwardRef 做不到的。

六、总结对比

需求 forwardRef 是否能解决 cloneElement + composeRefs 是否能解决
父组件传 ref 给内部 DOM
读取 children 已有的 ref
合并两个 ref 并覆盖回去
不增加额外 DOM 层 通常会增加
适合 dnd-kit 这种场景 不适合 非常适合

七、结论

你的场景是典型的“无 DOM 行为注入”:

  • 不产出额外节点
  • 增强 children 的能力
  • 保留使用者原有的 ref 和 props

React 生态里这种模式非常常见(Radix UI、Headless UI、很多拖拽库的 wrapper 都这么做)。
cloneElement + ref 合并 是目前最干净、最符合需求的解法。

forwardRef 在这里不但帮不上忙,反而会让 ref 指向错误的方向。

@CodFrm
Copy link
Member

CodFrm commented Feb 18, 2026

大概明白你的想法,如果追求干净,这样透传实现没有问题

不过我刚刚又看了一下这块的逻辑,其实根本不需要处理ref,这个组件其实是为了抽离Card的拖拽逻辑写的,当时应该是从Table那边复制过来的,Table那边也不用

@cyfung1031
Copy link
Collaborator Author

ArcoDesign.Card 没有使用 ref

删除拖拽组件ref逻辑 可以. 没问题

如果不是Card,改成其他有用到 ref 的元件的时候再算

@cyfung1031
Copy link
Collaborator Author

Table 那边的 DraggableContainer 是直接生成一个 tbody 出来
DraggableRow 就是生成一个 tr
(ArcoDesign Table设计就是要 render function )
这两个都用 forwardRef

Card 这个不做一层元素,直接用 Card 元素,所以只好这样搞
写法是不一样

Table 那边不改了。它能跑没事就算
这边是因为布局CSS问题才这样搞

@CodFrm CodFrm merged commit bc25491 into scriptscat:release/v1.3 Feb 18, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P1 🔥 重要但是不紧急的内容

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants