20260827230636

This commit is contained in:
oneao committed 2026-08-27 23:06:36 +08:00
1 parent 3824af0f12
commit 35ca496b5f
15 files changed
+2313 -321

No files matched your search

@@ -31,6 +31,10 @@ const props = defineProps({
disabled: { type: Boolean, default: false },
lazy: { type: Boolean, default: false },
draggable: { type: Boolean, default: false },
// 外部拖拽源解析器:(dataTransfer, event) => dragKey | null。
// 内部无节点拖拽(dragKey 为空)时,据此把树外拖入的源(如字段库)映射为拖拽键,
// 使树内节点可作为外部拖拽的落点。返回 null 表示不识别该外部拖拽源。
externalDragKey: { type: Function, default: null },
// 过滤关键词:非空时高亮命中节点并自动展开其祖先路径
filter: { type: String, default: '' },
blockNode: { type: Boolean, default: false },
@@ -302,6 +306,9 @@ function autoExpandParents(keys) {
const dragKey = ref(null)
const dropKey = ref(null)
const dropPosition = ref(null)
// 标记「树内节点发起的拖拽」:外部拖拽(如字段库)在 dragover 阶段会把占位键
// 写入 dragKey,不能据此误判为内部拖拽 —— 用独立标记区分子孙循环校验与清理分支。
const isInternalDrag = ref(false)
function isDescendant(parentKey, childKey) {
const stack = [...(flat.value.childrenMap.get(parentKey) || [])]
@@ -316,40 +323,63 @@ function isDescendant(parentKey, childKey) {
function onDragStart(node, e) {
const key = getNodeKey(node, fn.value)
dragKey.value = key
isInternalDrag.value = true
if (e.dataTransfer) {
e.dataTransfer.effectAllowed = 'move'
e.dataTransfer.setData('text/plain', String(key))
}
emit('dragstart', { node, event: e })
}
// 解析本次拖拽键:外部源每次重新解析(dragover 阶段 Chrome 读不到 getData(),
// 只能返回占位键;若被上阶段的占位值挡住,drop 阶段就拿不到真实字段 id 了);
// 无外部源时用内部节点拖拽键。
function resolveDragKey(e) {
if (!isInternalDrag.value && typeof props.externalDragKey === 'function') {
const key = props.externalDragKey(e?.dataTransfer, e)
if (key != null) return key
}
return dragKey.value
}
function onDragOver(node, e) {
e.preventDefault()
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move'
if (e.dataTransfer) {
// 外部源(如字段库)effectAllowed='copy' 时保持 copy,否则内部节点移动用 move
e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed === 'copy' ? 'copy' : 'move'
}
const rect = e.currentTarget.getBoundingClientRect()
dropPosition.value = calcDropPosition(e.clientY, rect)
dropKey.value = getNodeKey(node, fn.value)
// 外部拖拽源:临时写入 dragKey,让 drop 落点高亮/指示线可见
const external = resolveDragKey(e)
if (external != null && dragKey.value == null) dragKey.value = external
emit('dragover', { node, event: e })
}
function onDragLeave(node, e) {
emit('dragleave', { node, event: e })
}
// 根容器统一兜底:鼠标真正离开整棵树时清理 drop 指示(避免拖出后残留高亮/指示线)
// 根容器统一兜底:鼠标真正离开整棵树时清理 drop 指示(避免拖出后残留高亮/指示线);
// 外部拖拽未落放即拖离时同步清掉占位 dragKey,防止下次 hover 误判
function onTreeDragLeave(e) {
const root = e.currentTarget
if (root && !root.contains(e.relatedTarget)) {
dropKey.value = null
dropPosition.value = null
if (!isInternalDrag.value) dragKey.value = null
}
}
function onDrop(node, e) {
e.preventDefault()
const drag = dragKey.value
const drag = resolveDragKey(e)
const drop = getNodeKey(node, fn.value)
const wasInternal = isInternalDrag.value
// 先快照落点位置:resetDragVisual 会把 dropPosition 清为 null,
// 若在 emit 时才读取,before/inside/after 将全部丢失(永远为 null)
const pos = dropPosition.value
resetDragVisual()
if (drag == null || drag === drop) return
// 禁止拖入自身子孙,避免层级循环
if (isDescendant(drag, drop)) return
emit('drop', { dragKey: drag, dropKey: drop, dropPosition: dropPosition.value, node, event: e })
// 禁止拖入自身子孙,避免层级循环(仅树内节点拖拽需要校验)
if (wasInternal && isDescendant(drag, drop)) return
emit('drop', { dragKey: drag, dropKey: drop, dropPosition: pos, node, event: e })
}
function onDragEnd(e) {
const key = dragKey.value
@@ -360,6 +390,7 @@ function resetDragVisual() {
dragKey.value = null
dropKey.value = null
dropPosition.value = null
isInternalDrag.value = false
}
/* ---------- 键盘导航(roving tabindex) ---------- */