fl-web-component 1.3.7 → 1.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,309 @@
1
+ // ========= Error & Console Panel (非阻塞右下角显示,包含 console.log 等) =========
2
+ export default function installErrorConsolePanel() {
3
+ // ========== 可配置项 ==========
4
+ const MAX_ENTRIES = 400;
5
+ const PANEL_ID = 'js-error-panel';
6
+ const COLLAPSE_LENGTH = 1200; // 超过多少字符时折叠显示
7
+ const CAPTURE_CONSOLE = true; // 是否捕获 console.log/info/warn/debug(保留原行为)
8
+ // ================================
9
+
10
+ // 安全 stringify(处理循环引用)
11
+ function getCircularReplacer() {
12
+ const seen = new WeakSet();
13
+ return (key, value) => {
14
+ if (typeof value === "object" && value !== null) {
15
+ if (seen.has(value)) return "[Circular]";
16
+ seen.add(value);
17
+ }
18
+ return value;
19
+ };
20
+ }
21
+ function safeStringify(x) {
22
+ try {
23
+ if (typeof x === "string") return x;
24
+ if (x instanceof Error) return x.stack || x.message || String(x);
25
+ return JSON.stringify(x, getCircularReplacer(), 2);
26
+ } catch (e) {
27
+ try { return String(x); } catch (e2) { return "[unstringifiable]"; }
28
+ }
29
+ }
30
+
31
+ // ========== Panel DOM 创建 ==========
32
+ function createPanel() {
33
+ if (document.getElementById(PANEL_ID)) return document.getElementById(PANEL_ID);
34
+
35
+ const panel = document.createElement('div');
36
+ panel.id = PANEL_ID;
37
+ Object.assign(panel.style, {
38
+ position: 'fixed',
39
+ right: '12px',
40
+ bottom: '12px',
41
+ width: '460px',
42
+ maxHeight: '45vh',
43
+ boxSizing: 'border-box',
44
+ overflow: 'hidden',
45
+ zIndex: 2147483647,
46
+ fontFamily: 'Menlo,Consolas,monospace',
47
+ fontSize: '12px',
48
+ color: '#fff',
49
+ borderRadius: '8px',
50
+ boxShadow: '0 6px 18px rgba(0,0,0,0.55)',
51
+ background: 'linear-gradient(180deg, rgba(0,0,0,0.86), rgba(24,24,24,0.86))',
52
+ display: 'flex',
53
+ flexDirection: 'column',
54
+ gap: '6px',
55
+ padding: '8px'
56
+ });
57
+
58
+ // header
59
+ const header = document.createElement('div');
60
+ Object.assign(header.style, { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '8px' });
61
+ header.innerHTML = `<strong style="font-size:13px">JS Logs / Errors</strong><span style="opacity:.7;font-size:11px">(非阻塞)</span>`;
62
+
63
+ const btns = document.createElement('div');
64
+
65
+ const clearBtn = document.createElement('button');
66
+ clearBtn.textContent = '清空';
67
+ Object.assign(clearBtn.style, { marginLeft: '6px', cursor: 'pointer', fontSize: '12px' });
68
+ clearBtn.onclick = () => { entriesContainer.innerHTML = ''; entryCount = 0; updateStats(); };
69
+
70
+ const toggleBtn = document.createElement('button');
71
+ toggleBtn.textContent = '隐藏';
72
+ Object.assign(toggleBtn.style, { cursor: 'pointer', fontSize: '12px' });
73
+ toggleBtn.onclick = () => {
74
+ if (entriesContainer.style.display === 'none') { entriesContainer.style.display = ''; toggleBtn.textContent = '隐藏'; }
75
+ else { entriesContainer.style.display = 'none'; toggleBtn.textContent = '显示'; }
76
+ };
77
+
78
+ const statsSpan = document.createElement('span');
79
+ statsSpan.style.opacity = '.8';
80
+ statsSpan.style.fontSize = '11px';
81
+ statsSpan.textContent = '0 条';
82
+
83
+ btns.appendChild(statsSpan);
84
+ btns.appendChild(toggleBtn);
85
+ btns.appendChild(clearBtn);
86
+ header.appendChild(btns);
87
+
88
+ // entries container (scrollable)
89
+ const entriesContainer = document.createElement('div');
90
+ Object.assign(entriesContainer.style, {
91
+ overflowY: 'auto',
92
+ padding: '6px',
93
+ borderRadius: '6px',
94
+ background: 'rgba(0,0,0,0.32)',
95
+ maxHeight: 'calc(45vh - 48px)',
96
+ boxSizing: 'border-box'
97
+ });
98
+
99
+ panel.appendChild(header);
100
+ panel.appendChild(entriesContainer);
101
+ document.body.appendChild(panel);
102
+
103
+ // helper to update stats
104
+ function updateStats() {
105
+ statsSpan.textContent = `${entryCount} 条`;
106
+ }
107
+
108
+ panel._entriesContainer = entriesContainer;
109
+ panel._statsUpdater = updateStats;
110
+ return panel;
111
+ }
112
+
113
+ // ========== 添加条目 ==========
114
+ let entryCount = 0;
115
+ function addEntry({ type = 'log', message = '', time = new Date().toLocaleString(), meta = null }) {
116
+ const panel = createPanel();
117
+ const entriesContainer = panel._entriesContainer;
118
+ const updateStats = panel._statsUpdater;
119
+
120
+ // 限制数量
121
+ if (entryCount >= MAX_ENTRIES) {
122
+ const first = entriesContainer.firstChild;
123
+ if (first) entriesContainer.removeChild(first);
124
+ entryCount--;
125
+ }
126
+
127
+ const entry = document.createElement('div');
128
+ Object.assign(entry.style, {
129
+ padding: '6px',
130
+ marginBottom: '6px',
131
+ borderRadius: '6px',
132
+ background: 'rgba(255,255,255,0.03)',
133
+ });
134
+
135
+ const header = document.createElement('div');
136
+ header.style.display = 'flex';
137
+ header.style.justifyContent = 'space-between';
138
+ header.style.alignItems = 'center';
139
+ header.style.gap = '8px';
140
+
141
+ const left = document.createElement('div');
142
+ // 彩色 type 标签
143
+ const colorMap = {
144
+ error: '#ff6b6b',
145
+ 'window.onerror': '#ff6b6b',
146
+ resource: '#ff9966',
147
+ 'unhandledrejection': '#ff8c66',
148
+ 'console.error': '#ff6b6b',
149
+ 'console.warn': '#ffd966',
150
+ 'console.info': '#66bfff',
151
+ 'console.log': '#bfbfbf',
152
+ 'console.debug': '#a0a0a0',
153
+ 'Vue.error (2)': '#ff6b6b',
154
+ 'Vue.error (3)': '#ff6b6b'
155
+ };
156
+ const tagColor = colorMap[type] || '#bfbfbf';
157
+ left.innerHTML = `<span style="font-weight:700;color:${tagColor}">${type}</span> <span style="opacity:.7;font-size:11px"> ${time}</span>`;
158
+
159
+ const right = document.createElement('div');
160
+ right.style.display = 'flex';
161
+ right.style.gap = '6px';
162
+
163
+ const copyBtn = document.createElement('button');
164
+ copyBtn.textContent = '复制';
165
+ Object.assign(copyBtn.style, { cursor: 'pointer', fontSize: '11px' });
166
+
167
+ const expandBtn = document.createElement('button');
168
+ expandBtn.textContent = '展开';
169
+ Object.assign(expandBtn.style, { cursor: 'pointer', fontSize: '11px' });
170
+
171
+ right.appendChild(copyBtn);
172
+ right.appendChild(expandBtn);
173
+ header.appendChild(left);
174
+ header.appendChild(right);
175
+
176
+ const content = document.createElement('pre');
177
+ Object.assign(content.style, {
178
+ whiteSpace: 'pre-wrap',
179
+ wordBreak: 'break-word',
180
+ margin: '6px 0 0 0',
181
+ maxHeight: '240px',
182
+ overflow: 'auto',
183
+ fontSize: '12px',
184
+ lineHeight: '1.3',
185
+ display: 'block'
186
+ });
187
+
188
+ // 合成文本(message + meta)
189
+ let txt = typeof message === 'string' ? message : safeStringify(message);
190
+ if (meta) {
191
+ try {
192
+ const metaStr = safeStringify(meta);
193
+ txt = `${txt}\n\n[meta]\n${metaStr}`;
194
+ } catch (e) {}
195
+ }
196
+ const fullText = txt;
197
+
198
+ if (txt.length > COLLAPSE_LENGTH) {
199
+ content.textContent = txt.slice(0, COLLAPSE_LENGTH) + '\n\n...(已折叠,点击展开查看全部)';
200
+ expandBtn.onclick = () => { content.textContent = fullText; expandBtn.style.display = 'none'; entriesContainer.scrollTop = entriesContainer.scrollHeight; };
201
+ } else {
202
+ content.textContent = txt;
203
+ expandBtn.style.display = 'none';
204
+ }
205
+
206
+ copyBtn.onclick = () => {
207
+ try { navigator.clipboard.writeText(fullText); } catch (e) { try { prompt('复制文本:Ctrl+C, Enter', fullText); } catch (ee) {} }
208
+ };
209
+
210
+ entry.appendChild(header);
211
+ entry.appendChild(content);
212
+ entriesContainer.appendChild(entry);
213
+ entryCount++;
214
+ updateStats();
215
+
216
+ // 自动滚到底部
217
+ entriesContainer.scrollTop = entriesContainer.scrollHeight;
218
+ }
219
+
220
+ // 报告包装
221
+ function report(obj) {
222
+ try {
223
+ addEntry({ type: obj.type || 'log', message: obj.message || safeStringify(obj), time: new Date().toLocaleString(), meta: obj.meta || null });
224
+ } catch (e) {
225
+ console.warn('Error panel report failed', e);
226
+ }
227
+ }
228
+
229
+ // ========== 捕获逻辑 ==========
230
+
231
+ // 传统同步错误
232
+ window.onerror = function(message, source, lineno, colno, error) {
233
+ const msg = `${message}\n at ${source}:${lineno}:${colno}\n${error && error.stack ? error.stack : ''}`;
234
+ report({ type: 'window.onerror', message: msg });
235
+ return false;
236
+ };
237
+
238
+ // 资源加载错误(图片/script/css 等)
239
+ window.addEventListener('error', function(event) {
240
+ const target = event.target || event.srcElement;
241
+ if (target && (target.src || target.href)) {
242
+ const tag = target.tagName;
243
+ const url = target.src || target.href;
244
+ report({ type: 'resource', message: `Failed to load resource: <${tag}> ${url}` });
245
+ }
246
+ }, true);
247
+
248
+ // 未处理的 Promise 拒绝
249
+ window.addEventListener('unhandledrejection', function(event) {
250
+ report({ type: 'unhandledrejection', message: safeStringify(event.reason) });
251
+ });
252
+
253
+ // 捕获 console.*(可选)
254
+ if (CAPTURE_CONSOLE && typeof console !== 'undefined') {
255
+ const methods = ['log', 'info', 'warn', 'error', 'debug'];
256
+ methods.forEach((m) => {
257
+ if (!(m in console)) return;
258
+ const orig = console[m].bind(console);
259
+ console[m] = function(...args) {
260
+ try {
261
+ // 生成调用栈(方便定位是谁调用了 console)
262
+ let stack = null;
263
+ try {
264
+ const err = new Error();
265
+ if (err.stack) {
266
+ // 移除前两行(Error + 本函数)
267
+ stack = err.stack.split('\n').slice(2).join('\n');
268
+ }
269
+ } catch (e) { stack = null; }
270
+
271
+ const payload = {
272
+ type: `console.${m}`,
273
+ message: args.map(a => safeStringify(a)).join(' '),
274
+ meta: stack
275
+ };
276
+ report(payload);
277
+ } catch (e) {
278
+ // noop
279
+ }
280
+ // 保持原有行为
281
+ try { orig(...args); } catch (e) {}
282
+ };
283
+ });
284
+ }
285
+
286
+ // Vue 2 支持(若使用全局 Vue)
287
+ if (typeof Vue !== 'undefined' && Vue && Vue.config) {
288
+ try {
289
+ Vue.config.errorHandler = function(err, vm, info) {
290
+ report({ type: 'Vue.error (2)', message: `${info}\n${err && err.stack ? err.stack : safeStringify(err)}` });
291
+ };
292
+ } catch (e) { /* ignore */ }
293
+ }
294
+
295
+ // Vue 3 注册函数(在 main.js 创建 app 后调用)
296
+ window.registerVue3ErrorHandler = function(app) {
297
+ if (!app || !app.config) return;
298
+ app.config.errorHandler = (err, instance, info) => {
299
+ report({ type: 'Vue.error (3)', message: `${info}\n${err && err.stack ? err.stack : safeStringify(err)}` });
300
+ };
301
+ };
302
+
303
+ // 立即创建 panel(或可改为按需)
304
+ if (document.readyState === 'loading') {
305
+ document.addEventListener('DOMContentLoaded', createPanel);
306
+ } else createPanel();
307
+
308
+ }
309
+ // ========= End Panel =========