From c635cd8b08e9ed9ae726865be5daeb608708c1a4 Mon Sep 17 00:00:00 2001 From: humingxing <930621236@qq.com> Date: Wed, 14 Oct 2026 18:33:09 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=A2=84=E8=A7=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- preview-template/index.ejs | 1 + preview-template/workbench/css/workbench.css | 255 ++++++++- preview-template/workbench/js/agent.js | 526 ++++++++++++++++-- preview-template/workbench/js/host.js | 47 ++ preview-template/workbench/js/layout.js | 292 ++++++---- .../workbench/js/panels/console.js | 109 +++- .../workbench/js/panels/inspector.js | 127 ++++- 7 files changed, 1130 insertions(+), 227 deletions(-) diff --git a/preview-template/index.ejs b/preview-template/index.ejs index 74b4612..aa4014b 100644 --- a/preview-template/index.ejs +++ b/preview-template/index.ejs @@ -54,6 +54,7 @@
+ diff --git a/preview-template/workbench/css/workbench.css b/preview-template/workbench/css/workbench.css index 6a3e467..33e9dce 100644 --- a/preview-template/workbench/css/workbench.css +++ b/preview-template/workbench/css/workbench.css @@ -88,22 +88,27 @@ html[data-pw='shell'] .pw-shell-dummy-canvas, display: none !important; } -.pw-root.pw-workbench-off .pw-toolbar-center > :not(#pw-device-select):not(#pw-device-rotate) { +.pw-root.pw-workbench-off .pw-toolbar-center > :not(#pw-device-select):not(#pw-device-rotate):not(#pw-game-refresh) { display: none !important; } .pw-root.pw-workbench-off .pw-toolbar { - justify-content: space-between; + position: relative; + justify-content: center; } .pw-root.pw-workbench-off .pw-toolbar-center { - flex: 1 1 auto; - justify-content: flex-start; + flex: 0 0 auto; + justify-content: center; } .pw-root.pw-workbench-off .pw-toolbar-right { + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-50%); flex: 0 0 auto; - margin-left: auto; + margin: 0; } .pw-root.pw-workbench-off #pw-workspace { @@ -715,6 +720,84 @@ body { } /* —— 控制台(对齐 Chrome DevTools Console) —— */ +.pw-console-head { + flex-wrap: wrap; + gap: 6px; +} + +.pw-console-tools { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 6px; + min-width: 0; +} + +.pw-console-levels { + display: inline-flex; + align-items: center; + gap: 2px; +} + +.pw-console-chip { + display: inline-flex; + align-items: center; + gap: 4px; + height: 22px; + padding: 0 8px; + border: 1px solid transparent; + border-radius: 3px; + background: transparent; + color: #c7c7c7; + font-size: 11px; + line-height: 1; + cursor: pointer; +} + +.pw-console-chip:hover { + background: #3c4043; + color: #e8eaed; +} + +.pw-console-chip.active { + background: #3c4043; + color: #fff; + border-color: #5f6368; +} + +.pw-console-chip[data-level="error"].active { + background: #4b1113; + border-color: #8a1a1f; + color: #ffc7c7; +} + +.pw-console-chip[data-level="warn"].active { + background: #332b00; + border-color: #634b00; + color: #fdd663; +} + +.pw-console-chip-n { + min-width: 14px; + padding: 0 4px; + border-radius: 7px; + background: #202124; + color: #9aa0a6; + font-size: 10px; + line-height: 14px; + text-align: center; +} + +.pw-console-chip.active .pw-console-chip-n { + background: rgba(0, 0, 0, 0.35); + color: inherit; +} + +.pw-console-filter { + width: 120px; + max-width: 40vw; +} + .pw-console-list, .pw-console-list * { font-family: "Consolas", "Courier New", Menlo, Monaco, monospace !important; @@ -727,6 +810,19 @@ body { line-height: 1.4 !important; color: #e8eaed; padding: 0; + user-select: text; + -webkit-user-select: text; +} + +.pw-console-list .pw-console-msg, +.pw-console-list .pw-console-stack, +.pw-console-list .pw-v-fmt, +.pw-console-list .pw-v-text, +.pw-console-list .pw-v-string, +.pw-console-list .pw-v-error { + user-select: text; + -webkit-user-select: text; + cursor: text; } .pw-console-empty { @@ -804,11 +900,47 @@ body { color: inherit; } +.pw-console-src { + flex: 0 0 auto; + max-width: 36%; + margin-left: auto; + padding-left: 8px; + color: #9aa0a6; + font-size: 11px !important; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-align: right; + user-select: text; + -webkit-user-select: text; +} + +.pw-console-row:hover .pw-console-src { + color: #8ab4f8; +} + +.pw-console-src-empty { + min-width: 0; + padding: 0; +} + +.pw-console-stack-spacer { + flex: 0 0 10px; + width: 10px; +} + +.pw-console-stack-toggle { + flex: 0 0 10px; + margin-top: 2px; +} + .pw-console-toggle { cursor: pointer; display: inline-flex; align-items: flex-start; gap: 3px; + user-select: none; + -webkit-user-select: none; } .pw-console-caret { @@ -843,8 +975,10 @@ body { word-break: break-word; } +/* %c 内联样式优先;无样式时跟控制台默认前景色 */ .pw-v-fmt { color: inherit; } .pw-v-text { color: #e8eaed; } +/* 仅对象属性展开时的字符串值(带引号),消息参数用 .pw-v-text */ .pw-v-string { color: #f28b82; } .pw-v-number { color: #a8c7fa; } .pw-v-boolean { color: #c58af9; } @@ -864,39 +998,42 @@ body { padding: 4px 6px; background: #2a2a2a; border-bottom: 1px solid var(--pw-border); - cursor: move; + cursor: grab; flex-shrink: 0; + user-select: none; + touch-action: none; +} + +.pw-float-head:active { + cursor: grabbing; } .pw-float-title { color: #fff; font-weight: 600; - cursor: grab; - user-select: none; padding: 2px 4px; border-radius: 3px; -} - -.pw-float-title:hover { - background: #3a3a3a; -} - -.pw-float-title:active { - cursor: grabbing; + pointer-events: none; } .pw-float-hint { color: var(--pw-muted); font-size: 10px; margin-left: auto; + pointer-events: none; } .pw-float-close { flex-shrink: 0; + cursor: pointer; +} + +.pw-float-dock-dragging, +.pw-float-dragging { + opacity: 0.85; } -.pw-float-dock-dragging { - opacity: 0.4; +.pw-float-dragging { pointer-events: none !important; } @@ -906,6 +1043,8 @@ body { overflow: hidden; display: flex; flex-direction: column; + cursor: default; + touch-action: auto; } .pw-float-body .pw-panel-root, @@ -1005,21 +1144,24 @@ body { .pw-insp-section.open .pw-insp-sec-body { display: block; } .pw-insp-row { - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 6px; + display: grid; + grid-template-columns: minmax(96px, 0.95fr) minmax(110px, 1.25fr); + align-items: start; + column-gap: 10px; + margin-bottom: 7px; min-height: 22px; } .pw-insp-label { - flex: 0 0 52px; - color: #9d9d9d; + color: #b0b0b0; font-size: 11px; - text-align: right; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + line-height: 1.35; + text-align: left; + padding-top: 3px; + word-break: break-word; + overflow-wrap: anywhere; + hyphens: auto; + user-select: text; } .pw-insp-input { @@ -1090,6 +1232,7 @@ body { position: relative; width: 28px; height: 16px; + margin-top: 3px; cursor: pointer; } @@ -1137,6 +1280,62 @@ body { padding: 4px 0; } +.pw-insp-select { + cursor: pointer; +} + +.pw-insp-ref { + flex: 1; + min-width: 0; + display: flex; + align-items: center; + gap: 6px; + height: 22px; + padding: 0 6px; + border: 1px solid #2f5d50; + border-radius: 2px; + background: #1a2e28; + color: #9cdcfe; + overflow: hidden; +} + +.pw-insp-ref-kind { + flex: 0 0 auto; + color: #4ec9b0; + font-size: 10px; +} + +.pw-insp-ref-name { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.pw-insp-color { + flex: 1; + min-width: 0; + display: flex; + align-items: center; + gap: 6px; +} + +.pw-insp-color-picker { + width: 28px; + height: 22px; + padding: 0; + border: 1px solid #3c3c3c; + border-radius: 2px; + background: transparent; + cursor: pointer; +} + +.pw-insp-color-hex { + flex: 1; + min-width: 0; +} + /* —— 可拖拽停靠 —— */ .pw-dock { display: flex; diff --git a/preview-template/workbench/js/agent.js b/preview-template/workbench/js/agent.js index 8fbe3e2..dc62406 100644 --- a/preview-template/workbench/js/agent.js +++ b/preview-template/workbench/js/agent.js @@ -25,11 +25,16 @@ this._overlay = null; this._consoleOrig = null; this._consoleBuf = []; + this._consoleHist = []; this._consoleFlushTimer = 0; this._consoleObjs = {}; this._consoleObjSeq = 1; + this._consoleErrBound = false; + this._consolePost = false; this._hierSnap = null; this._boundMessage = this._onMessage.bind(this); + this._boundConsoleError = this._onConsoleError.bind(this); + this._boundConsoleRejection = this._onConsoleRejection.bind(this); this._sampler = null; this._markers = null; this._device = null; @@ -44,6 +49,8 @@ global.PWDevicePreview.hideOfficialToolbar(); } window.addEventListener('message', this._boundMessage); + // 尽早静默缓冲,订阅控制台后再推送到壳(避免打开面板前日志丢失) + this._hookConsole(true); this._ensureOverlay(); this._waitForEngine(function (cc) { self.cc = cc; @@ -178,13 +185,17 @@ WorkbenchAgent.prototype.hibernate = function () { this._hibernating = true; + this._consolePost = false; for (var i = 0; i < CHANNELS.length; i++) this._onChannelOff(CHANNELS[i]); + this._hookConsole(false); + this._consoleHist.length = 0; this._post({ type: 'event', name: 'agent.hibernate', data: { on: true } }); return true; }; WorkbenchAgent.prototype.wake = function () { this._hibernating = false; + this._hookConsole(true); for (var i = 0; i < CHANNELS.length; i++) { if (this._subs[CHANNELS[i]] > 0) this._onChannelOn(CHANNELS[i]); } @@ -217,6 +228,8 @@ }, ASSETS_DEFAULT_MS); } else if (ch === 'console') { this._hookConsole(true); + this._consolePost = true; + this._flushConsoleHist(); } else if (ch === 'pick') { this._bindCanvasPick(true); this._clearTimer('pickRect'); @@ -239,7 +252,13 @@ } else if (ch === 'assets') { this._clearTimer('assets'); } else if (ch === 'console') { - this._hookConsole(false); + // 保持静默缓冲,仅停止推送;hibernate 时才真正卸 hook + this._consolePost = false; + if (this._consoleFlushTimer) { + clearTimeout(this._consoleFlushTimer); + this._consoleFlushTimer = 0; + } + this._consoleBuf.length = 0; } else if (ch === 'pick') { this._bindCanvasPick(false); this._clearTimer('pickRect'); @@ -357,6 +376,170 @@ return { uuid: this._selectedUuid, ok: !!node }; }; + /** 组件内部属性,不在 Inspector 展示 */ + var COMP_PROP_SKIP = { + _name: 1, _objFlags: 1, __scriptAsset: 1, node: 1, _enabled: 1, + __prefab: 1, __editorExtras__: 1, uuid: 1, name: 1, enabled: 1, + }; + + /** + * 读取 CCClass 属性元数据(displayName / type / enumList / visible)。 + * @param {Function} ctor + * @returns {object} + */ + function getCtorAttrs(ctor) { + if (!ctor) return {}; + if (ctor.__attrs__ && typeof ctor.__attrs__ === 'object') return ctor.__attrs__; + return {}; + } + + /** + * @param {object} attrs + * @param {string} key + * @param {string} name + * @returns {*} + */ + function attrOf(attrs, key, name) { + return attrs[key + '$_$' + name]; + } + + /** + * 列出组件应展示的属性名(对齐编辑器:走 __props__,隐藏 _ 开头与 visible:false)。 + * @param {Function} ctor + * @returns {string[]} + */ + function listCompPropKeys(ctor) { + var attrs = getCtorAttrs(ctor); + var raw = (ctor && (ctor.__props__ || ctor.__values__)) || []; + var out = []; + for (var i = 0; i < raw.length; i++) { + var key = raw[i]; + if (!key || COMP_PROP_SKIP[key]) continue; + if (key.charAt(0) === '_' || key.indexOf('__') === 0) continue; + if (attrOf(attrs, key, 'visible') === false) continue; + out.push(key); + } + return out; + } + + /** + * 将运行时值序列化为 Inspector DTO。 + * @param {*} val + * @param {object} attrs + * @param {string} key + * @param {object} cc + * @returns {{type:string, value:*, enumOptions?:Array, readOnly?:boolean}} + */ + function serializeFieldValue(val, attrs, key, cc) { + var enumList = attrOf(attrs, key, 'enumList'); + var typeTag = attrOf(attrs, key, 'type'); + if (enumList && enumList.length) { + return { + type: 'enum', + value: val, + enumOptions: enumList.map(function (it) { + return { name: String(it.name), value: it.value }; + }), + }; + } + if (typeTag === 'Enum') { + return { type: 'number', value: typeof val === 'number' ? val : Number(val) || 0 }; + } + var t = typeof val; + if (t === 'boolean' || t === 'string') return { type: t, value: val }; + if (t === 'number') return { type: 'number', value: isFinite(val) ? val : 0 }; + if (val == null) { + var ctorHint = attrOf(attrs, key, 'ctor'); + var hintName = ctorHint && (ctorHint.name || ctorHint.prototype && ctorHint.prototype.__classname__); + if (hintName || typeTag === 'Object') { + return { + type: 'ref', + readOnly: true, + value: { kind: hintName || 'Object', name: '(空)', uuid: '' }, + }; + } + return { type: 'string', value: '' }; + } + if (cc && cc.Color && val instanceof cc.Color) { + return { + type: 'color', + value: { r: val.r | 0, g: val.g | 0, b: val.b | 0, a: val.a != null ? (val.a | 0) : 255 }, + }; + } + // Size + if (typeof val.width === 'number' && typeof val.height === 'number' && val.x === undefined) { + return { type: 'size', value: { width: val.width, height: val.height } }; + } + // Vec2 / Vec3 + if (typeof val.x === 'number' && typeof val.y === 'number') { + if (typeof val.z === 'number') { + return { type: 'vec3', value: { x: val.x, y: val.y, z: val.z } }; + } + return { type: 'vec2', value: { x: val.x, y: val.y } }; + } + // Asset / Node / Component 引用 + if (typeof val === 'object') { + var kind = (val.constructor && val.constructor.name) || 'Object'; + var name = ''; + if (val.name != null) name = String(val.name); + else if (val.node && val.node.name) name = String(val.node.name); + var uuid = val._uuid || val.uuid || ''; + return { + type: 'ref', + readOnly: true, + value: { kind: kind, name: name || kind, uuid: uuid ? String(uuid) : '' }, + }; + } + return { type: 'string', value: String(val) }; + } + + /** + * 提取单个组件的 Inspector 字段列表。 + * @param {object} comp + * @param {number} index + * @param {object} cc + * @returns {{name:string, index:number, fields:Array}} + */ + function extractCompFields(comp, index, cc) { + var ctor = comp.constructor; + var cname = (cc && cc.js && typeof cc.js.getClassName === 'function') + ? (cc.js.getClassName(ctor) || ctor.name) + : (ctor && ctor.name) || 'Component'; + var attrs = getCtorAttrs(ctor); + var keys = listCompPropKeys(ctor); + // 无元数据时有限回退 + if (!keys.length) { + var own = Object.keys(comp); + for (var o = 0; o < own.length && keys.length < 24; o++) { + var ok = own[o]; + if (!ok || ok.charAt(0) === '_' || COMP_PROP_SKIP[ok]) continue; + var ot = typeof comp[ok]; + if (ot === 'number' || ot === 'boolean' || ot === 'string') keys.push(ok); + } + if (!keys.length) { + console.warn('[Agent] 组件无 CCClass 属性元数据', cname); + } + } + var fields = []; + var max = 80; + for (var i = 0; i < keys.length && fields.length < max; i++) { + var key = keys[i]; + var label = attrOf(attrs, key, 'displayName') || key; + var raw; + try { raw = comp[key]; } catch (e) { continue; } + var ser = serializeFieldValue(raw, attrs, key, cc); + fields.push({ + key: key, + label: label, + type: ser.type, + value: ser.value, + enumOptions: ser.enumOptions || null, + readOnly: !!ser.readOnly, + }); + } + return { name: cname, index: index, fields: fields }; + } + WorkbenchAgent.prototype._getNodeProps = function (uuid) { var node = this._findNode(uuid); if (!node) return null; @@ -364,28 +547,12 @@ var p = node.getPosition ? node.getPosition() : node.position; var e = node.eulerAngles || { x: 0, y: 0, z: 0 }; var s = node.getScale ? node.getScale() : node.scale; - var uit = node.getComponent && cc ? node.getComponent(cc.UITransform) : null; var comps = []; if (node.components) { for (var i = 0; i < node.components.length; i++) { var comp = node.components[i]; if (!comp || !comp.constructor) continue; - var cname = comp.constructor.name; - if (cname === 'UITransform') continue; - var fields = []; - var keys = Object.keys(comp); - var shown = 0; - for (var k = 0; k < keys.length && shown < 24; k++) { - var key = keys[k]; - if (!key || key.charAt(0) === '_' || key === 'node' || key === 'uuid') continue; - var val = comp[key]; - var t = typeof val; - if (t === 'number' || t === 'boolean' || t === 'string') { - fields.push({ key: key, type: t, value: val }); - shown++; - } - } - comps.push({ name: cname, index: i, fields: fields }); + comps.push(extractCompFields(comp, i, cc)); } } return { @@ -396,10 +563,6 @@ pos: { x: p.x, y: p.y, z: p.z }, euler: { x: e.x, y: e.y, z: e.z }, scale: { x: s.x, y: s.y, z: s.z }, - ui: uit ? { - width: uit.width, height: uit.height, - anchorX: uit.anchorX, anchorY: uit.anchorY, - } : null, comps: comps, }; }; @@ -408,7 +571,6 @@ var node = this._findNode(uuid); if (!node || !fid) return false; var cc = this.cc; - var uit = node.getComponent && cc ? node.getComponent(cc.UITransform) : null; try { if (fid === 'name') node.name = String(value); else if (fid === 'active') node.active = !!value; @@ -434,24 +596,132 @@ var sz = fid === 'sz' ? Number(value) : sc.z; if (typeof node.setScale === 'function') node.setScale(sx, sy, sz); else if (cc && cc.Vec3) node.scale = new cc.Vec3(sx, sy, sz); - } else if (uit && (fid === 'uiw' || fid === 'uih' || fid === 'uax' || fid === 'uay')) { - if (fid === 'uiw') uit.width = Number(value); - if (fid === 'uih') uit.height = Number(value); - if (fid === 'uax') uit.anchorX = Number(value); - if (fid === 'uay') uit.anchorY = Number(value); } else if (fid.indexOf('c') === 0 && fid.indexOf('_') > 0) { - var parts = fid.split('_'); - var ci = parseInt(parts[0].slice(1), 10); - var key = parts.slice(1).join('_'); - if (node.components && node.components[ci]) node.components[ci][key] = value; + return this._setCompProp(node, fid, value); + } else { + return false; } return true; - } catch (e) { - console.warn('[Agent] set prop', fid, e); + } catch (err) { + console.warn('[Agent] set prop', fid, err); + return false; + } + }; + + /** + * 写回组件字段。fid: c{index}_{key}[_axis] + * @param {object} node + * @param {string} fid + * @param {*} value + * @returns {boolean} + */ + WorkbenchAgent.prototype._setCompProp = function (node, fid, value) { + var m = /^c(\d+)_(.+)$/.exec(fid); + if (!m) return false; + var ci = parseInt(m[1], 10); + var rest = m[2]; + var comp = node.components && node.components[ci]; + if (!comp) return false; + var cc = this.cc; + var axis = ''; + var key = rest; + var axisMatch = /^(.*)_(x|y|z|w|h|r|g|b|a|hex)$/.exec(rest); + if (axisMatch) { + // 优先整键命中(字段名本身含下划线时仍以整键为准) + if (!(rest in comp) && (axisMatch[1] in comp || hasCompProp(comp, axisMatch[1]))) { + key = axisMatch[1]; + axis = axisMatch[2]; + } + } + if (axis) { + return setCompAxisValue(comp, key, axis, value, cc); + } + var attrs = getCtorAttrs(comp.constructor); + if (attrOf(attrs, key, 'type') === 'Object' || attrOf(attrs, key, 'ctor')) { + // 引用只读 + return false; + } + var cur; + try { cur = comp[key]; } catch (e1) { return false; } + if (cur && typeof cur === 'object' && !(typeof cur === 'boolean')) { + // 未带轴的对象写入拒绝(避免覆盖引用) + if (typeof cur.x === 'number' || typeof cur.width === 'number' || (cc && cc.Color && cur instanceof cc.Color)) { + return false; + } + } + try { + if (typeof cur === 'boolean') comp[key] = !!value; + else if (typeof cur === 'number') comp[key] = Number(value); + else if (typeof cur === 'string') comp[key] = String(value); + else if (attrOf(attrs, key, 'enumList')) comp[key] = Number(value); + else comp[key] = value; + return true; + } catch (e2) { + console.warn('[Agent] set comp prop', key, e2); return false; } }; + /** + * @param {object} comp + * @param {string} key + * @returns {boolean} + */ + function hasCompProp(comp, key) { + if (!comp || !key) return false; + if (key in comp) return true; + var ctor = comp.constructor; + var props = (ctor && (ctor.__props__ || ctor.__values__)) || []; + return props.indexOf(key) >= 0; + } + + /** + * @param {object} comp + * @param {string} key + * @param {string} axis + * @param {*} value + * @param {object} cc + * @returns {boolean} + */ + function setCompAxisValue(comp, key, axis, value, cc) { + var cur; + try { cur = comp[key]; } catch (e) { return false; } + if (axis === 'hex') { + var hex = String(value).replace(/^#/, ''); + if (hex.length < 6) return false; + var r = parseInt(hex.slice(0, 2), 16); + var g = parseInt(hex.slice(2, 4), 16); + var b = parseInt(hex.slice(4, 6), 16); + var a = hex.length >= 8 ? parseInt(hex.slice(6, 8), 16) : 255; + if (cc && cc.Color) { + comp[key] = new cc.Color(r, g, b, a); + return true; + } + if (cur && typeof cur.r === 'number') { + cur.r = r; cur.g = g; cur.b = b; cur.a = a; + return true; + } + return false; + } + var num = Number(value); + if (!isFinite(num)) return false; + if (!cur || typeof cur !== 'object') return false; + if (axis === 'w' && typeof cur.width === 'number') { cur.width = num; return true; } + if (axis === 'h' && typeof cur.height === 'number') { cur.height = num; return true; } + if ((axis === 'x' || axis === 'y' || axis === 'z') && typeof cur[axis] === 'number') { + cur[axis] = num; + // 触发 setter(部分引擎属性需整体赋值) + try { comp[key] = cur; } catch (e2) { /* 就地改写已足够 */ } + return true; + } + if ((axis === 'r' || axis === 'g' || axis === 'b' || axis === 'a') && typeof cur[axis] === 'number') { + cur[axis] = num; + try { comp[key] = cur; } catch (e3) { /* */ } + return true; + } + return false; + } + WorkbenchAgent.prototype._listAssets = function () { var cc = this.cc; if (!cc || !cc.assetManager) return { rows: [], bundles: [] }; @@ -634,40 +904,100 @@ console.warn = wrap('warn', this._consoleOrig.warn); console.error = wrap('error', this._consoleOrig.error); console.debug = wrap('debug', this._consoleOrig.debug); + this._bindConsoleErrors(true); } else if (this._consoleOrig) { + this._bindConsoleErrors(false); console.log = this._consoleOrig.log; console.info = this._consoleOrig.info; console.warn = this._consoleOrig.warn; console.error = this._consoleOrig.error; console.debug = this._consoleOrig.debug; this._consoleOrig = null; + this._consolePost = false; if (this._consoleFlushTimer) { clearTimeout(this._consoleFlushTimer); this._consoleFlushTimer = 0; } - this._flushConsole(); + this._consoleBuf.length = 0; + } + }; + + /** 挂载/卸载 window 未捕获异常监听(随 console 通道启停) */ + WorkbenchAgent.prototype._bindConsoleErrors = function (on) { + if (on) { + if (this._consoleErrBound) return; + window.addEventListener('error', this._boundConsoleError); + window.addEventListener('unhandledrejection', this._boundConsoleRejection); + this._consoleErrBound = true; + } else if (this._consoleErrBound) { + window.removeEventListener('error', this._boundConsoleError); + window.removeEventListener('unhandledrejection', this._boundConsoleRejection); + this._consoleErrBound = false; + } + }; + + /** 脚本运行时错误 → 控制台 error */ + WorkbenchAgent.prototype._onConsoleError = function (ev) { + if (!ev) return; + var args = ev.error instanceof Error ? [ev.error] : [ev.message || 'Script error']; + var entry = this._serializeConsoleArgs('error', args); + if (ev.filename) { + entry.src = this._srcFromUrl(ev.filename, ev.lineno || 0, ev.colno || 0); } + this._enqueueConsole(entry); + }; + + /** 未处理 Promise 拒绝 → 对齐浏览器 Uncaught (in promise) */ + WorkbenchAgent.prototype._onConsoleRejection = function (ev) { + var reason = ev && ev.reason; + var args; + if (reason instanceof Error) { + args = ['Uncaught (in promise)', reason]; + } else { + args = ['Uncaught (in promise) ' + String(reason)]; + } + this._pushConsole('error', args); }; WorkbenchAgent.prototype._pushConsole = function (level, args) { - var entry = this._serializeConsoleArgs(level, args); - this._consoleBuf.push(entry); - if (this._consoleBuf.length > 200) this._consoleBuf.splice(0, this._consoleBuf.length - 200); - var self = this; - if (!this._consoleFlushTimer) { - this._consoleFlushTimer = setTimeout(function () { - self._consoleFlushTimer = 0; - self._flushConsole(); - }, CONSOLE_FLUSH_MS); + this._enqueueConsole(this._serializeConsoleArgs(level, args)); + }; + + WorkbenchAgent.prototype._enqueueConsole = function (entry) { + if (!entry) return; + if (this._consolePost && !this._hibernating) { + this._consoleBuf.push(entry); + var self = this; + if (!this._consoleFlushTimer) { + this._consoleFlushTimer = setTimeout(function () { + self._consoleFlushTimer = 0; + self._flushConsole(); + }, CONSOLE_FLUSH_MS); + } + return; } + // 未订阅时静默环缓冲,打开面板时再推送 + this._consoleHist.push(entry); + if (this._consoleHist.length > 200) this._consoleHist.splice(0, this._consoleHist.length - 200); }; WorkbenchAgent.prototype._flushConsole = function () { - if (!this._consoleBuf.length) return; + if (!this._consolePost || !this._consoleBuf.length) { + this._consoleBuf.length = 0; + return; + } var batch = this._consoleBuf.splice(0, this._consoleBuf.length); this._post({ type: 'event', name: 'console.batch', data: { items: batch } }); }; + /** 订阅时把未推送历史一次性推给壳 */ + WorkbenchAgent.prototype._flushConsoleHist = function () { + if (!this._consolePost || !this._consoleHist.length) return; + var batch = this._consoleHist.splice(0, this._consoleHist.length); + this._post({ type: 'event', name: 'console.batch', data: { items: batch } }); + this._consoleBuf.length = 0; + }; + /** * 将 console 参数序列化为可 postMessage 的富结构(保留 %c/%s 等,对象可展开)。 * 原始 console.* 仍会调用,浏览器 DevTools 打印不受影响。 @@ -676,28 +1006,124 @@ var parts = []; var plain = ''; var FMT_RE = /%(?:%|[csdifoO])/; + var rawStack = ''; + try { rawStack = new Error().stack || ''; } catch (e0) { rawStack = ''; } if (args.length && typeof args[0] === 'string' && FMT_RE.test(args[0])) { var formatted = this._formatConsole(args); parts = formatted.parts; plain = formatted.plain; + // 对齐 Chrome:%c 只作用于格式串本身;其后附加参数用默认色、不加引号 for (var r = 0; r < formatted.rest.length; r++) { - var rp = this._valuePart(formatted.rest[r]); - parts.push(rp); - plain += (plain ? ' ' : '') + (rp.plain || ''); + var rv = formatted.rest[r]; + if (typeof rv === 'string') { + parts.push({ kind: 'text', text: rv, plain: rv }); + plain += (plain ? ' ' : '') + rv; + } else { + var rp = this._valuePart(rv); + parts.push(rp); + plain += (plain ? ' ' : '') + (rp.plain || ''); + } } } else { for (var i = 0; i < args.length; i++) { - var p = this._valuePart(args[i], i === 0 && typeof args[i] === 'string'); + // 顶层消息字符串不加引号(属性展开时仍用 string 带引号) + var p = this._valuePart(args[i], typeof args[i] === 'string'); parts.push(p); plain += (i ? ' ' : '') + (p.plain || ''); } } + var src = this._srcFromStack(rawStack); + var callStack = ''; + for (var j = 0; j < parts.length; j++) { + if (parts[j] && parts[j].kind === 'error' && parts[j].stack) { + if (!src) src = this._srcFromStack(parts[j].stack); + callStack = parts[j].stack; + break; + } + } + if (!callStack && (level === 'warn' || level === 'error')) { + callStack = this._businessStack(rawStack); + } return { id: 'c' + Date.now() + '_' + Math.random().toString(36).slice(2, 7), level: level, t: Date.now(), parts: parts, plain: plain, + src: src, + stack: callStack || '', + }; + }; + + /** 去掉 agent 帧后的业务堆栈文本 */ + WorkbenchAgent.prototype._businessStack = function (stack) { + if (!stack) return ''; + var lines = String(stack).split('\n'); + var out = []; + for (var i = 0; i < lines.length; i++) { + var line = lines[i]; + var t = line.trim(); + if (!t) continue; + if (t.indexOf('Error') === 0 && t.indexOf('at ') < 0) continue; + if (this._isAgentStackLine(t)) continue; + out.push(line); + } + return out.join('\n'); + }; + + WorkbenchAgent.prototype._isAgentStackLine = function (line) { + return /agent\.js|WorkbenchAgent|_pushConsole|_enqueueConsole|_serializeConsole|_captureConsoleSrc|_srcFromStack|_businessStack|_hookConsole|_onConsoleError|_onConsoleRejection|_valuePart|_formatConsole|_flushConsole/i.test(line); + }; + + /** 解析 stack:优先可读源文件名,跳过 agent / UUID 哈希帧 */ + WorkbenchAgent.prototype._srcFromStack = function (stack) { + if (!stack) return null; + var lines = String(stack).split('\n'); + var best = null; + var bestScore = -1; + for (var i = 0; i < lines.length; i++) { + var line = lines[i].trim(); + if (!line || /^error$/i.test(line) || (line.indexOf('Error') === 0 && line.indexOf('at ') < 0)) continue; + if (this._isAgentStackLine(line)) continue; + var m = line.match(/\((.+?):(\d+):(\d+)\)\s*$/) || line.match(/at\s+(\S+?):(\d+):(\d+)\s*$/); + if (!m) continue; + var cand = this._srcFromUrl(m[1], parseInt(m[2], 10) || 0, parseInt(m[3], 10) || 0); + var score = this._scoreSrcFile(cand.file); + if (score > bestScore) { + bestScore = score; + best = cand; + } + } + return best; + }; + + /** 来源文件可读性评分(越高越优先展示) */ + WorkbenchAgent.prototype._scoreSrcFile = function (file) { + var f = String(file || ''); + if (!f || f === '(anonymous)') return 0; + if (/^[a-f0-9]{16,}$/i.test(f) || /^[a-f0-9-]{32,}$/i.test(f)) return 1; + if (/\.ts$/i.test(f)) return 50; + if (/\.js$/i.test(f) && /[a-zA-Z]/.test(f) && !/^[a-f0-9.-]+$/i.test(f.replace(/\.js$/i, ''))) return 40; + if (/\.(js|mjs|cjs)$/i.test(f)) return 20; + if (/[a-zA-Z]/.test(f)) return 10; + return 2; + }; + + /** URL + 行列 → 简短来源 */ + WorkbenchAgent.prototype._srcFromUrl = function (url, line, col) { + var u = String(url || ''); + var file = u.split('/').pop() || u; + var q = file.indexOf('?'); + if (q >= 0) file = file.slice(0, q); + var h = file.indexOf('#'); + if (h >= 0) file = file.slice(0, h); + if (!file) file = '(anonymous)'; + var ln = line || 0; + return { + file: file, + line: ln, + col: col || 0, + text: ln ? (file + ':' + ln) : file, }; }; @@ -751,7 +1177,7 @@ plain += lit; } } - return { parts: parts, plain: plain, rest: args.slice(ai) }; + return { parts: parts, plain: plain, rest: args.slice(ai), style: style }; }; WorkbenchAgent.prototype._valuePart = function (value, bareString) { diff --git a/preview-template/workbench/js/host.js b/preview-template/workbench/js/host.js index 757f542..683009e 100644 --- a/preview-template/workbench/js/host.js +++ b/preview-template/workbench/js/host.js @@ -125,6 +125,46 @@ if (this.layout) this.layout.gameHost = host; }; + /** 刷新并重启游戏(重载 iframe,保留壳布局) */ + PreviewWorkbenchHost.prototype._reloadGame = function () { + var self = this; + this._ready = false; + this._paused = false; + this._setStatus('正在刷新游戏…', false); + var pauseBtn = document.getElementById('pw-game-pause'); + if (pauseBtn) { + pauseBtn.textContent = '暂停'; + pauseBtn.classList.remove('active'); + } + for (var k in this._subs) { + if (Object.prototype.hasOwnProperty.call(this._subs, k)) this._subs[k] = false; + } + if (this.bridge) { + try { this.bridge.detach(); } catch (e) { /* */ } + } + this._mountGameIframe(); + if (!this._iframe) { + this._setStatus('刷新失败:无游戏宿主', false); + return; + } + this.bridge = new global.PWBridge(); + this.bridge.attach(this._iframe, { crossOrigin: this._crossOrigin }); + global.__PW_Bridge = this.bridge; + this._wirePanelsBridge(); + this._bindBridgeEvents(); + this.bridge.on('agent.ready', function (data) { + self._ready = !!(data && data.ok); + self._setStatus(self._ready ? '引擎已就绪' : '引擎未就绪', self._ready); + if (self._ready) { + self._fillDeviceSelect(); + if (self._workbenchOn) self._syncSubscriptions(self.layout.getVisiblePanels()); + else self.bridge.hibernate().catch(function () { /* */ }); + if (self.layout) self.layout.onEngineReady(); + } + }); + console.info('[调试工作台] 已刷新游戏 iframe'); + }; + PreviewWorkbenchHost.prototype._remountSameOrigin = function () { if (this.bridge) this.bridge.detach(); var info = global.PWBridge.buildGameUrl(true); @@ -291,6 +331,13 @@ }); } + var refreshBtn = document.getElementById('pw-game-refresh'); + if (refreshBtn) { + refreshBtn.addEventListener('click', function () { + self._reloadGame(); + }); + } + var pauseBtn = document.getElementById('pw-game-pause'); if (pauseBtn) { pauseBtn.addEventListener('click', function () { diff --git a/preview-template/workbench/js/layout.js b/preview-template/workbench/js/layout.js index 37e7d66..f0c99f9 100644 --- a/preview-template/workbench/js/layout.js +++ b/preview-template/workbench/js/layout.js @@ -652,32 +652,117 @@ WorkbenchLayout.prototype._onDocDragOver = function (ev) { if (!this._dragPanel) return; - var leafEl = ev.target.closest && ev.target.closest('.pw-leaf'); - if (!leafEl) { + // 按指针坐标几何命中(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(); - var leafId = leafEl.getAttribute('data-leaf-id'); - var info = this._leafEls[leafId]; - if (!info) return; + 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 = (ev.clientX - rect.left) / Math.max(1, rect.width); - var y = (ev.clientY - rect.top) / Math.max(1, rect.height); - var edge = this._hitEdge(info.node, x, y, ev.target); + 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, this._dragPanel) + ? 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); - } else if (resolved.node && resolved.node.type === 'split' && resolved.edge !== 'center') { + 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(); @@ -688,14 +773,30 @@ width: sr.width, height: sr.height, }, resolved.edge), 'region', false); - } else { - this._showOverlay(leafEl, resolved.edge, false); + return; } - } else { - var targetInfo = resolved.node ? this._nodeEls[resolved.node.id] : null; - var targetEl = targetInfo ? targetInfo.el : leafEl; - this._showOverlay(targetEl, resolved.edge, resolved.node && resolved.node.type === 'game'); } + 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'); }; /** 取目标矩形某一侧的新建区预览条带 */ @@ -914,10 +1015,9 @@ }; WorkbenchLayout.prototype._applyDrop = function (panelId, targetId, edge, group, groupCols) { - this._closeFloat(panelId, false); - // 组停靠:用「列内容快照」而非节点引用,避免 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); @@ -936,24 +1036,45 @@ 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) return; + 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) { - this._openFloat(panelId, null); - this._dirty = true; - this._save(); - return; - } - - if (edge === 'center') { + // 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') { @@ -1277,7 +1398,14 @@ } } this._normalizeSizes(flatSizes, flatKids.length); - return { type: 'split', dir: node.dir, sizes: flatSizes, children: flatKids }; + // 必须保留 id:按 id 停靠(尤其是整树左右新建)依赖稳定标识 + return { + type: 'split', + id: node.id || ('split_' + (this._leafSeq++)), + dir: node.dir, + sizes: flatSizes, + children: flatKids, + }; } return node; }; @@ -1463,8 +1591,8 @@ var head = document.createElement('div'); head.className = 'pw-float-head'; head.innerHTML = - '' + - '拖标题可停靠' + + '' + + '拖标题栏移动 / 停靠' + ''; head.querySelector('.pw-float-title').textContent = PANEL_TITLES[panelId] || panelId; var content = document.createElement('div'); @@ -1480,31 +1608,15 @@ self._closeFloat(panelId, true); self._savePanelMemory(); }); - // 标题文字:HTML5 拖放到布局区停靠 - var title = head.querySelector('.pw-float-title'); - title.addEventListener('dragstart', function (ev) { - self._floatDrag = null; - win.classList.add('pw-float-dock-dragging'); - self._beginDrag(panelId, ev); - }); - title.addEventListener('dragend', function () { - win.classList.remove('pw-float-dock-dragging'); - self._clearDragVisual(); - self._dragPanel = null; - self._hoverTargetId = null; - self._hoverEdge = null; - self._hoverGroup = null; - self._hoverGroupCols = null; - }); - // 标题栏空白处:移动悬浮窗位置(用当前坐标,禁止用创建时闭包坐标) + // 仅标题栏可拖:移动窗口;拖到停靠区松手即停靠(禁止拖面板内容) head.addEventListener('pointerdown', function (ev) { if (ev.target && ev.target.closest && ev.target.closest('.pw-float-close')) return; - if (ev.target && ev.target.closest && ev.target.closest('.pw-float-title')) return; var f = self._floats[panelId]; if (!f) return; ev.preventDefault(); - head.setPointerCapture && head.setPointerCapture(ev.pointerId); + if (head.setPointerCapture) head.setPointerCapture(ev.pointerId); self._setGameIframePassThrough(true); + win.classList.add('pw-float-dragging'); self._floatDrag = { id: panelId, ox: ev.clientX, @@ -1533,7 +1645,6 @@ f.el.style.left = f.x + 'px'; f.el.style.top = f.y + 'px'; d.moved = true; - // 移动时探测下方停靠区(标题拖放之外的快捷停靠) this._probeFloatDockHover(ev.clientX, ev.clientY, d.id); }; @@ -1544,9 +1655,17 @@ window.removeEventListener('pointerup', this._onFloatUp); this._setGameIframePassThrough(false); if (!d) return; + var win = this._floats[d.id] && this._floats[d.id].el; + if (win) win.classList.remove('pw-float-dragging'); + // 使用拖动过程中最后一次悬停结果,避免松手瞬间坐标抖动导致重探测失败 + var targetId = this._hoverTargetId; + var edge = this._hoverEdge; + var group = this._hoverGroup; + var groupCols = this._hoverGroupCols; var docked = false; - if (d.moved && this._hoverEdge && (this._hoverTargetId || (this._hoverGroupCols && this._hoverGroupCols.length))) { - docked = this._dockFloatingPanel(d.id, ev.clientX, ev.clientY); + if (d.moved && edge && (targetId || (groupCols && groupCols.length) || (group && group.length))) { + this._applyDrop(d.id, targetId, edge, group, groupCols); + docked = !this._floats[d.id]; } this._clearDragVisual(); this._dragPanel = null; @@ -1560,22 +1679,28 @@ } }; - /** 悬浮窗移动时:临时穿透命中下方 leaf,刷新停靠指示 */ + /** 悬浮窗移动时:按坐标几何命中 leaf,刷新停靠指示 */ WorkbenchLayout.prototype._probeFloatDockHover = function (cx, cy, panelId) { - var floats = this._floats; - var ids = []; - for (var id in floats) { - if (Object.prototype.hasOwnProperty.call(floats, id) && floats[id].el) { - ids.push(id); - floats[id].el.style.pointerEvents = 'none'; + this._dragPanel = panelId; + var info = this._findLeafAtClientPoint(cx, cy); + // 外缘停靠:即使点不在任何 leaf 上(例如右侧空白),仍允许 + if (!info) { + var ws = this.workspace ? this.workspace.getBoundingClientRect() : null; + if (ws && ws.width > 40) { + var margin = Math.min(48, ws.width * 0.08); + if (cx >= ws.right - margin || cx <= ws.left + margin) { + var side = cx >= ws.right - margin ? 'right' : 'left'; + var rootEdge = this._resolveWorkspaceSideDrop(side); + if (rootEdge) { + this._hoverTargetId = '__workspace__'; + this._hoverEdge = rootEdge.edge; + this._hoverGroup = null; + this._hoverGroupCols = null; + this._paintDockHover(rootEdge, null); + return; + } + } } - } - var el = document.elementFromPoint(cx, cy); - for (var i = 0; i < ids.length; i++) { - if (floats[ids[i]] && floats[ids[i]].el) floats[ids[i]].el.style.pointerEvents = ''; - } - var leafEl = el && el.closest && el.closest('.pw-leaf'); - if (!leafEl) { this._clearDragVisual(); this._hoverTargetId = null; this._hoverEdge = null; @@ -1583,40 +1708,7 @@ this._hoverGroupCols = null; return; } - this._dragPanel = panelId; - var leafId = leafEl.getAttribute('data-leaf-id'); - var info = this._leafEls[leafId]; - if (!info) return; - 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, el); - 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; - if (resolved.group && resolved.group.length) { - this._showOverlayRect(this._edgeStripRect(this._unionNodeRects(resolved.group), resolved.edge), 'region', false); - } else 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); - } - } else { - var targetInfo = resolved.node ? this._nodeEls[resolved.node.id] : null; - var targetEl = targetInfo ? targetInfo.el : leafEl; - this._showOverlay(targetEl, resolved.edge, resolved.node && resolved.node.type === 'game'); - } + this._updateDockHoverFromPoint(cx, cy, panelId, info, null); }; WorkbenchLayout.prototype._dockFloatingPanel = function (panelId, cx, cy) { diff --git a/preview-template/workbench/js/panels/console.js b/preview-template/workbench/js/panels/console.js index 047b63b..f30a945 100644 --- a/preview-template/workbench/js/panels/console.js +++ b/preview-template/workbench/js/panels/console.js @@ -15,6 +15,8 @@ this._bridge = null; this._expanded = {}; this._onListClick = this._onListClick.bind(this); + this._onLevelClick = this._onLevelClick.bind(this); + this._counts = { all: 0, error: 0, warn: 0, info: 0 }; } ConsolePanel.prototype.setBridge = function (bridge) { @@ -26,28 +28,24 @@ var root = document.createElement('div'); root.className = 'pw-panel-inner pw-console'; root.innerHTML = - '
' + + '
' + '控制台' + - '
' + - '' + - '' + + '
' + + '
' + + '' + + '' + + '' + + '' + + '
' + + '' + '' + '
' + '
'; this.el = root; this.list = root.querySelector('#pw-console-list'); this.list.addEventListener('click', this._onListClick); - root.querySelector('#pw-console-level').addEventListener('change', function (ev) { - self.filter = ev.target.value; - self.render(); - }); + var levels = root.querySelector('.pw-console-levels'); + if (levels) levels.addEventListener('click', this._onLevelClick); root.querySelector('#pw-console-q').addEventListener('input', function (ev) { self.query = ev.target.value || ''; self.render(); @@ -55,11 +53,23 @@ root.querySelector('#pw-console-clear').addEventListener('click', function () { self.buffer.length = 0; self._expanded = {}; + self._recount(); self.render(); }); return root; }; + ConsolePanel.prototype._onLevelClick = function (ev) { + var btn = ev.target.closest && ev.target.closest('[data-level]'); + if (!btn || !this.el) return; + this.filter = btn.getAttribute('data-level') || 'all'; + var chips = this.el.querySelectorAll('.pw-console-chip'); + for (var i = 0; i < chips.length; i++) { + chips[i].classList.toggle('active', chips[i].getAttribute('data-level') === this.filter); + } + this.render(); + }; + /** * @param {Array} items Agent 序列化后的富日志 */ @@ -67,7 +77,7 @@ if (!items || !items.length) return; for (var i = 0; i < items.length; i++) { var it = items[i]; - var key = it.level + '|' + (it.plain || ''); + var key = it.level + '|' + (it.plain || '') + '|' + ((it.src && it.src.text) || ''); var last = this.buffer.length ? this.buffer[this.buffer.length - 1] : null; if (last && last._key === key) { last.count = (last.count || 1) + 1; @@ -82,10 +92,37 @@ var removed = this.buffer.shift(); if (removed) delete this._expanded[removed.id]; } + this._recount(); this.render(); }; + ConsolePanel.prototype._recount = function () { + var c = { all: 0, error: 0, warn: 0, info: 0 }; + for (var i = 0; i < this.buffer.length; i++) { + var lv = this.buffer[i].level; + var n = this.buffer[i].count || 1; + c.all += n; + if (lv === 'error') c.error += n; + else if (lv === 'warn') c.warn += n; + else c.info += n; + } + this._counts = c; + if (!this.el) return; + var nodes = this.el.querySelectorAll('[data-count]'); + for (var j = 0; j < nodes.length; j++) { + var k = nodes[j].getAttribute('data-count'); + nodes[j].textContent = String(c[k] || 0); + } + }; + ConsolePanel.prototype._onListClick = function (ev) { + var stackBtn = ev.target.closest && ev.target.closest('[data-stack]'); + if (stackBtn) { + var sid = 'stack:' + (stackBtn.getAttribute('data-stack') || ''); + this._expanded[sid] = !this._expanded[sid]; + this.render(); + return; + } var toggle = ev.target.closest && ev.target.closest('[data-expand]'); if (!toggle) return; var oid = toggle.getAttribute('data-oid'); @@ -114,6 +151,15 @@ } }; + ConsolePanel.prototype._matchFilter = function (item) { + if (this.filter === 'error') return item.level === 'error'; + if (this.filter === 'warn') return item.level === 'warn'; + if (this.filter === 'info') { + return item.level === 'log' || item.level === 'info' || item.level === 'debug'; + } + return true; + }; + ConsolePanel.prototype.render = function () { if (!this.list) return; var q = this.query.toLowerCase(); @@ -121,12 +167,14 @@ var any = false; for (var i = 0; i < this.buffer.length; i++) { var item = this.buffer[i]; - if (this.filter !== 'all' && item.level !== this.filter) continue; + if (!this._matchFilter(item)) continue; if (q && String(item.plain || '').toLowerCase().indexOf(q) < 0) continue; any = true; html += this._renderRow(item); } - this.list.innerHTML = any ? html : '
暂无日志(打开本面板后开始捕获;原始输出仍在浏览器控制台)
'; + this.list.innerHTML = any + ? html + : '
暂无日志(切到「全部」可看警告/错误;原始输出仍在浏览器控制台)
'; this.list.scrollTop = this.list.scrollHeight; }; @@ -140,10 +188,27 @@ var count = item.count > 1 ? '' + item.count + '' : ''; + var srcText = item.src && item.src.text ? item.src.text : ''; + var src = srcText + ? '' + escapeHtml(srcText) + '' + : ''; + + var stackKey = 'stack:' + item.id; + var hasStack = !!(item.stack && String(item.stack).trim()); + var stackOpen = !!this._expanded[stackKey]; + var stackToggle = hasStack + ? '' + : ''; + var stackBlock = hasStack && stackOpen + ? '
' + escapeHtml(item.stack) + '
' + : ''; + return ( '
' + - icon + count + - '
' + body + '
' + + stackToggle + icon + count + + '
' + body + stackBlock + '
' + + src + '
' ); }; @@ -178,8 +243,8 @@ return 'ƒ ' + escapeHtml(part.name || 'anonymous') + '()'; } if (kind === 'error') { - var stack = part.stack ? '
' + escapeHtml(part.stack) + '
' : ''; - return '' + escapeHtml((part.name || 'Error') + ': ' + (part.message || '')) + '' + stack; + // 堆栈在行级折叠展示,此处只渲染消息 + return '' + escapeHtml((part.name || 'Error') + ': ' + (part.message || '')) + ''; } if (kind === 'object') return this._renderObject(part, item); return '' + escapeHtml(part.plain || '') + ''; diff --git a/preview-template/workbench/js/panels/inspector.js b/preview-template/workbench/js/panels/inspector.js index 386c721..f9232b6 100644 --- a/preview-template/workbench/js/panels/inspector.js +++ b/preview-template/workbench/js/panels/inspector.js @@ -2,7 +2,7 @@ 'use strict'; /** - * 属性面板(壳侧):通过 Bridge 读写节点属性。 + * 属性面板(壳侧):通过 Bridge 读写节点属性;标签优先使用 displayName。 * @constructor */ function InspectorPanel() { @@ -67,15 +67,6 @@ html.push(rowVec3('缩放', ['sx', 'sy', 'sz'], [p.scale.x, p.scale.y, p.scale.z])); html.push('
'); - if (p.ui) { - html.push('
'); - html.push('
UITransform
'); - html.push('
'); - html.push(rowVec2('尺寸', ['uiw', 'uih'], [p.ui.width, p.ui.height], ['W', 'H'])); - html.push(rowVec2('锚点', ['uax', 'uay'], [p.ui.anchorX, p.ui.anchorY], ['X', 'Y'])); - html.push('
'); - } - var comps = p.comps || []; for (var i = 0; i < comps.length; i++) { var c = comps[i]; @@ -86,11 +77,7 @@ var fields = c.fields || []; if (!fields.length) html.push('
无可编辑公开字段
'); for (var f = 0; f < fields.length; f++) { - var field = fields[f]; - var fid = 'c' + c.index + '_' + field.key; - if (field.type === 'number') html.push(rowNum(field.key, fid, field.value)); - else if (field.type === 'boolean') html.push(rowCheck(field.key, fid, field.value)); - else html.push(rowText(field.key, fid, field.value)); + html.push(renderField(c.index, fields[f])); } html.push('
'); } @@ -100,6 +87,35 @@ this._bindSections(); }; + /** + * @param {number} compIndex + * @param {object} field + * @returns {string} + */ + function renderField(compIndex, field) { + var label = field.label || field.key; + var fid = 'c' + compIndex + '_' + field.key; + var type = field.type; + if (field.readOnly || type === 'ref') { + return rowRef(label, field.value); + } + if (type === 'boolean') return rowCheck(label, fid, !!field.value); + if (type === 'number') return rowNum(label, fid, field.value); + if (type === 'string') return rowText(label, fid, field.value); + if (type === 'enum') return rowEnum(label, fid, field.value, field.enumOptions || []); + if (type === 'vec2') { + return rowVec2(label, [fid + '_x', fid + '_y'], [field.value.x, field.value.y], ['X', 'Y']); + } + if (type === 'vec3') { + return rowVec3(label, [fid + '_x', fid + '_y', fid + '_z'], [field.value.x, field.value.y, field.value.z]); + } + if (type === 'size') { + return rowVec2(label, [fid + '_w', fid + '_h'], [field.value.width, field.value.height], ['W', 'H']); + } + if (type === 'color') return rowColor(label, fid, field.value); + return rowText(label, fid, field.value == null ? '' : String(field.value)); + } + InspectorPanel.prototype._bindSections = function () { var heads = this.body.querySelectorAll('.pw-insp-sec-head[data-toggle]'); for (var i = 0; i < heads.length; i++) { @@ -120,35 +136,89 @@ var run = function () { var fid = el.getAttribute('data-fid'); var value; - if (el.type === 'checkbox') value = !!el.checked; + if (el.tagName === 'SELECT') value = parseEnumValue(el); + else if (el.type === 'checkbox') value = !!el.checked; + else if (el.type === 'color') value = el.value; else if (el.type === 'number') value = parseFloat(el.value); else value = el.value; self._bridge.call('node.set', { uuid: self._uuid, fid: fid, value: value }); + if (el.getAttribute('data-color-pair')) { + var siblings = el.parentNode ? el.parentNode.querySelectorAll('[data-fid="' + fid + '"]') : []; + for (var si = 0; si < siblings.length; si++) { + if (siblings[si] === el) continue; + var nv = String(value); + if (nv.charAt(0) !== '#' && siblings[si].type === 'color') nv = '#' + nv; + if (siblings[si].type === 'color') siblings[si].value = nv.slice(0, 7); + else siblings[si].value = nv; + } + } }; el.addEventListener('change', run); - el.addEventListener('input', run); + if (el.type !== 'checkbox' && el.tagName !== 'SELECT') el.addEventListener('input', run); })(inputs[i]); } }; + function parseEnumValue(el) { + var opt = el.options[el.selectedIndex]; + if (!opt) return el.value; + var raw = opt.getAttribute('data-num'); + if (raw != null && raw !== '') return Number(raw); + return el.value; + } + + function labelHtml(label) { + var text = label == null ? '' : String(label); + return ''; + } function rowText(label, id, value) { - return '
' + labelHtml(label) + + '
'; } function rowNum(label, id, value) { - return '
' + labelHtml(label) + + '
'; } function rowCheck(label, id, value) { - return '
'; } + function rowEnum(label, id, value, options) { + var opts = []; + for (var i = 0; i < options.length; i++) { + var o = options[i]; + var selected = o.value === value || String(o.value) === String(value); + opts.push(''); + } + return '
' + labelHtml(label) + + '
'; + } + function rowRef(label, value) { + var v = value || {}; + var text = (v.kind ? v.kind + ' · ' : '') + (v.name || '(空)'); + return '
' + labelHtml(label) + + '
' + + '' + escapeHtml(v.kind || 'Object') + '' + + '' + escapeHtml(v.name || '(空)') + '
'; + } + function rowColor(label, id, value) { + var r = (value && value.r) || 0; + var g = (value && value.g) || 0; + var b = (value && value.b) || 0; + var hex = '#' + toHex2(r) + toHex2(g) + toHex2(b); + return '
' + labelHtml(label) + + '
' + + '' + + '' + + '
'; + } function rowVec3(label, ids, values) { - return '
' + - '' + + return '
' + labelHtml(label) + '
' + axis('X', ids[0], values[0]) + axis('Y', ids[1], values[1]) + @@ -156,8 +226,7 @@ '
'; } function rowVec2(label, ids, values, axisNames) { - return '
' + - '' + + return '
' + labelHtml(label) + '
' + axis(axisNames[0], ids[0], values[0]) + axis(axisNames[1], ids[1], values[1]) + @@ -168,6 +237,10 @@ '
'; } + function toHex2(n) { + var s = Math.max(0, Math.min(255, n | 0)).toString(16); + return s.length < 2 ? '0' + s : s; + } function escapeHtml(s) { return String(s).replace(/&/g, '&').replace(//g, '>');