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.
328 lines
14 KiB
328 lines
14 KiB
(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 =
|
|
'<div class="pw-panel-head pw-console-head">' +
|
|
'<span class="pw-panel-title">控制台</span>' +
|
|
'<div class="pw-panel-tools pw-console-tools">' +
|
|
'<div class="pw-console-levels" role="group" aria-label="日志级别">' +
|
|
'<button type="button" class="pw-console-chip active" data-level="all" title="全部">全部<span class="pw-console-chip-n" data-count="all">0</span></button>' +
|
|
'<button type="button" class="pw-console-chip" data-level="error" title="错误">错误<span class="pw-console-chip-n" data-count="error">0</span></button>' +
|
|
'<button type="button" class="pw-console-chip" data-level="warn" title="警告">警告<span class="pw-console-chip-n" data-count="warn">0</span></button>' +
|
|
'<button type="button" class="pw-console-chip" data-level="info" title="信息/日志">信息<span class="pw-console-chip-n" data-count="info">0</span></button>' +
|
|
'</div>' +
|
|
'<input id="pw-console-q" class="pw-input pw-console-filter" placeholder="过滤" />' +
|
|
'<button type="button" id="pw-console-clear" class="pw-btn">清空</button>' +
|
|
'</div></div>' +
|
|
'<div class="pw-panel-body pw-console-list" id="pw-console-list"></div>';
|
|
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
|
|
: '<div class="pw-console-empty">暂无日志(切到「全部」可看警告/错误;原始输出仍在浏览器控制台)</div>';
|
|
this.list.scrollTop = this.list.scrollHeight;
|
|
};
|
|
|
|
ConsolePanel.prototype._renderRow = function (item) {
|
|
var icon = '';
|
|
if (item.level === 'warn') icon = '<span class="pw-console-icon pw-console-icon-warn" title="warning"></span>';
|
|
else if (item.level === 'error') icon = '<span class="pw-console-icon pw-console-icon-error" title="error"></span>';
|
|
else if (item.level === 'info') icon = '<span class="pw-console-icon pw-console-icon-info" title="info"></span>';
|
|
|
|
var body = this._renderParts(item.parts || [], item);
|
|
var count = item.count > 1
|
|
? '<span class="pw-console-count">' + item.count + '</span>'
|
|
: '';
|
|
var srcText = item.src && item.src.text ? item.src.text : '';
|
|
var src = srcText
|
|
? '<span class="pw-console-src" title="' + escapeAttr(srcText) + '">' + escapeHtml(srcText) + '</span>'
|
|
: '<span class="pw-console-src pw-console-src-empty"></span>';
|
|
|
|
var stackKey = 'stack:' + item.id;
|
|
var hasStack = !!(item.stack && String(item.stack).trim());
|
|
var stackOpen = !!this._expanded[stackKey];
|
|
var stackToggle = hasStack
|
|
? '<span class="pw-console-toggle pw-console-stack-toggle' + (stackOpen ? ' open' : '') +
|
|
'" data-stack="' + escapeAttr(item.id) + '" title="展开/折叠堆栈"><span class="pw-console-caret"></span></span>'
|
|
: '<span class="pw-console-stack-spacer"></span>';
|
|
var stackBlock = hasStack && stackOpen
|
|
? '<pre class="pw-console-stack">' + escapeHtml(item.stack) + '</pre>'
|
|
: '';
|
|
|
|
return (
|
|
'<div class="pw-console-row level-' + item.level + '">' +
|
|
stackToggle + icon + count +
|
|
'<div class="pw-console-msg">' + body + stackBlock + '</div>' +
|
|
src +
|
|
'</div>'
|
|
);
|
|
};
|
|
|
|
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 '<span class="pw-v-fmt"' + styleAttr + '>' + escapeHtml(part.text || '') + '</span>';
|
|
}
|
|
if (kind === 'text') return '<span class="pw-v-text">' + escapeHtml(part.text || '') + '</span>';
|
|
if (kind === 'string') return '<span class="pw-v-string">"' + escapeHtml(part.text || '') + '"</span>';
|
|
if (kind === 'number' || kind === 'bigint') {
|
|
return '<span class="pw-v-number">' + escapeHtml(String(part.value)) + (kind === 'bigint' ? 'n' : '') + '</span>';
|
|
}
|
|
if (kind === 'boolean') return '<span class="pw-v-boolean">' + String(part.value) + '</span>';
|
|
if (kind === 'null') return '<span class="pw-v-null">null</span>';
|
|
if (kind === 'undefined') return '<span class="pw-v-undefined">undefined</span>';
|
|
if (kind === 'symbol') return '<span class="pw-v-symbol">' + escapeHtml(part.text || '') + '</span>';
|
|
if (kind === 'function') {
|
|
return '<span class="pw-v-function">ƒ ' + escapeHtml(part.name || 'anonymous') + '()</span>';
|
|
}
|
|
if (kind === 'error') {
|
|
// 堆栈在行级折叠展示,此处只渲染消息
|
|
return '<span class="pw-v-error">' + escapeHtml((part.name || 'Error') + ': ' + (part.message || '')) + '</span>';
|
|
}
|
|
if (kind === 'object') return this._renderObject(part, item);
|
|
return '<span class="pw-v-text">' + escapeHtml(part.plain || '') + '</span>';
|
|
};
|
|
|
|
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 =
|
|
'<span class="pw-console-toggle' + (open ? ' open' : '') + '" data-expand="' + escapeAttr(oid) +
|
|
'" data-oid="' + escapeAttr(oid) + '">' +
|
|
'<span class="pw-console-caret"></span>' +
|
|
'<span class="pw-v-object">' + escapeHtml(part.preview || part.ctor || 'Object') + '</span>' +
|
|
'</span>';
|
|
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 += '<div class="pw-console-prop"><span class="pw-v-muted">…</span></div>';
|
|
continue;
|
|
}
|
|
rows +=
|
|
'<div class="pw-console-prop">' +
|
|
'<span class="pw-v-key">' + escapeHtml(pr.key) + '</span>' +
|
|
'<span class="pw-v-muted">: </span>' +
|
|
this._renderPartNested(pr.part, item) +
|
|
'</div>';
|
|
}
|
|
return head + '<div class="pw-console-children">' + rows + '</div>';
|
|
};
|
|
|
|
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, '>')
|
|
.replace(/"/g, '"');
|
|
}
|
|
|
|
function escapeAttr(s) {
|
|
return String(s)
|
|
.replace(/&/g, '&')
|
|
.replace(/"/g, '"')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>');
|
|
}
|
|
|
|
global.PWConsolePanel = ConsolePanel;
|
|
})(window);
|
|
|