(function (global) { 'use strict'; /** * 控制台面板(壳侧):DevTools 风格渲染;原始 console 仍在游戏页输出。 * @constructor */ function ConsolePanel() { this.el = null; this.list = null; this.filter = 'all'; this.query = ''; this.buffer = []; this.capacity = 500; 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) { this._bridge = bridge; }; ConsolePanel.prototype.mount = function () { var self = this; 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); 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(); }); 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 序列化后的富日志 */ ConsolePanel.prototype.applyBatch = function (items) { if (!items || !items.length) return; for (var i = 0; i < items.length; i++) { var it = items[i]; 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; } else { it.count = 1; it._key = key; if (!it.children) it.children = {}; this.buffer.push(it); } } while (this.buffer.length > this.capacity) { 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'); if (!oid) return; var self = this; var open = !this._expanded[oid]; this._expanded[oid] = open; if (open && this._bridge) { this._bridge.call('console.inspect', { oid: oid, path: '' }).then(function (res) { self._storeChildren(oid, res && res.props); self.render(); }).catch(function () { self.render(); }); } else { this.render(); } }; ConsolePanel.prototype._storeChildren = function (oid, props) { if (!props) return; for (var i = 0; i < this.buffer.length; i++) { var item = this.buffer[i]; if (!item.children) item.children = {}; item.children[oid] = props; } }; 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(); var html = ''; var any = false; for (var i = 0; i < this.buffer.length; i++) { var item = this.buffer[i]; 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.scrollTop = this.list.scrollHeight; }; ConsolePanel.prototype._renderRow = function (item) { var icon = ''; if (item.level === 'warn') icon = ''; else if (item.level === 'error') icon = ''; else if (item.level === 'info') icon = ''; var body = this._renderParts(item.parts || [], item); 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 ( '
' + stackToggle + icon + count + '
' + body + stackBlock + '
' + src + '
' ); }; ConsolePanel.prototype._renderParts = function (parts, item) { var html = ''; for (var i = 0; i < parts.length; i++) { if (i) html += ' '; html += this._renderPart(parts[i], item); } return html; }; ConsolePanel.prototype._renderPart = function (part, item) { if (!part) return ''; var kind = part.kind; if (kind === 'styled') { var css = sanitizeCss(part.css || ''); var styleAttr = css ? ' style="' + escapeAttr(css) + '"' : ''; return '' + escapeHtml(part.text || '') + ''; } if (kind === 'text') return '' + escapeHtml(part.text || '') + ''; if (kind === 'string') return '"' + escapeHtml(part.text || '') + '"'; if (kind === 'number' || kind === 'bigint') { return '' + escapeHtml(String(part.value)) + (kind === 'bigint' ? 'n' : '') + ''; } if (kind === 'boolean') return '' + String(part.value) + ''; if (kind === 'null') return 'null'; if (kind === 'undefined') return 'undefined'; if (kind === 'symbol') return '' + escapeHtml(part.text || '') + ''; if (kind === 'function') { return 'ƒ ' + escapeHtml(part.name || 'anonymous') + '()'; } if (kind === 'error') { // 堆栈在行级折叠展示,此处只渲染消息 return '' + escapeHtml((part.name || 'Error') + ': ' + (part.message || '')) + ''; } if (kind === 'object') return this._renderObject(part, item); return '' + escapeHtml(part.plain || '') + ''; }; ConsolePanel.prototype._renderObject = function (part, item) { return this._renderPartNested(part, item); }; ConsolePanel.prototype._renderPartNested = function (part, item) { if (!part) return ''; if (part.kind !== 'object') return this._renderPart(part, item); var oid = part.oid; var open = !!this._expanded[oid]; var head = '' + '' + '' + escapeHtml(part.preview || part.ctor || 'Object') + '' + ''; if (!open) return head; var props = (item.children && item.children[oid]) || []; var rows = ''; for (var i = 0; i < props.length; i++) { var pr = props[i]; if (pr.key === '…') { rows += '
…
'; continue; } rows += '
' + '' + escapeHtml(pr.key) + '' + ': ' + this._renderPartNested(pr.part, item) + '
'; } return head + '
' + rows + '
'; }; function sanitizeCss(css) { if (typeof css !== 'string' || !css) return ''; if (/url\s*\(|expression\s*\(|javascript:|@import|behavior\s*:/i.test(css)) return ''; var allow = { color: 1, background: 1, 'background-color': 1, 'font-weight': 1, 'font-style': 1, 'font-size': 1, 'text-decoration': 1, 'text-shadow': 1, 'line-height': 1, opacity: 1, }; var parts = css.split(';'); var out = []; for (var i = 0; i < parts.length; i++) { var p = parts[i].trim(); if (!p) continue; var colon = p.indexOf(':'); if (colon <= 0) continue; var prop = p.slice(0, colon).trim().toLowerCase(); var val = p.slice(colon + 1).trim(); if (!allow[prop] || !val) continue; if (/url\s*\(|expression|javascript:/i.test(val)) continue; out.push(prop + ': ' + val); } return out.join('; '); } function escapeHtml(s) { return String(s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function escapeAttr(s) { return String(s) .replace(/&/g, '&') .replace(/"/g, '"') .replace(//g, '>'); } global.PWConsolePanel = ConsolePanel; })(window);