You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

487 lines
20 KiB

4 days ago
(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);