(function (global) { 'use strict'; var WorkbenchLayout = global.PWLayout; if (!WorkbenchLayout) { console.error('[调试工作台] layout-dock 需先加载 layout-core'); return; } WorkbenchLayout.prototype._beginDrag = function (panelId, ev) { if (this.mode === 'compact') { ev.preventDefault(); return; } this._dragPanel = panelId; this._hoverGroupCols = null; this._setGameIframePassThrough(true); ev.dataTransfer.setData('text/pw-panel', panelId); ev.dataTransfer.effectAllowed = 'move'; if (this.dropOverlay) this.dropOverlay.hidden = true; }; WorkbenchLayout.prototype._onDocDragEnd = function () { var self = this; this._setGameIframePassThrough(false); // drop 常在 dragend 之后触发,延迟清理避免丢掉落点信息 setTimeout(function () { self._dragPanel = null; self._hoverTargetId = null; self._hoverEdge = null; self._hoverGroup = null; self._hoverGroupCols = null; if (self.dropOverlay) { self.dropOverlay.hidden = true; self.dropOverlay.className = 'pw-drop-overlay'; } }, 0); }; WorkbenchLayout.prototype._clearDragVisual = function () { if (this.dropOverlay) { this.dropOverlay.hidden = true; this.dropOverlay.className = 'pw-drop-overlay'; } }; WorkbenchLayout.prototype._onDocDragOver = function (ev) { if (!this._dragPanel) return; // 按指针坐标几何命中(iframe/浮层下 ev.target 不可靠) var info = this._findLeafAtClientPoint(ev.clientX, ev.clientY); if (!info || !info.el) { if (this.dropOverlay) this.dropOverlay.hidden = true; this._hoverTargetId = null; this._hoverEdge = null; this._hoverGroup = null; this._hoverGroupCols = null; return; } ev.preventDefault(); this._updateDockHoverFromPoint(ev.clientX, ev.clientY, this._dragPanel, info, ev.target); }; /** * 按客户区坐标找最深(面积最小)的 leaf;不依赖 elementFromPoint(游戏 iframe 会挡住)。 * @param {number} cx * @param {number} cy * @returns {{el:HTMLElement, node:object}|null} */ WorkbenchLayout.prototype._findLeafAtClientPoint = function (cx, cy) { var best = null; var bestArea = Infinity; for (var id in this._leafEls) { if (!Object.prototype.hasOwnProperty.call(this._leafEls, id)) continue; var info = this._leafEls[id]; if (!info || !info.el || !info.node) continue; var r = info.el.getBoundingClientRect(); if (cx < r.left || cx > r.right || cy < r.top || cy > r.bottom) continue; var area = Math.max(1, r.width) * Math.max(1, r.height); if (area < bestArea) { bestArea = area; best = info; } } return best; }; /** * 根据指针位置更新停靠悬停状态与蓝框。 * @param {number} cx * @param {number} cy * @param {string} panelId * @param {{el:HTMLElement, node:object}|null} info * @param {EventTarget=} hitTarget */ WorkbenchLayout.prototype._updateDockHoverFromPoint = function (cx, cy, panelId, info, hitTarget) { if (!info || !info.el || !info.node) { this._clearDragVisual(); this._hoverTargetId = null; this._hoverEdge = null; this._hoverGroup = null; this._hoverGroupCols = null; return; } var leafEl = info.el; var rect = leafEl.getBoundingClientRect(); var x = (cx - rect.left) / Math.max(1, rect.width); var y = (cy - rect.top) / Math.max(1, rect.height); var edge = this._hitEdge(info.node, x, y, hitTarget || null); // 仅工作区左右外缘才整树新建列(勿把任意 leaf 的左右边误当成整树) var ws = this.workspace ? this.workspace.getBoundingClientRect() : null; if (ws && ws.width > 40) { var margin = Math.min(48, ws.width * 0.08); var atRight = cx >= ws.right - margin; var atLeft = cx <= ws.left + margin; if (atRight || atLeft) { var rootEdge = this._resolveWorkspaceSideDrop(atRight ? 'right' : 'left'); if (rootEdge) { this._hoverTargetId = '__workspace__'; this._hoverEdge = rootEdge.edge; this._hoverGroup = null; this._hoverGroupCols = null; this._paintDockHover(rootEdge, leafEl); return; } } } var resolved = this._resolveDropTarget(info.node, edge); this._hoverTargetId = resolved.node ? resolved.node.id : null; this._hoverEdge = resolved.edge; this._hoverGroup = resolved.group || null; this._hoverGroupCols = (resolved.group && resolved.group.length) ? this._snapshotGroupColumns(resolved.group, panelId) : null; this._paintDockHover(resolved, leafEl); }; /** * 工作区左右外缘 → 相对整棵树新建列。 * @param {string} edge * @returns {{node:object, edge:string, group:null}|null} */ WorkbenchLayout.prototype._resolveWorkspaceSideDrop = function (edge) { if (!this._tree) return null; this._ensureId(this._tree); // node 仅用于绘制蓝框;真正落点用 __workspace__ 哨兵,避免 cleanup 丢 id 后找不到 return { node: this._tree, edge: edge, group: null }; }; /** * 绘制停靠蓝框。 * @param {{node:object|null, edge:string, group:Array|null}} resolved * @param {HTMLElement} fallbackEl */ WorkbenchLayout.prototype._paintDockHover = function (resolved, fallbackEl) { if (resolved.group && resolved.group.length) { this._showOverlayRect(this._edgeStripRect(this._unionNodeRects(resolved.group), resolved.edge), 'region', false); return; } if (resolved.node && resolved.node.type === 'split' && resolved.edge !== 'center') { var splitInfo = this._nodeEls[resolved.node.id]; if (splitInfo && splitInfo.el) { var sr = splitInfo.el.getBoundingClientRect(); var rootRect = this.root.getBoundingClientRect(); this._showOverlayRect(this._edgeStripRect({ left: sr.left - rootRect.left, top: sr.top - rootRect.top, width: sr.width, height: sr.height, }, resolved.edge), 'region', false); return; } } var targetInfo = resolved.node ? this._nodeEls[resolved.node.id] : null; var targetEl = targetInfo && targetInfo.el ? targetInfo.el : fallbackEl; if (!targetEl && resolved.node === this._tree && this.workspace) { targetEl = this.workspace; } if (!targetEl) { this._clearDragVisual(); return; } if (resolved.node === this._tree && (resolved.edge === 'left' || resolved.edge === 'right')) { var wr = this.workspace.getBoundingClientRect(); var rr = this.root.getBoundingClientRect(); this._showOverlayRect(this._edgeStripRect({ left: wr.left - rr.left, top: wr.top - rr.top, width: wr.width, height: wr.height, }, resolved.edge), 'region', false); return; } this._showOverlay(targetEl, resolved.edge, resolved.node && resolved.node.type === 'game'); }; /** 取目标矩形某一侧的新建区预览条带 */ WorkbenchLayout.prototype._edgeStripRect = function (rect, edge) { var r = { left: rect.left, top: rect.top, width: rect.width, height: rect.height }; var ratio = 0.36; if (edge === 'bottom') { r.top = rect.top + rect.height * (1 - ratio); r.height = rect.height * ratio; } else if (edge === 'top') { r.height = rect.height * ratio; } else if (edge === 'left') { r.width = rect.width * ratio; } else if (edge === 'right') { r.left = rect.left + rect.width * (1 - ratio); r.width = rect.width * ratio; } return r; }; WorkbenchLayout.prototype._isGameNode = function (node) { return !!(node && node.type === 'game'); }; WorkbenchLayout.prototype._splitHasGame = function (split) { if (!split || split.type !== 'split') return false; for (var i = 0; i < split.children.length; i++) { if (this._isGameNode(split.children[i])) return true; if (split.children[i].type === 'split' && this._splitHasGame(split.children[i])) return true; } return false; }; /** * 取横/竖排中与 leaf 相邻的连续「非游戏」兄弟,用于跨多列/多行建区且不吞掉游戏区。 */ WorkbenchLayout.prototype._contiguousNonGameSiblings = function (parent, leafIndex) { var kids = parent.children || []; var start = leafIndex; var end = leafIndex; while (start > 0 && !this._isGameNode(kids[start - 1])) start--; while (end < kids.length - 1 && !this._isGameNode(kids[end + 1])) end++; var group = []; for (var i = start; i <= end; i++) group.push(kids[i]); return { start: start, end: end, nodes: group }; }; WorkbenchLayout.prototype._unionNodeRects = function (nodes) { var rootRect = this.root.getBoundingClientRect(); var left = Infinity; var top = Infinity; var right = -Infinity; var bottom = -Infinity; for (var i = 0; i < nodes.length; i++) { this._ensureId(nodes[i]); var info = this._nodeEls[nodes[i].id]; if (!info || !info.el) continue; var r = info.el.getBoundingClientRect(); left = Math.min(left, r.left); top = Math.min(top, r.top); right = Math.max(right, r.right); bottom = Math.max(bottom, r.bottom); } if (!isFinite(left)) { return { left: 0, top: 0, width: 0, height: 0 }; } return { left: left - rootRect.left, top: top - rootRect.top, width: right - left, height: bottom - top, }; }; /** * 边沿停靠解析: * - 标题中心 → 合并页签 * - 横排含游戏区时的上/下 → 只包住连续非游戏兄弟(红框范围),不横跨游戏 * - 竖排含游戏区时的左/右 → 同上 * - 不含游戏的横排上/下 → 提到该横排父级整行 */ WorkbenchLayout.prototype._resolveDropTarget = function (leafNode, edge) { if (edge === 'center') return { node: leafNode, edge: edge, group: null }; var parentInfo = this._findParent(this._tree, leafNode); if (!parentInfo || !parentInfo.parent || parentInfo.parent.type !== 'split') { this._ensureId(leafNode); return { node: leafNode, edge: edge, group: null }; } var parent = parentInfo.parent; var index = parentInfo.index; // 横排 + 上/下:若同排有游戏,只圈选连续非游戏列 if ((edge === 'top' || edge === 'bottom') && parent.dir === 'h') { if (this._splitHasGame(parent) || this._parentRowHasGameSibling(parent)) { var g = this._contiguousNonGameSiblings(parent, index); if (g.nodes.length >= 1) { return { node: null, edge: edge, group: g.nodes, groupParent: parent, groupStart: g.start, groupEnd: g.end }; } } this._ensureId(parent); return { node: parent, edge: edge, group: null }; } // 竖排 + 左/右:若同列有游戏,只圈选连续非游戏行 if ((edge === 'left' || edge === 'right') && parent.dir === 'v') { if (this._splitHasGame(parent) || this._parentRowHasGameSibling(parent)) { var g2 = this._contiguousNonGameSiblings(parent, index); if (g2.nodes.length >= 1) { return { node: null, edge: edge, group: g2.nodes, groupParent: parent, groupStart: g2.start, groupEnd: g2.end }; } } this._ensureId(parent); return { node: parent, edge: edge, group: null }; } // 外侧边:提升到父级(仍避开把游戏一并包进「整行上下」) if (parent.dir === 'h' && edge === 'left' && index === 0) { this._ensureId(parent); return { node: parent, edge: edge, group: null }; } if (parent.dir === 'h' && edge === 'right' && index === parent.children.length - 1) { this._ensureId(parent); return { node: parent, edge: edge, group: null }; } if (parent.dir === 'v' && edge === 'top' && index === 0) { this._ensureId(parent); return { node: parent, edge: edge, group: null }; } if (parent.dir === 'v' && edge === 'bottom' && index === parent.children.length - 1) { this._ensureId(parent); return { node: parent, edge: edge, group: null }; } this._ensureId(leafNode); return { node: leafNode, edge: edge, group: null }; }; WorkbenchLayout.prototype._parentRowHasGameSibling = function (split) { // 父级自身子节点含 game for (var i = 0; i < split.children.length; i++) { if (this._isGameNode(split.children[i])) return true; } return false; }; WorkbenchLayout.prototype._hitEdge = function (node, x, y, target) { // 标题栏优先:合并 if (target && target.closest && target.closest('.pw-leaf-tabbar, .pw-leaf-tab')) { return 'center'; } var m = 0.22; var onLeft = x < m; var onRight = x > 1 - m; var onTop = y < m; var onBottom = y > 1 - m; // 角落按更近边 if (onLeft && onTop) return x < y ? 'left' : 'top'; if (onRight && onTop) return (1 - x) < y ? 'right' : 'top'; if (onLeft && onBottom) return x < (1 - y) ? 'left' : 'bottom'; if (onRight && onBottom) return (1 - x) < (1 - y) ? 'right' : 'bottom'; if (onLeft) return 'left'; if (onRight) return 'right'; if (onTop) return 'top'; if (onBottom) return 'bottom'; // 游戏区域中心不允许合并页签,默认下方提示 if (node.type === 'game') return 'bottom'; return 'center'; }; WorkbenchLayout.prototype._showOverlay = function (leafEl, edge, isGame) { var rect = leafEl.getBoundingClientRect(); var rootRect = this.root.getBoundingClientRect(); this._showOverlayRect({ left: rect.left - rootRect.left, top: rect.top - rootRect.top, width: rect.width, height: rect.height, }, edge, isGame); }; WorkbenchLayout.prototype._showOverlayRect = function (rect, edge, isGame) { var overlay = this.dropOverlay; if (!overlay) return; overlay.hidden = false; overlay.style.left = rect.left + 'px'; overlay.style.top = rect.top + 'px'; overlay.style.width = Math.max(0, rect.width) + 'px'; overlay.style.height = Math.max(0, rect.height) + 'px'; var isRegion = edge === 'region'; overlay.className = 'pw-drop-overlay edge-' + (isRegion ? 'center' : edge) + (isGame ? ' is-game' : '') + (isRegion ? ' is-region' : ''); var center = overlay.querySelector('.pw-drop-e-center'); if (center) { center.hidden = !!isGame; center.textContent = isRegion ? '在此新建区域' : '合并到此区域'; } }; WorkbenchLayout.prototype._onDocDrop = function (ev) { var panelId = ev.dataTransfer.getData('text/pw-panel') || this._dragPanel; var targetId = this._hoverTargetId; var edge = this._hoverEdge; var group = this._hoverGroup; var groupCols = this._hoverGroupCols; this._clearDragVisual(); this._dragPanel = null; this._hoverTargetId = null; this._hoverEdge = null; this._hoverGroup = null; this._hoverGroupCols = null; if (!panelId || !edge) return; if (!targetId && !(groupCols && groupCols.length) && !(group && group.length)) return; ev.preventDefault(); this._applyDrop(panelId, targetId, edge, group, groupCols); }; WorkbenchLayout.prototype._applyDrop = function (panelId, targetId, edge, group, groupCols) { // 组停靠:用「列内容快照」而非节点引用,避免 remove/cleanup 后 id 失效导致只包一列 if (edge !== 'center' && ((groupCols && groupCols.length) || (group && group.length))) { this._closeFloat(panelId, false); var cols = (groupCols && groupCols.length) ? groupCols : this._snapshotGroupColumns(group, panelId); this._removePanel(panelId); var nodes = this._resolveColumnsFromSnapshot(cols); if (nodes.length >= 1) { this._splitBesideGroup(nodes, edge, panelId); } else { this._openFloat(panelId, null); } this._dirty = true; this._tree = this._cleanup(this._tree) || this._presetTree('minimal'); this.rebuild(); this._rememberPanelFromTree(panelId); this._save(); return; } // 整树左右外侧:直接相对当前根拆分,不依赖可能被 cleanup 丢掉的根 id if (targetId === '__workspace__' && (edge === 'left' || edge === 'right')) { this._closeFloat(panelId, false); this._removePanel(panelId); this._splitBeside(this._tree, edge, panelId); this._dirty = true; this._tree = this._cleanup(this._tree) || this._presetTree('minimal'); this.rebuild(); this._rememberPanelFromTree(panelId); this._save(); return; } var target = this._findNodeById(this._tree, targetId); if (!target) { console.warn('[调试工作台] 停靠失败:找不到目标', targetId, edge); return; } if (target.type === 'tabs' && (target.panels || []).indexOf(panelId) >= 0 && edge === 'center') { this._closeFloat(panelId, false); target.active = panelId; this.rebuild(); this._save(); return; } this._closeFloat(panelId, false); this._removePanel(panelId); target = this._findNodeById(this._tree, targetId); if (!target) { // cleanup 可能重建了 split 节点;若原目标就是根,回退整树停靠 if (edge === 'left' || edge === 'right') { this._splitBeside(this._tree, edge, panelId); } else { this._openFloat(panelId, null); this._dirty = true; this._save(); return; } } else if (edge === 'center') { if (target.type === 'game' || target.type === 'split') { this._splitBeside(target, 'bottom', panelId); } else if (target.type === 'tabs') { if (target.panels.indexOf(panelId) < 0) target.panels.push(panelId); target.active = panelId; } } else { this._splitBeside(target, edge, panelId); } this._dirty = true; this._tree = this._cleanup(this._tree) || this._presetTree('minimal'); this.rebuild(); this._rememberPanelFromTree(panelId); this._save(); }; })(window);