feat(节点交互): 实现联动拖拽功能并优化节点聚焦体验

- 新增联动拖拽功能,支持配置开关,拖动父节点时自动移动子节点
- 优化节点聚焦交互,聚焦时放大节点并居中视图
- 重构后代节点查找逻辑,改用迭代方式提高性能
- 在顶部导航栏添加联动拖拽开关按钮
- 为i18n添加相关翻译字段
- 调整markdown渲染样式增加内边距
This commit is contained in:
liuziting
2026-01-21 23:01:56 +08:00
parent 1b9deed3a7
commit a7f8138b1a
7 changed files with 290 additions and 41 deletions

View File

@@ -107,6 +107,7 @@ export function useThinkFlow({ t, locale }: { t: Translate; locale: Ref<string>
removeEdges,
fitView,
onNodeDragStart,
onNodeDrag,
onNodeDragStop
} = useVueFlow()
@@ -122,6 +123,7 @@ export function useThinkFlow({ t, locale }: { t: Translate; locale: Ref<string>
const summaryContent = ref('')
const draggingNodeId = ref<string | null>(null)
const dragLastPositionByNodeId = new Map<string, { x: number; y: number }>()
/**
* 交互:按住 Space 启用“抓手拖拽画布”
@@ -149,10 +151,79 @@ export function useThinkFlow({ t, locale }: { t: Translate; locale: Ref<string>
*/
onNodeDragStart(e => {
draggingNodeId.value = e.node.id
dragLastPositionByNodeId.set(e.node.id, {
x: e.node.position.x,
y: e.node.position.y
})
})
onNodeDragStop(() => {
/**
* 处理节点拖拽事件 (联动移动子节点)
*/
const handleNodeDrag = (payload: any) => {
if (!config.hierarchicalDragging) return
const node = payload?.node ?? payload
if (!node?.id || !node?.position) return
const lastPosition = dragLastPositionByNodeId.get(node.id)
if (!lastPosition) {
const fallbackDelta = payload?.delta
dragLastPositionByNodeId.set(node.id, { x: node.position.x, y: node.position.y })
if (fallbackDelta && typeof fallbackDelta.x === 'number' && typeof fallbackDelta.y === 'number') {
const descendantIds = getDescendantIds(node.id)
if (descendantIds.size === 0) return
const selectedNodeIds = new Set(flowNodes.value.filter(n => n.selected).map(n => n.id))
descendantIds.forEach(id => {
if (!selectedNodeIds.has(id)) {
const targetNode = flowNodes.value.find(n => n.id === id)
if (targetNode) {
targetNode.position = {
x: targetNode.position.x + fallbackDelta.x,
y: targetNode.position.y + fallbackDelta.y
}
}
}
})
}
return
}
const dx = node.position.x - lastPosition.x
const dy = node.position.y - lastPosition.y
if (dx === 0 && dy === 0) return
dragLastPositionByNodeId.set(node.id, { x: node.position.x, y: node.position.y })
const descendantIds = getDescendantIds(node.id)
if (descendantIds.size === 0) return
// 获取当前所有选中的节点,避免重复位移
const selectedNodeIds = new Set(flowNodes.value.filter(n => n.selected).map(n => n.id))
// 批量更新子节点位置
descendantIds.forEach(id => {
if (!selectedNodeIds.has(id)) {
const targetNode = flowNodes.value.find(n => n.id === id)
if (targetNode) {
// 直接更新位置对象,确保 Vue 能够检测到深层变化
targetNode.position = {
x: targetNode.position.x + dx,
y: targetNode.position.y + dy
}
}
}
})
}
onNodeDrag(handleNodeDrag)
onNodeDragStop(e => {
draggingNodeId.value = null
if (e?.node?.id) {
dragLastPositionByNodeId.delete(e.node.id)
}
})
/**
@@ -166,16 +237,25 @@ export function useThinkFlow({ t, locale }: { t: Translate; locale: Ref<string>
})
/**
* 获取节点所有后代节点 id用于激活路径计算
* 获取节点所有后代节点 ID (迭代实现,更健壮)
*/
const getDescendantIds = (nodeId: string, ids: Set<string> = new Set()): Set<string> => {
flowEdges.value.forEach(edge => {
if (edge.source === nodeId) {
ids.add(edge.target)
getDescendantIds(edge.target, ids)
const getDescendantIds = (nodeId: string): Set<string> => {
const descendants = new Set<string>()
const stack = [nodeId]
const edges = flowEdges.value
while (stack.length > 0) {
const currentId = stack.pop()!
for (const edge of edges) {
if (edge.source === currentId) {
if (!descendants.has(edge.target)) {
descendants.add(edge.target)
stack.push(edge.target)
}
}
}
})
return ids
}
return descendants
}
/**
@@ -235,7 +315,8 @@ export function useThinkFlow({ t, locale }: { t: Translate; locale: Ref<string>
edgeType: 'default',
backgroundVariant: BackgroundVariant.Lines,
showControls: true,
showMiniMap: true
showMiniMap: true,
hierarchicalDragging: true
})
const lastAppliedStatus = ref('')
@@ -763,6 +844,7 @@ export function useThinkFlow({ t, locale }: { t: Translate; locale: Ref<string>
fitView,
resetLayout,
centerRoot,
handleNodeDrag,
startNewSession,
executeReset,
generateSummary,