Initial commit

This commit is contained in:
TYt50
2026-01-17 16:34:39 +08:00
commit 73b8a6d23f
72 changed files with 45972 additions and 0 deletions

View File

@@ -0,0 +1,769 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>LittleWhiteBox 监控台</title>
<style>
:root {
--border: rgba(255,255,255,0.10);
--text: rgba(255,255,255,0.92);
--muted: rgba(255,255,255,0.65);
--info: #bdbdbd;
--warn: #ffcc66;
--error: #ff6b6b;
--accent: #7aa2ff;
--success: #4ade80;
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
font-size: 12px;
}
html, body {
height: 100%;
margin: 0;
background: transparent;
color: var(--text);
}
.root {
height: 100%;
display: flex;
flex-direction: column;
}
.topbar {
display: flex;
gap: 8px;
align-items: center;
justify-content: space-between;
padding: 8px;
border-bottom: 1px solid var(--border);
background: rgba(18,18,18,0.65);
backdrop-filter: blur(6px);
flex-shrink: 0;
}
.content {
flex: 1;
overflow: auto;
padding: 10px;
}
.tabs {
display: flex;
gap: 6px;
}
.tab {
padding: 6px 10px;
border-radius: 8px;
cursor: pointer;
border: 1px solid var(--border);
background: rgba(255,255,255,0.04);
user-select: none;
}
.tab.active {
border-color: rgba(122,162,255,0.55);
background: rgba(122,162,255,0.10);
}
button {
border: 1px solid var(--border);
background: rgba(255,255,255,0.05);
color: var(--text);
border-radius: 8px;
padding: 6px 10px;
cursor: pointer;
font-size: 12px;
}
button:hover {
background: rgba(255,255,255,0.09);
}
select {
border: 1px solid var(--border);
background: rgba(0,0,0,0.25);
color: var(--text);
border-radius: 8px;
padding: 6px 8px;
font-size: 12px;
outline: none;
}
.row {
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
margin-bottom: 10px;
}
.card {
border: 1px solid var(--border);
background: rgba(0,0,0,0.18);
border-radius: 10px;
overflow: hidden;
}
.muted { color: var(--muted); }
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
.empty-hint { padding: 20px; text-align: center; color: var(--muted); }
/* 日志 */
.log-item {
padding: 8px 10px;
border-bottom: 1px solid rgba(255,255,255,0.06);
}
.log-item:last-child { border-bottom: none; }
.log-header {
display: flex;
gap: 8px;
align-items: baseline;
flex-wrap: wrap;
}
.log-toggle {
width: 16px;
height: 16px;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: var(--muted);
font-size: 10px;
flex-shrink: 0;
border-radius: 4px;
user-select: none;
transition: transform 0.15s;
}
.log-toggle:hover { background: rgba(255,255,255,0.1); }
.log-toggle.empty { visibility: hidden; cursor: default; }
.log-item.open .log-toggle { transform: rotate(90deg); }
.time { color: var(--muted); }
.lvl { font-weight: 700; }
.lvl.info { color: var(--info); }
.lvl.warn { color: var(--warn); }
.lvl.error { color: var(--error); }
.mod { color: var(--accent); }
.msg { color: var(--text); word-break: break-word; }
.stack {
margin: 8px 0 0 24px;
padding: 10px;
white-space: pre-wrap;
word-break: break-all;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
color: rgba(255,255,255,0.85);
background: rgba(0,0,0,0.3);
border-radius: 6px;
font-size: 11px;
display: none;
}
.log-item.open .stack { display: block; }
/* 事件 */
.section-collapse { margin-bottom: 12px; }
.section-collapse-header {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
cursor: pointer;
border-radius: 8px;
background: rgba(255,255,255,0.03);
border: 1px solid var(--border);
user-select: none;
}
.section-collapse-header:hover { background: rgba(255,255,255,0.06); }
.section-collapse-header .arrow {
transition: transform 0.2s;
color: var(--muted);
font-size: 10px;
}
.section-collapse-header.open .arrow { transform: rotate(90deg); }
.section-collapse-header .title { flex: 1; }
.section-collapse-header .count { color: var(--muted); font-size: 11px; }
.section-collapse-content {
display: none;
padding: 8px 0 0 0;
max-height: 200px;
overflow-y: auto;
}
.section-collapse-header.open + .section-collapse-content { display: block; }
.module-section { margin-bottom: 8px; }
.module-header {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
cursor: pointer;
border-radius: 6px;
background: rgba(255,255,255,0.02);
border: 1px solid rgba(255,255,255,0.06);
user-select: none;
}
.module-header:hover { background: rgba(255,255,255,0.05); }
.module-header .arrow { transition: transform 0.2s; color: var(--muted); font-size: 9px; }
.module-header.open .arrow { transform: rotate(90deg); }
.module-header .name { color: var(--accent); font-weight: 600; }
.module-header .count { color: var(--muted); }
.module-events { display: none; padding: 6px 10px 6px 28px; }
.module-header.open + .module-events { display: block; }
.event-tag {
display: inline-block;
padding: 2px 8px;
margin: 2px 4px 2px 0;
border-radius: 6px;
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.08);
font-size: 11px;
font-family: ui-monospace, monospace;
}
.event-tag .dup { color: var(--error); font-weight: 700; margin-left: 4px; }
.pill {
display: inline-flex;
padding: 2px 8px;
border-radius: 999px;
border: 1px solid var(--border);
background: rgba(255,255,255,0.05);
}
.repeat-badge {
min-width: 18px;
height: 18px;
padding: 0 5px;
border-radius: 999px;
background: rgba(255,255,255,0.12);
color: var(--muted);
font-size: 10px;
font-weight: 600;
margin-left: 6px;
display: inline-flex;
align-items: center;
justify-content: center;
}
/* 缓存 */
table { width: 100%; border-collapse: collapse; }
th, td { padding: 8px 10px; border-bottom: 1px solid rgba(255,255,255,0.06); text-align: left; }
th { color: rgba(255,255,255,0.75); font-weight: 600; }
.right { text-align: right; }
.cache-detail-row { display: none; }
.cache-detail-row.open { display: table-row; }
.cache-detail-row td { padding: 0; }
.pre {
padding: 10px;
white-space: pre-wrap;
font-family: ui-monospace, monospace;
color: rgba(255,255,255,0.80);
background: rgba(0,0,0,0.25);
font-size: 11px;
}
/* 性能 */
.perf-overview {
display: flex;
gap: 16px;
flex-wrap: wrap;
padding: 12px;
margin-bottom: 12px;
border: 1px solid var(--border);
border-radius: 10px;
background: rgba(0,0,0,0.18);
}
.perf-stat { display: flex; flex-direction: column; gap: 2px; }
.perf-stat .label { font-size: 10px; color: var(--muted); text-transform: uppercase; }
.perf-stat .value { font-size: 16px; font-weight: 700; }
.perf-stat .value.good { color: var(--success); }
.perf-stat .value.warn { color: var(--warn); }
.perf-stat .value.bad { color: var(--error); }
.perf-section { margin-bottom: 16px; }
.perf-section-title {
font-size: 11px;
color: var(--muted);
text-transform: uppercase;
margin-bottom: 6px;
display: flex;
align-items: center;
gap: 8px;
}
.perf-item { padding: 8px 10px; border-bottom: 1px solid rgba(255,255,255,0.06); }
.perf-item:last-child { border-bottom: none; }
.perf-item .top { display: flex; gap: 8px; align-items: baseline; }
.perf-item .url { flex: 1; font-family: ui-monospace, monospace; font-size: 11px; word-break: break-all; }
.perf-item .duration { font-weight: 700; }
.perf-item .duration.slow { color: var(--warn); }
.perf-item .duration.very-slow { color: var(--error); }
</style>
</head>
<body>
<div class="root">
<div class="topbar">
<div class="tabs">
<div class="tab active" data-tab="logs">日志</div>
<div class="tab" data-tab="events">事件</div>
<div class="tab" data-tab="caches">缓存</div>
<div class="tab" data-tab="performance">性能</div>
</div>
<button id="btn-refresh" type="button">刷新</button>
</div>
<div class="content">
<section id="tab-logs">
<div class="row">
<span class="muted">过滤</span>
<select id="log-level"><option value="all">全部</option><option value="info">INFO</option><option value="warn">WARN</option><option value="error">ERROR</option></select>
<span class="muted">模块</span>
<select id="log-module"><option value="all">全部</option></select>
<button id="btn-clear-logs" type="button">清空</button>
<span class="muted" id="log-count"></span>
</div>
<div class="card" id="log-list"></div>
</section>
<section id="tab-events" style="display:none">
<div class="section-collapse">
<div class="section-collapse-header" id="module-section-header">
<span class="arrow"></span>
<span class="title">模块事件监听</span>
<span class="count" id="module-count"></span>
</div>
<div class="section-collapse-content" id="module-list"></div>
</div>
<div class="row">
<span class="muted">触发历史</span>
<button id="btn-clear-events" type="button">清空历史</button>
</div>
<div class="card" id="event-list"></div>
</section>
<section id="tab-caches" style="display:none">
<div class="row">
<button id="btn-clear-all-caches" type="button">清理全部</button>
<span class="muted" id="cache-count"></span>
</div>
<div class="card" id="cache-card">
<table>
<thead><tr><th>缓存项目</th><th>条数</th><th>大小</th><th class="right">操作</th></tr></thead>
<tbody id="cache-tbody"></tbody>
</table>
<div id="cache-empty" class="empty-hint" style="display:none;">暂无缓存注册</div>
</div>
</section>
<section id="tab-performance" style="display:none">
<div class="perf-overview">
<div class="perf-stat"><span class="label">FPS</span><span class="value" id="perf-fps">--</span></div>
<div class="perf-stat" id="perf-memory-stat"><span class="label">内存</span><span class="value" id="perf-memory">--</span></div>
<div class="perf-stat"><span class="label">DOM</span><span class="value" id="perf-dom">--</span></div>
<div class="perf-stat"><span class="label">消息</span><span class="value" id="perf-messages">--</span></div>
<div class="perf-stat"><span class="label">图片</span><span class="value" id="perf-images">--</span></div>
</div>
<div class="perf-section">
<div class="perf-section-title"><span>慢请求 (≥500ms)</span><button id="btn-clear-requests" type="button">清空</button></div>
<div class="card" id="perf-requests"></div>
</div>
<div class="perf-section">
<div class="perf-section-title"><span>长任务</span><button id="btn-clear-tasks" type="button">清空</button></div>
<div class="card" id="perf-tasks"></div>
</div>
</section>
</div>
</div>
<script type="module">
const PARENT_ORIGIN = (() => {
try { return new URL(document.referrer).origin; } catch { return window.location.origin; }
})();
const post = (payload) => {
try { parent.postMessage({ source: 'LittleWhiteBox-DebugFrame', ...payload }, PARENT_ORIGIN); } catch {}
};
// ═══════════════════════════════════════════════════════════════════════
// 状态
// ═══════════════════════════════════════════════════════════════════════
const state = {
logs: [],
events: [],
eventStatsDetail: {},
caches: [],
performance: {},
openCacheDetail: null,
cacheDetails: {},
openModules: new Set(),
openLogIds: new Set(),
pendingData: null,
mouseDown: false,
};
// ═══════════════════════════════════════════════════════════════════════
// 用户交互检测 - 核心:交互时不刷新
// ═══════════════════════════════════════════════════════════════════════
document.addEventListener('mousedown', () => { state.mouseDown = true; });
document.addEventListener('mouseup', () => {
state.mouseDown = false;
// 鼠标抬起后,如果有待处理数据,延迟一点再应用(让用户完成选择)
if (state.pendingData) {
setTimeout(() => {
if (!isUserInteracting() && state.pendingData) {
applyData(state.pendingData);
state.pendingData = null;
}
}, 300);
}
});
function isUserInteracting() {
// 1. 鼠标按下中
if (state.mouseDown) return true;
// 2. 有文字被选中
const sel = document.getSelection();
if (sel && sel.toString().length > 0) return true;
// 3. 焦点在输入元素上
const active = document.activeElement;
if (active && (active.tagName === 'INPUT' || active.tagName === 'SELECT' || active.tagName === 'TEXTAREA')) return true;
return false;
}
// ═══════════════════════════════════════════════════════════════════════
// 工具函数
// ═══════════════════════════════════════════════════════════════════════
const fmtTime = (ts) => {
try {
const d = new Date(Number(ts) || Date.now());
return `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}:${String(d.getSeconds()).padStart(2,'0')}`;
} catch { return '--:--:--'; }
};
const fmtBytes = (n) => {
const v = Number(n);
if (!Number.isFinite(v) || v <= 0) return '-';
const units = ['B', 'KB', 'MB', 'GB'];
let idx = 0, x = v;
while (x >= 1024 && idx < units.length - 1) { x /= 1024; idx++; }
return `${x.toFixed(idx === 0 ? 0 : 1)} ${units[idx]}`;
};
const fmtMB = (bytes) => Number.isFinite(bytes) && bytes > 0 ? (bytes / 1048576).toFixed(0) + 'MB' : '--';
const escapeHtml = (s) => String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
// ═══════════════════════════════════════════════════════════════════════
// 日志渲染
// ═══════════════════════════════════════════════════════════════════════
function getLogFilters() {
return {
level: document.getElementById('log-level').value,
module: document.getElementById('log-module').value
};
}
function filteredLogs() {
const f = getLogFilters();
return (state.logs || []).filter(l => {
if (!l) return false;
if (f.level !== 'all' && l.level !== f.level) return false;
if (f.module !== 'all' && String(l.module) !== f.module) return false;
return true;
});
}
function renderLogModuleOptions() {
const sel = document.getElementById('log-module');
const current = sel.value || 'all';
const mods = [...new Set((state.logs || []).map(l => l?.module).filter(Boolean))].sort();
sel.innerHTML = `<option value="all">全部</option>` + mods.map(m => `<option value="${escapeHtml(m)}">${escapeHtml(m)}</option>`).join('');
if ([...sel.options].some(o => o.value === current)) sel.value = current;
}
function renderLogs() {
renderLogModuleOptions();
const logs = filteredLogs();
document.getElementById('log-count').textContent = `${logs.length}`;
const list = document.getElementById('log-list');
if (!logs.length) {
list.innerHTML = `<div class="empty-hint">暂无日志</div>`;
return;
}
// 清理已不存在的ID
const currentIds = new Set(logs.map(l => l.id));
for (const id of state.openLogIds) {
if (!currentIds.has(id)) state.openLogIds.delete(id);
}
list.innerHTML = logs.map(l => {
const lvl = escapeHtml(l.level || 'info');
const mod = escapeHtml(l.module || 'unknown');
const msg = escapeHtml(l.message || '');
const stack = l.stack ? escapeHtml(String(l.stack)) : '';
const hasStack = !!stack;
const isOpen = state.openLogIds.has(l.id);
return `<div class="log-item${isOpen ? ' open' : ''}" data-id="${l.id}">
<div class="log-header">
<span class="log-toggle${hasStack ? '' : ' empty'}" data-id="${l.id}">▶</span>
<span class="time">${fmtTime(l.timestamp)}</span>
<span class="lvl ${lvl}">[${lvl.toUpperCase()}]</span>
<span class="mod">${mod}</span>
<span class="msg">${msg}</span>
</div>
${hasStack ? `<div class="stack">${stack}</div>` : ''}
</div>`;
}).join('');
// 绑定展开事件
list.querySelectorAll('.log-toggle:not(.empty)').forEach(toggle => {
toggle.addEventListener('click', (e) => {
e.stopPropagation();
const id = Number(toggle.getAttribute('data-id'));
const item = toggle.closest('.log-item');
if (state.openLogIds.has(id)) {
state.openLogIds.delete(id);
item.classList.remove('open');
} else {
state.openLogIds.add(id);
item.classList.add('open');
}
});
});
}
// ═══════════════════════════════════════════════════════════════════════
// 事件渲染
// ═══════════════════════════════════════════════════════════════════════
function renderModuleList() {
const detail = state.eventStatsDetail || {};
const modules = Object.keys(detail).sort();
const container = document.getElementById('module-list');
const countEl = document.getElementById('module-count');
const totalListeners = Object.values(detail).reduce((sum, m) => sum + (m.total || 0), 0);
if (countEl) countEl.textContent = `${modules.length} 模块 · ${totalListeners} 监听器`;
if (!modules.length) {
container.innerHTML = `<div class="muted" style="padding:10px;">暂无模块监听</div>`;
return;
}
container.innerHTML = modules.map(mod => {
const info = detail[mod] || {};
const events = info.events || {};
const isOpen = state.openModules.has(mod);
const eventTags = Object.keys(events).sort().map(ev => {
const cnt = events[ev];
return `<span class="event-tag">${escapeHtml(ev)}${cnt > 1 ? `<span class="dup">×${cnt}</span>` : ''}</span>`;
}).join('');
return `<div class="module-section">
<div class="module-header${isOpen ? ' open' : ''}" data-mod="${escapeHtml(mod)}">
<span class="arrow">▶</span>
<span class="name">${escapeHtml(mod)}</span>
<span class="count">(${info.total || 0})</span>
</div>
<div class="module-events">${eventTags || '<span class="muted">无事件</span>'}</div>
</div>`;
}).join('');
container.querySelectorAll('.module-header').forEach(el => {
el.addEventListener('click', () => {
const mod = el.getAttribute('data-mod');
state.openModules.has(mod) ? state.openModules.delete(mod) : state.openModules.add(mod);
renderModuleList();
});
});
}
function renderEvents() {
renderModuleList();
const list = document.getElementById('event-list');
const events = state.events || [];
if (!events.length) {
list.innerHTML = `<div class="empty-hint">暂无事件历史</div>`;
return;
}
list.innerHTML = events.slice().reverse().map(e => {
const repeat = (e.repeatCount || 1) > 1 ? `<span class="repeat-badge">×${e.repeatCount}</span>` : '';
return `<div class="log-item"><div class="log-header">
<span class="time">${fmtTime(e.timestamp)}</span>
<span class="pill mono">${escapeHtml(e.type || 'CUSTOM')}</span>
<span class="mod">${escapeHtml(e.eventName || '')}</span>
${repeat}
<span class="msg">${escapeHtml(e.dataSummary || '')}</span>
</div></div>`;
}).join('');
}
// ═══════════════════════════════════════════════════════════════════════
// 缓存渲染
// ═══════════════════════════════════════════════════════════════════════
function renderCaches() {
const caches = state.caches || [];
document.getElementById('cache-count').textContent = `${caches.length}`;
const tbody = document.getElementById('cache-tbody');
const emptyHint = document.getElementById('cache-empty');
const table = tbody.closest('table');
if (!caches.length) {
table.style.display = 'none';
emptyHint.style.display = '';
return;
}
table.style.display = '';
emptyHint.style.display = 'none';
let html = '';
for (const c of caches) {
const mid = escapeHtml(c.moduleId);
const isOpen = state.openCacheDetail === c.moduleId;
const detailBtn = c.hasDetail ? `<button data-act="detail" data-mid="${mid}">${isOpen ? '收起' : '详情'}</button>` : '';
html += `<tr>
<td>${escapeHtml(c.name || c.moduleId)}<div class="muted mono">${mid}</div></td>
<td>${c.size == null ? '-' : c.size}</td>
<td>${fmtBytes(c.bytes)}</td>
<td class="right">${detailBtn}<button data-act="clear" data-mid="${mid}">清理</button></td>
</tr>`;
if (isOpen && state.cacheDetails[c.moduleId] !== undefined) {
html += `<tr class="cache-detail-row open"><td colspan="4"><div class="pre">${escapeHtml(JSON.stringify(state.cacheDetails[c.moduleId], null, 2))}</div></td></tr>`;
}
}
tbody.innerHTML = html;
tbody.querySelectorAll('button[data-act]').forEach(btn => {
btn.addEventListener('click', e => {
const act = btn.getAttribute('data-act');
const mid = btn.getAttribute('data-mid');
if (act === 'clear') {
if (confirm(`确定清理缓存:${mid}`)) post({ type: 'XB_DEBUG_ACTION', action: 'clearCache', moduleId: mid });
} else if (act === 'detail') {
state.openCacheDetail = state.openCacheDetail === mid ? null : mid;
if (state.openCacheDetail) post({ type: 'XB_DEBUG_ACTION', action: 'cacheDetail', moduleId: mid });
else renderCaches();
}
});
});
}
// ═══════════════════════════════════════════════════════════════════════
// 性能渲染
// ═══════════════════════════════════════════════════════════════════════
function renderPerformance() {
const perf = state.performance || {};
const fps = perf.fps || 0;
const fpsEl = document.getElementById('perf-fps');
fpsEl.textContent = fps > 0 ? fps : '--';
fpsEl.className = 'value' + (fps >= 50 ? ' good' : fps >= 30 ? ' warn' : fps > 0 ? ' bad' : '');
const memEl = document.getElementById('perf-memory');
const memStat = document.getElementById('perf-memory-stat');
if (perf.memory) {
const pct = perf.memory.total > 0 ? (perf.memory.used / perf.memory.total * 100) : 0;
memEl.textContent = fmtMB(perf.memory.used);
memEl.className = 'value' + (pct < 60 ? ' good' : pct < 80 ? ' warn' : ' bad');
memStat.style.display = '';
} else {
memStat.style.display = 'none';
}
const dom = perf.domCount || 0;
const domEl = document.getElementById('perf-dom');
domEl.textContent = dom.toLocaleString();
domEl.className = 'value' + (dom < 3000 ? ' good' : dom < 6000 ? ' warn' : ' bad');
const msg = perf.messageCount || 0;
document.getElementById('perf-messages').textContent = msg;
document.getElementById('perf-messages').className = 'value' + (msg < 100 ? ' good' : msg < 300 ? ' warn' : ' bad');
const img = perf.imageCount || 0;
document.getElementById('perf-images').textContent = img;
document.getElementById('perf-images').className = 'value' + (img < 50 ? ' good' : img < 100 ? ' warn' : ' bad');
const reqContainer = document.getElementById('perf-requests');
const requests = perf.requests || [];
reqContainer.innerHTML = requests.length ? requests.slice().reverse().map(r => {
const durClass = r.duration >= 2000 ? 'very-slow' : r.duration >= 1000 ? 'slow' : '';
return `<div class="perf-item"><div class="top"><span class="time">${fmtTime(r.timestamp)}</span><span class="pill mono">${escapeHtml(r.method)}</span><span class="url">${escapeHtml(r.url)}</span><span class="duration ${durClass}">${r.duration}ms</span></div></div>`;
}).join('') : `<div class="empty-hint">暂无慢请求</div>`;
const taskContainer = document.getElementById('perf-tasks');
const tasks = perf.longTasks || [];
taskContainer.innerHTML = tasks.length ? tasks.slice().reverse().map(t => {
const durClass = t.duration >= 200 ? 'very-slow' : t.duration >= 100 ? 'slow' : '';
return `<div class="perf-item"><div class="top"><span class="time">${fmtTime(t.timestamp)}</span><span class="duration ${durClass}">${t.duration}ms</span><span class="muted">${escapeHtml(t.source || '未知')}</span></div></div>`;
}).join('') : `<div class="empty-hint">暂无长任务</div>`;
}
// ═══════════════════════════════════════════════════════════════════════
// Tab 切换
// ═══════════════════════════════════════════════════════════════════════
function switchTab(tab) {
document.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.tab === tab));
['logs', 'events', 'caches', 'performance'].forEach(name => {
document.getElementById(`tab-${name}`).style.display = name === tab ? '' : 'none';
});
}
// ═══════════════════════════════════════════════════════════════════════
// 数据应用
// ═══════════════════════════════════════════════════════════════════════
function applyData(payload) {
state.logs = payload?.logs || [];
state.events = payload?.events || [];
state.eventStatsDetail = payload?.eventStatsDetail || {};
state.caches = payload?.caches || [];
state.performance = payload?.performance || {};
renderLogs();
renderEvents();
renderCaches();
renderPerformance();
}
// ═══════════════════════════════════════════════════════════════════════
// 事件绑定
// ═══════════════════════════════════════════════════════════════════════
document.getElementById('btn-refresh').addEventListener('click', () => post({ type: 'XB_DEBUG_ACTION', action: 'refresh' }));
document.getElementById('btn-clear-logs').addEventListener('click', () => {
if (confirm('确定清空日志?')) {
state.openLogIds.clear();
post({ type: 'XB_DEBUG_ACTION', action: 'clearLogs' });
}
});
document.getElementById('btn-clear-events').addEventListener('click', () => {
if (confirm('确定清空事件历史?')) post({ type: 'XB_DEBUG_ACTION', action: 'clearEvents' });
});
document.getElementById('btn-clear-all-caches').addEventListener('click', () => {
if (confirm('确定清理全部缓存?')) post({ type: 'XB_DEBUG_ACTION', action: 'clearAllCaches' });
});
document.getElementById('btn-clear-requests').addEventListener('click', () => post({ type: 'XB_DEBUG_ACTION', action: 'clearRequests' }));
document.getElementById('btn-clear-tasks').addEventListener('click', () => post({ type: 'XB_DEBUG_ACTION', action: 'clearTasks' }));
document.getElementById('log-level').addEventListener('change', renderLogs);
document.getElementById('log-module').addEventListener('change', renderLogs);
document.querySelectorAll('.tab').forEach(t => t.addEventListener('click', () => switchTab(t.dataset.tab)));
document.getElementById('module-section-header')?.addEventListener('click', function() { this.classList.toggle('open'); });
// ═══════════════════════════════════════════════════════════════════════
// 消息监听
// ═══════════════════════════════════════════════════════════════════════
window.addEventListener('message', (event) => {
if (event.origin !== PARENT_ORIGIN || event.source !== parent) return;
const msg = event?.data;
if (!msg || msg.source !== 'LittleWhiteBox-DebugHost') return;
if (msg.type === 'XB_DEBUG_DATA') {
// 核心逻辑用户交互时暂存数据不刷新DOM
if (isUserInteracting()) {
state.pendingData = msg.payload;
} else {
applyData(msg.payload);
state.pendingData = null;
}
}
if (msg.type === 'XB_DEBUG_CACHE_DETAIL') {
const mid = msg.payload?.moduleId;
if (mid) {
state.cacheDetails[mid] = msg.payload?.detail;
renderCaches();
}
}
});
post({ type: 'FRAME_READY' });
</script>
</body>
</html>

View File

@@ -0,0 +1,748 @@
// ═══════════════════════════════════════════════════════════════════════════
// 导入和常量
// ═══════════════════════════════════════════════════════════════════════════
import { extensionFolderPath } from "../../core/constants.js";
import { postToIframe, isTrustedMessage } from "../../core/iframe-messaging.js";
const STORAGE_EXPANDED_KEY = "xiaobaix_debug_panel_pos_v2";
const STORAGE_MINI_KEY = "xiaobaix_debug_panel_minipos_v2";
// ═══════════════════════════════════════════════════════════════════════════
// 状态变量
// ═══════════════════════════════════════════════════════════════════════════
let isOpen = false;
let isExpanded = false;
let panelEl = null;
let miniBtnEl = null;
let iframeEl = null;
let dragState = null;
let pollTimer = null;
let lastLogId = 0;
let frameReady = false;
let messageListenerBound = false;
let resizeHandler = null;
// ═══════════════════════════════════════════════════════════════════════════
// 性能监控状态
// ═══════════════════════════════════════════════════════════════════════════
let perfMonitorActive = false;
let originalFetch = null;
let longTaskObserver = null;
let fpsFrameId = null;
let lastFrameTime = 0;
let frameCount = 0;
let currentFps = 0;
const requestLog = [];
const longTaskLog = [];
const MAX_PERF_LOG = 50;
const SLOW_REQUEST_THRESHOLD = 500;
// ═══════════════════════════════════════════════════════════════════════════
// 工具函数
// ═══════════════════════════════════════════════════════════════════════════
const clamp = (n, min, max) => Math.max(min, Math.min(max, n));
const isMobile = () => window.innerWidth <= 768;
const countErrors = (logs) => (logs || []).filter(l => l?.level === "error").length;
const maxLogId = (logs) => (logs || []).reduce((m, l) => Math.max(m, Number(l?.id) || 0), 0);
// ═══════════════════════════════════════════════════════════════════════════
// 存储
// ═══════════════════════════════════════════════════════════════════════════
function readJSON(key) {
try {
const raw = localStorage.getItem(key);
return raw ? JSON.parse(raw) : null;
} catch { return null; }
}
function writeJSON(key, data) {
try { localStorage.setItem(key, JSON.stringify(data)); } catch {}
}
// ═══════════════════════════════════════════════════════════════════════════
// 页面统计
// ═══════════════════════════════════════════════════════════════════════════
function getPageStats() {
try {
return {
domCount: document.querySelectorAll('*').length,
messageCount: document.querySelectorAll('.mes').length,
imageCount: document.querySelectorAll('img').length
};
} catch {
return { domCount: 0, messageCount: 0, imageCount: 0 };
}
}
// ═══════════════════════════════════════════════════════════════════════════
// 性能监控Fetch 拦截
// ═══════════════════════════════════════════════════════════════════════════
function startFetchInterceptor() {
if (originalFetch) return;
originalFetch = window.fetch;
window.fetch = async function(input, init) {
const url = typeof input === 'string' ? input : input?.url || '';
const method = init?.method || 'GET';
const startTime = performance.now();
const timestamp = Date.now();
try {
const response = await originalFetch.apply(this, arguments);
const duration = performance.now() - startTime;
if (url.includes('/api/') && duration >= SLOW_REQUEST_THRESHOLD) {
requestLog.push({ url, method, duration: Math.round(duration), timestamp, status: response.status });
if (requestLog.length > MAX_PERF_LOG) requestLog.shift();
}
return response;
} catch (err) {
const duration = performance.now() - startTime;
requestLog.push({ url, method, duration: Math.round(duration), timestamp, status: 'error' });
if (requestLog.length > MAX_PERF_LOG) requestLog.shift();
throw err;
}
};
}
function stopFetchInterceptor() {
if (originalFetch) {
window.fetch = originalFetch;
originalFetch = null;
}
}
// ═══════════════════════════════════════════════════════════════════════════
// 性能监控:长任务检测
// ═══════════════════════════════════════════════════════════════════════════
function startLongTaskObserver() {
if (longTaskObserver) return;
try {
if (typeof PerformanceObserver === 'undefined') return;
longTaskObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration >= 200) {
let source = '主页面';
try {
const attr = entry.attribution?.[0];
if (attr) {
if (attr.containerType === 'iframe') {
source = 'iframe';
if (attr.containerSrc) {
const url = new URL(attr.containerSrc, location.href);
source += `: ${url.pathname.split('/').pop() || url.pathname}`;
}
} else if (attr.containerName) {
source = attr.containerName;
}
}
} catch {}
longTaskLog.push({
duration: Math.round(entry.duration),
timestamp: Date.now(),
source
});
if (longTaskLog.length > MAX_PERF_LOG) longTaskLog.shift();
}
}
});
longTaskObserver.observe({ entryTypes: ['longtask'] });
} catch {}
}
function stopLongTaskObserver() {
if (longTaskObserver) {
try { longTaskObserver.disconnect(); } catch {}
longTaskObserver = null;
}
}
// ═══════════════════════════════════════════════════════════════════════════
// 性能监控FPS 计算
// ═══════════════════════════════════════════════════════════════════════════
function startFpsMonitor() {
if (fpsFrameId) return;
lastFrameTime = performance.now();
frameCount = 0;
const loop = (now) => {
frameCount++;
if (now - lastFrameTime >= 1000) {
currentFps = frameCount;
frameCount = 0;
lastFrameTime = now;
}
fpsFrameId = requestAnimationFrame(loop);
};
fpsFrameId = requestAnimationFrame(loop);
}
function stopFpsMonitor() {
if (fpsFrameId) {
cancelAnimationFrame(fpsFrameId);
fpsFrameId = null;
}
currentFps = 0;
}
// ═══════════════════════════════════════════════════════════════════════════
// 性能监控:内存
// ═══════════════════════════════════════════════════════════════════════════
function getMemoryInfo() {
if (typeof performance === 'undefined' || !performance.memory) return null;
const mem = performance.memory;
return {
used: mem.usedJSHeapSize,
total: mem.totalJSHeapSize,
limit: mem.jsHeapSizeLimit
};
}
// ═══════════════════════════════════════════════════════════════════════════
// 性能监控:生命周期
// ═══════════════════════════════════════════════════════════════════════════
function startPerfMonitor() {
if (perfMonitorActive) return;
perfMonitorActive = true;
startFetchInterceptor();
startLongTaskObserver();
startFpsMonitor();
}
function stopPerfMonitor() {
if (!perfMonitorActive) return;
perfMonitorActive = false;
stopFetchInterceptor();
stopLongTaskObserver();
stopFpsMonitor();
requestLog.length = 0;
longTaskLog.length = 0;
}
// ═══════════════════════════════════════════════════════════════════════════
// 样式注入
// ═══════════════════════════════════════════════════════════════════════════
function ensureStyle() {
if (document.getElementById("xiaobaix-debug-style")) return;
const style = document.createElement("style");
style.id = "xiaobaix-debug-style";
style.textContent = `
#xiaobaix-debug-btn {
display: inline-flex !important;
align-items: center !important;
gap: 6px !important;
}
#xiaobaix-debug-btn .dbg-light {
width: 8px;
height: 8px;
border-radius: 50%;
background: #555;
flex-shrink: 0;
transition: background 0.2s, box-shadow 0.2s;
}
#xiaobaix-debug-btn .dbg-light.on {
background: #4ade80;
box-shadow: 0 0 6px #4ade80;
}
#xiaobaix-debug-mini {
position: fixed;
z-index: 10000;
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: rgba(28, 28, 32, 0.96);
border: 1px solid rgba(255,255,255,0.12);
border-radius: 8px;
color: rgba(255,255,255,0.9);
font-size: 12px;
cursor: pointer;
user-select: none;
touch-action: none;
box-shadow: 0 4px 14px rgba(0,0,0,0.35);
transition: box-shadow 0.2s;
}
#xiaobaix-debug-mini:hover {
box-shadow: 0 6px 18px rgba(0,0,0,0.45);
}
#xiaobaix-debug-mini .badge {
padding: 2px 6px;
border-radius: 999px;
background: rgba(255,80,80,0.18);
border: 1px solid rgba(255,80,80,0.35);
color: #fca5a5;
font-size: 10px;
}
#xiaobaix-debug-mini .badge.hidden { display: none; }
#xiaobaix-debug-mini.flash {
animation: xbdbg-flash 0.35s ease-in-out 2;
}
@keyframes xbdbg-flash {
0%,100% { box-shadow: 0 4px 14px rgba(0,0,0,0.35); }
50% { box-shadow: 0 0 0 4px rgba(255,80,80,0.4); }
}
#xiaobaix-debug-panel {
position: fixed;
z-index: 10001;
background: rgba(22,22,26,0.97);
border: 1px solid rgba(255,255,255,0.10);
border-radius: 10px;
box-shadow: 0 12px 36px rgba(0,0,0,0.5);
display: flex;
flex-direction: column;
overflow: hidden;
}
@media (min-width: 769px) {
#xiaobaix-debug-panel {
resize: both;
min-width: 320px;
min-height: 260px;
}
}
@media (max-width: 768px) {
#xiaobaix-debug-panel {
left: 0 !important;
right: 0 !important;
top: 0 !important;
width: 100% !important;
border-radius: 0;
resize: none;
}
}
#xiaobaix-debug-titlebar {
user-select: none;
padding: 8px 10px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
background: rgba(30,30,34,0.98);
border-bottom: 1px solid rgba(255,255,255,0.08);
flex-shrink: 0;
}
@media (min-width: 769px) {
#xiaobaix-debug-titlebar { cursor: move; }
}
#xiaobaix-debug-titlebar .left {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: rgba(255,255,255,0.88);
}
#xiaobaix-debug-titlebar .right {
display: flex;
align-items: center;
gap: 6px;
}
.xbdbg-btn {
width: 28px;
height: 24px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 6px;
border: 1px solid rgba(255,255,255,0.10);
background: rgba(255,255,255,0.05);
color: rgba(255,255,255,0.85);
cursor: pointer;
font-size: 12px;
transition: background 0.15s;
}
.xbdbg-btn:hover { background: rgba(255,255,255,0.12); }
#xiaobaix-debug-frame {
flex: 1;
border: 0;
width: 100%;
background: transparent;
}
`;
document.head.appendChild(style);
}
// ═══════════════════════════════════════════════════════════════════════════
// 定位计算
// ═══════════════════════════════════════════════════════════════════════════
function getAnchorRect() {
const anchor = document.getElementById("nonQRFormItems");
if (anchor) return anchor.getBoundingClientRect();
return { top: window.innerHeight - 60, right: window.innerWidth, left: 0, width: window.innerWidth };
}
function getDefaultMiniPos() {
const rect = getAnchorRect();
const btnW = 90, btnH = 32, margin = 8;
return { left: rect.right - btnW - margin, top: rect.top - btnH - margin };
}
function applyMiniPosition() {
if (!miniBtnEl) return;
const saved = readJSON(STORAGE_MINI_KEY);
const def = getDefaultMiniPos();
const pos = saved || def;
const w = miniBtnEl.offsetWidth || 90;
const h = miniBtnEl.offsetHeight || 32;
miniBtnEl.style.left = `${clamp(pos.left, 0, window.innerWidth - w)}px`;
miniBtnEl.style.top = `${clamp(pos.top, 0, window.innerHeight - h)}px`;
}
function saveMiniPos() {
if (!miniBtnEl) return;
const r = miniBtnEl.getBoundingClientRect();
writeJSON(STORAGE_MINI_KEY, { left: Math.round(r.left), top: Math.round(r.top) });
}
function applyExpandedPosition() {
if (!panelEl) return;
if (isMobile()) {
const rect = getAnchorRect();
panelEl.style.left = "0";
panelEl.style.top = "0";
panelEl.style.width = "100%";
panelEl.style.height = `${rect.top}px`;
return;
}
const saved = readJSON(STORAGE_EXPANDED_KEY);
const defW = 480, defH = 400;
const w = saved?.width >= 320 ? saved.width : defW;
const h = saved?.height >= 260 ? saved.height : defH;
const left = saved?.left != null ? clamp(saved.left, 0, window.innerWidth - w) : 20;
const top = saved?.top != null ? clamp(saved.top, 0, window.innerHeight - h) : 80;
panelEl.style.left = `${left}px`;
panelEl.style.top = `${top}px`;
panelEl.style.width = `${w}px`;
panelEl.style.height = `${h}px`;
}
function saveExpandedPos() {
if (!panelEl || isMobile()) return;
const r = panelEl.getBoundingClientRect();
writeJSON(STORAGE_EXPANDED_KEY, { left: Math.round(r.left), top: Math.round(r.top), width: Math.round(r.width), height: Math.round(r.height) });
}
// ═══════════════════════════════════════════════════════════════════════════
// 数据获取与通信
// ═══════════════════════════════════════════════════════════════════════════
async function getDebugSnapshot() {
const { xbLog, CacheRegistry } = await import("../../core/debug-core.js");
const { EventCenter } = await import("../../core/event-manager.js");
const pageStats = getPageStats();
return {
logs: xbLog.getAll(),
events: EventCenter.getEventHistory?.() || [],
eventStatsDetail: EventCenter.statsDetail?.() || {},
caches: CacheRegistry.getStats(),
performance: {
requests: requestLog.slice(),
longTasks: longTaskLog.slice(),
fps: currentFps,
memory: getMemoryInfo(),
domCount: pageStats.domCount,
messageCount: pageStats.messageCount,
imageCount: pageStats.imageCount
}
};
}
function postToFrame(msg) {
try { postToIframe(iframeEl, { ...msg }, "LittleWhiteBox-DebugHost"); } catch {}
}
async function sendSnapshotToFrame() {
if (!frameReady) return;
const snapshot = await getDebugSnapshot();
postToFrame({ type: "XB_DEBUG_DATA", payload: snapshot });
updateMiniBadge(snapshot.logs);
}
async function handleAction(action) {
const { xbLog, CacheRegistry } = await import("../../core/debug-core.js");
const { EventCenter } = await import("../../core/event-manager.js");
switch (action?.action) {
case "refresh": await sendSnapshotToFrame(); break;
case "clearLogs": xbLog.clear(); await sendSnapshotToFrame(); break;
case "clearEvents": EventCenter.clearHistory?.(); await sendSnapshotToFrame(); break;
case "clearCache": if (action.moduleId) CacheRegistry.clear(action.moduleId); await sendSnapshotToFrame(); break;
case "clearAllCaches": CacheRegistry.clearAll(); await sendSnapshotToFrame(); break;
case "clearRequests": requestLog.length = 0; await sendSnapshotToFrame(); break;
case "clearTasks": longTaskLog.length = 0; await sendSnapshotToFrame(); break;
case "cacheDetail":
postToFrame({ type: "XB_DEBUG_CACHE_DETAIL", payload: { moduleId: action.moduleId, detail: CacheRegistry.getDetail(action.moduleId) } });
break;
case "exportLogs":
postToFrame({ type: "XB_DEBUG_EXPORT", payload: { text: xbLog.export() } });
break;
}
}
function bindMessageListener() {
if (messageListenerBound) return;
messageListenerBound = true;
// eslint-disable-next-line no-restricted-syntax
window.addEventListener("message", async (e) => {
// Guarded by isTrustedMessage (origin + source).
if (!isTrustedMessage(e, iframeEl, "LittleWhiteBox-DebugFrame")) return;
const msg = e?.data;
if (msg.type === "FRAME_READY") { frameReady = true; await sendSnapshotToFrame(); }
else if (msg.type === "XB_DEBUG_ACTION") await handleAction(msg);
else if (msg.type === "CLOSE_PANEL") closeDebugPanel();
});
}
// ═══════════════════════════════════════════════════════════════════════════
// UI 更新
// ═══════════════════════════════════════════════════════════════════════════
function updateMiniBadge(logs) {
if (!miniBtnEl) return;
const badge = miniBtnEl.querySelector(".badge");
if (!badge) return;
const errCount = countErrors(logs);
badge.classList.toggle("hidden", errCount <= 0);
badge.textContent = errCount > 0 ? String(errCount) : "";
const newMax = maxLogId(logs);
if (newMax > lastLogId && !isExpanded) {
miniBtnEl.classList.remove("flash");
// Force reflow to restart animation.
// eslint-disable-next-line no-unused-expressions
miniBtnEl.offsetWidth;
miniBtnEl.classList.add("flash");
}
lastLogId = newMax;
}
function updateSettingsLight() {
const light = document.querySelector("#xiaobaix-debug-btn .dbg-light");
if (light) light.classList.toggle("on", isOpen);
}
// ═══════════════════════════════════════════════════════════════════════════
// 拖拽:最小化按钮
// ═══════════════════════════════════════════════════════════════════════════
function onMiniDown(e) {
if (e.button !== undefined && e.button !== 0) return;
dragState = {
startX: e.clientX, startY: e.clientY,
startLeft: miniBtnEl.getBoundingClientRect().left,
startTop: miniBtnEl.getBoundingClientRect().top,
pointerId: e.pointerId, moved: false
};
try { e.currentTarget.setPointerCapture(e.pointerId); } catch {}
e.preventDefault();
}
function onMiniMove(e) {
if (!dragState || dragState.pointerId !== e.pointerId) return;
const dx = e.clientX - dragState.startX, dy = e.clientY - dragState.startY;
if (Math.abs(dx) > 3 || Math.abs(dy) > 3) dragState.moved = true;
const w = miniBtnEl.offsetWidth || 90, h = miniBtnEl.offsetHeight || 32;
miniBtnEl.style.left = `${clamp(dragState.startLeft + dx, 0, window.innerWidth - w)}px`;
miniBtnEl.style.top = `${clamp(dragState.startTop + dy, 0, window.innerHeight - h)}px`;
e.preventDefault();
}
function onMiniUp(e) {
if (!dragState || dragState.pointerId !== e.pointerId) return;
const wasMoved = dragState.moved;
try { e.currentTarget.releasePointerCapture(e.pointerId); } catch {}
dragState = null;
saveMiniPos();
if (!wasMoved) expandPanel();
}
// ═══════════════════════════════════════════════════════════════════════════
// 拖拽:展开面板标题栏
// ═══════════════════════════════════════════════════════════════════════════
function onTitleDown(e) {
if (isMobile()) return;
if (e.button !== undefined && e.button !== 0) return;
if (e.target?.closest?.(".xbdbg-btn")) return;
dragState = {
startX: e.clientX, startY: e.clientY,
startLeft: panelEl.getBoundingClientRect().left,
startTop: panelEl.getBoundingClientRect().top,
pointerId: e.pointerId
};
try { e.currentTarget.setPointerCapture(e.pointerId); } catch {}
e.preventDefault();
}
function onTitleMove(e) {
if (!dragState || isMobile() || dragState.pointerId !== e.pointerId) return;
const dx = e.clientX - dragState.startX, dy = e.clientY - dragState.startY;
const w = panelEl.offsetWidth, h = panelEl.offsetHeight;
panelEl.style.left = `${clamp(dragState.startLeft + dx, 0, window.innerWidth - w)}px`;
panelEl.style.top = `${clamp(dragState.startTop + dy, 0, window.innerHeight - h)}px`;
e.preventDefault();
}
function onTitleUp(e) {
if (!dragState || isMobile() || dragState.pointerId !== e.pointerId) return;
try { e.currentTarget.releasePointerCapture(e.pointerId); } catch {}
dragState = null;
saveExpandedPos();
}
// ═══════════════════════════════════════════════════════════════════════════
// 轮询与 resize
// ═══════════════════════════════════════════════════════════════════════════
function startPoll() {
stopPoll();
pollTimer = setInterval(async () => {
if (!isOpen) return;
try { await sendSnapshotToFrame(); } catch {}
}, 1500);
}
function stopPoll() {
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
}
function onResize() {
if (!isOpen) return;
if (isExpanded) applyExpandedPosition();
else applyMiniPosition();
}
// ═══════════════════════════════════════════════════════════════════════════
// 面板生命周期
// ═══════════════════════════════════════════════════════════════════════════
function createMiniButton() {
if (miniBtnEl) return;
miniBtnEl = document.createElement("div");
miniBtnEl.id = "xiaobaix-debug-mini";
miniBtnEl.innerHTML = `<span>监控</span><span class="badge hidden"></span>`;
document.body.appendChild(miniBtnEl);
applyMiniPosition();
miniBtnEl.addEventListener("pointerdown", onMiniDown, { passive: false });
miniBtnEl.addEventListener("pointermove", onMiniMove, { passive: false });
miniBtnEl.addEventListener("pointerup", onMiniUp, { passive: false });
miniBtnEl.addEventListener("pointercancel", onMiniUp, { passive: false });
}
function removeMiniButton() {
miniBtnEl?.remove();
miniBtnEl = null;
}
function createPanel() {
if (panelEl) return;
panelEl = document.createElement("div");
panelEl.id = "xiaobaix-debug-panel";
const titlebar = document.createElement("div");
titlebar.id = "xiaobaix-debug-titlebar";
titlebar.innerHTML = `
<div class="left"><span>小白X 监控台</span></div>
<div class="right">
<button class="xbdbg-btn" id="xbdbg-min" title="最小化" type="button">—</button>
<button class="xbdbg-btn" id="xbdbg-close" title="关闭" type="button">×</button>
</div>
`;
iframeEl = document.createElement("iframe");
iframeEl.id = "xiaobaix-debug-frame";
iframeEl.src = `${extensionFolderPath}/modules/debug-panel/debug-panel.html`;
panelEl.appendChild(titlebar);
panelEl.appendChild(iframeEl);
document.body.appendChild(panelEl);
applyExpandedPosition();
titlebar.addEventListener("pointerdown", onTitleDown, { passive: false });
titlebar.addEventListener("pointermove", onTitleMove, { passive: false });
titlebar.addEventListener("pointerup", onTitleUp, { passive: false });
titlebar.addEventListener("pointercancel", onTitleUp, { passive: false });
panelEl.querySelector("#xbdbg-min")?.addEventListener("click", collapsePanel);
panelEl.querySelector("#xbdbg-close")?.addEventListener("click", closeDebugPanel);
if (!isMobile()) {
panelEl.addEventListener("mouseup", saveExpandedPos);
panelEl.addEventListener("mouseleave", saveExpandedPos);
}
frameReady = false;
}
function removePanel() {
panelEl?.remove();
panelEl = null;
iframeEl = null;
frameReady = false;
}
function expandPanel() {
if (isExpanded) return;
isExpanded = true;
if (miniBtnEl) miniBtnEl.style.display = "none";
if (panelEl) {
panelEl.style.display = "";
} else {
createPanel();
}
}
function collapsePanel() {
if (!isExpanded) return;
isExpanded = false;
saveExpandedPos();
if (panelEl) panelEl.style.display = "none";
if (miniBtnEl) {
miniBtnEl.style.display = "";
applyMiniPosition();
}
}
async function openDebugPanel() {
if (isOpen) return;
isOpen = true;
ensureStyle();
bindMessageListener();
const { enableDebugMode } = await import("../../core/debug-core.js");
enableDebugMode();
startPerfMonitor();
createMiniButton();
startPoll();
updateSettingsLight();
if (!resizeHandler) { resizeHandler = onResize; window.addEventListener("resize", resizeHandler); }
try { window.registerModuleCleanup?.("debugPanel", closeDebugPanel); } catch {}
}
async function closeDebugPanel() {
if (!isOpen) return;
isOpen = false;
isExpanded = false;
stopPoll();
stopPerfMonitor();
frameReady = false;
lastLogId = 0;
try { const { disableDebugMode } = await import("../../core/debug-core.js"); disableDebugMode(); } catch {}
removePanel();
removeMiniButton();
updateSettingsLight();
}
// ═══════════════════════════════════════════════════════════════════════════
// 导出
// ═══════════════════════════════════════════════════════════════════════════
export async function toggleDebugPanel() {
if (isOpen) await closeDebugPanel();
else await openDebugPanel();
}
export { openDebugPanel as openDebugPanelExplicit, closeDebugPanel as closeDebugPanelExplicit };
if (typeof window !== "undefined") {
window.xbDebugPanelToggle = toggleDebugPanel;
window.xbDebugPanelClose = closeDebugPanel;
}