// ==UserScript== // @name DevTools F12 Pro Max - // @namespace https://example.com // @version 3.3.0 // @description 移动端edge精简开发者工具 // @author Assistant // @match *://*/* // @run-at document-start // @grant none // @license MIT // ==/UserScript== //作者邮箱:2451226407@qq.com (function () { 'use strict'; // ==================== 常量与配置 ==================== const STORAGE_KEY_NETWORK = '__devtools_network_logs__'; const STORAGE_KEY_CONSOLE = '__devtools_console_logs__'; const STORAGE_KEY_SETTINGS = '__devtools_settings__'; const STORAGE_KEY_BTN_POS = '__devtools_btn_pos__'; const MAX_NETWORK_LOGS = 500; const MAX_CONSOLE_LOGS = 1000; const MAX_BODY_PREVIEW = 8000; const DEFAULT_SETTINGS = { buttonColor: '#ff6b35', panelHeight: 400, preserveLog: true, autoScroll: true, networkFilter: 'all', consoleFilter: 'all', searchScope: 'url', autoHideTime: 15, autoHideEnabled: true, autoHideOpacity: 0.6, autoHideRevealWidth: 28, mobilePanelHeight: 320, theme: 'dark', }; // ==================== 工具函数 ==================== const safeStringify = (obj, maxLen = MAX_BODY_PREVIEW) => { try { if (obj === undefined) return 'undefined'; if (obj === null) return 'null'; if (typeof obj === 'string') return obj.length > maxLen ? obj.slice(0, maxLen) + '...[truncated]' : obj; const s = JSON.stringify(obj, getCircularReplacer(), 2); return s && s.length > maxLen ? s.slice(0, maxLen) + '...[truncated]' : (s || String(obj)); } catch (e) { return String(obj); } }; const getCircularReplacer = () => { const seen = new WeakSet(); return (key, value) => { if (typeof value === 'object' && value !== null) { if (seen.has(value)) return '[Circular]'; seen.add(value); } if (value instanceof Error) return { message: value.message, stack: value.stack, name: value.name }; if (typeof value === 'function') return '[Function: ' + (value.name || 'anonymous') + ']'; if (typeof value === 'symbol') return value.toString(); if (value === undefined) return 'undefined'; return value; }; }; const formatTimestamp = (ts) => { const d = new Date(ts); return d.toLocaleTimeString() + '.' + String(d.getMilliseconds()).padStart(3, '0'); }; const formatSize = (bytes) => { if (bytes === undefined || bytes === null || isNaN(bytes)) return '—'; if (bytes < 1024) return bytes + ' B'; if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB'; return (bytes / 1048576).toFixed(2) + ' MB'; }; const escapeHtml = (str) => { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; }; const truncateUrl = (url, maxLen = 80) => { return url.length > maxLen ? url.slice(0, maxLen) + '...' : url; }; const isMobile = () => /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent) || window.innerWidth <= 768; // ==================== 数据存储管理器 ==================== class DataStore { constructor() { this.networkLogs = []; this.consoleLogs = []; this.settings = { ...DEFAULT_SETTINGS }; this.btnPosition = { normalX: null, normalY: null, adsorbed: false }; this._loadAll(); this._networkIdCounter = this.networkLogs.length > 0 ? Math.max(...this.networkLogs.map(l => l.id)) + 1 : 1; this._consoleIdCounter = this.consoleLogs.length > 0 ? Math.max(...this.consoleLogs.map(l => l.id)) + 1 : 1; } _loadAll() { try { const rawNet = sessionStorage.getItem(STORAGE_KEY_NETWORK); if (rawNet) this.networkLogs = JSON.parse(rawNet); } catch (e) { this.networkLogs = []; } try { const rawCon = sessionStorage.getItem(STORAGE_KEY_CONSOLE); if (rawCon) this.consoleLogs = JSON.parse(rawCon); } catch (e) { this.consoleLogs = []; } try { const rawSet = sessionStorage.getItem(STORAGE_KEY_SETTINGS); if (rawSet) this.settings = { ...this.settings, ...JSON.parse(rawSet) }; } catch (e) { /* keep defaults */ } try { const rawPos = sessionStorage.getItem(STORAGE_KEY_BTN_POS); if (rawPos) { const parsed = JSON.parse(rawPos); if (parsed.normalX !== undefined) { this.btnPosition = parsed; } else if (parsed.x !== undefined) { this.btnPosition = { normalX: parsed.x, normalY: parsed.y, adsorbed: false }; } } } catch (e) { this.btnPosition = { normalX: null, normalY: null, adsorbed: false }; } } _saveNetwork() { try { const trimmed = this.networkLogs.slice(-MAX_NETWORK_LOGS); sessionStorage.setItem(STORAGE_KEY_NETWORK, JSON.stringify(trimmed)); } catch (e) { /* storage full */ } } _saveConsole() { try { const trimmed = this.consoleLogs.slice(-MAX_CONSOLE_LOGS); sessionStorage.setItem(STORAGE_KEY_CONSOLE, JSON.stringify(trimmed)); } catch (e) { /* storage full */ } } _saveSettings() { try { sessionStorage.setItem(STORAGE_KEY_SETTINGS, JSON.stringify(this.settings)); } catch (e) { /* ignore */ } } saveBtnPosition(position) { this.btnPosition = position; try { sessionStorage.setItem(STORAGE_KEY_BTN_POS, JSON.stringify(position)); } catch (e) { /* ignore */ } } addNetworkEntry(entry) { entry.id = this._networkIdCounter++; this.networkLogs.push(entry); if (this.networkLogs.length > MAX_NETWORK_LOGS * 1.5) { this.networkLogs = this.networkLogs.slice(-MAX_NETWORK_LOGS); } this._saveNetwork(); return entry; } updateNetworkEntry(id, updates) { const entry = this.networkLogs.find(e => e.id === id); if (entry) { Object.assign(entry, updates); this._saveNetwork(); } return entry; } addConsoleEntry(entry) { entry.id = this._consoleIdCounter++; this.consoleLogs.push(entry); if (this.consoleLogs.length > MAX_CONSOLE_LOGS * 1.5) { this.consoleLogs = this.consoleLogs.slice(-MAX_CONSOLE_LOGS); } this._saveConsole(); return entry; } clearNetwork() { this.networkLogs = []; this._networkIdCounter = 1; this._saveNetwork(); } clearConsole() { this.consoleLogs = []; this._consoleIdCounter = 1; this._saveConsole(); } updateSettings(partial) { Object.assign(this.settings, partial); this._saveSettings(); } } const store = new DataStore(); // ==================== 拦截器 ==================== function setupInterceptors() { // XHR 拦截 const OrigXHR = window.XMLHttpRequest; if (OrigXHR) { const origOpen = OrigXHR.prototype.open; const origSend = OrigXHR.prototype.send; const origSetRequestHeader = OrigXHR.prototype.setRequestHeader; const origGetResponseHeader = OrigXHR.prototype.getResponseHeader; const origGetAllResponseHeaders = OrigXHR.prototype.getAllResponseHeaders; OrigXHR.prototype.open = function (method, url, async, user, password) { this.__devtools = { method: method.toUpperCase(), url: url.toString(), requestHeaders: {}, startTime: performance.now(), async: async !== false, }; return origOpen.apply(this, arguments); }; OrigXHR.prototype.setRequestHeader = function (name, value) { if (this.__devtools && this.__devtools.requestHeaders) { this.__devtools.requestHeaders[name] = value; } return origSetRequestHeader.apply(this, arguments); }; OrigXHR.prototype.send = function (body) { const devData = this.__devtools; if (devData) { devData.requestBody = body; const entry = store.addNetworkEntry({ method: devData.method, url: devData.url, status: 0, statusText: 'Pending', type: 'xhr', requestHeaders: { ...devData.requestHeaders }, responseHeaders: {}, requestBody: safeStringify(body, 2000), responseBody: null, startTime: devData.startTime, endTime: 0, duration: 0, size: 0, timestamp: Date.now(), }); this.__devtools_entryId = entry.id; devData.entryId = entry.id; } const onReadyStateChange = () => { if (!this.__devtools) return; const entryId = this.__devtools_entryId || this.__devtools.entryId; if (this.readyState === 4) { const endTime = performance.now(); let respHeaders = {}; try { respHeaders = parseHeaderString(origGetAllResponseHeaders.call(this)); } catch (e) { } let respBody = null; try { if (this.responseType === '' || this.responseType === 'text') { respBody = safeStringify(this.responseText, MAX_BODY_PREVIEW); } else if (this.responseType === 'json') { respBody = safeStringify(this.response, MAX_BODY_PREVIEW); } else { respBody = '[Binary/Blob response - ' + this.responseType + ']'; } } catch (e) { respBody = '[Unable to read response]'; } let size = 0; try { if (this.responseText) size = new Blob([this.responseText]).size; else if (this.response) size = JSON.stringify(this.response).length; } catch (e) { } store.updateNetworkEntry(entryId, { status: this.status, statusText: this.statusText || '', responseHeaders: respHeaders, responseBody: respBody, endTime: endTime, duration: Math.round(endTime - (this.__devtools.startTime || endTime)), size: size, }); } }; this.addEventListener('readystatechange', onReadyStateChange); const origOnLoad = this.onload; const origOnError = this.onerror; this.onload = function (e) { onReadyStateChange(); if (origOnLoad) return origOnLoad.apply(this, arguments); }; this.onerror = function (e) { onReadyStateChange(); if (origOnError) return origOnError.apply(this, arguments); }; try { return origSend.apply(this, arguments); } catch (e) { if (devData && devData.entryId) { store.updateNetworkEntry(devData.entryId, { status: 0, statusText: 'Error: ' + e.message, endTime: performance.now(), duration: Math.round(performance.now() - devData.startTime), }); } throw e; } }; } // Fetch 拦截 const origFetch = window.fetch; if (origFetch) { window.fetch = function (input, init) { const startTime = performance.now(); let method = 'GET'; let url = ''; let requestHeaders = {}; let requestBody = null; if (typeof input === 'string') { url = input; } else if (input instanceof Request) { url = input.url; method = input.method; requestHeaders = {}; input.headers.forEach((v, k) => { requestHeaders[k] = v; }); } if (init) { if (init.method) method = init.method.toUpperCase(); if (init.headers) { if (init.headers instanceof Headers) { init.headers.forEach((v, k) => { requestHeaders[k] = v; }); } else if (Array.isArray(init.headers)) { init.headers.forEach(([k, v]) => { requestHeaders[k] = v; }); } else { Object.assign(requestHeaders, init.headers); } } if (init.body) requestBody = safeStringify(init.body, 2000); } const entry = store.addNetworkEntry({ method: method.toUpperCase(), url: url, status: 0, statusText: 'Pending', type: 'fetch', requestHeaders: requestHeaders, responseHeaders: {}, requestBody: requestBody, responseBody: null, startTime: startTime, endTime: 0, duration: 0, size: 0, timestamp: Date.now(), }); const entryId = entry.id; return origFetch.apply(this, arguments).then(response => { const endTime = performance.now(); const clonedResponse = response.clone(); const respHeaders = {}; response.headers.forEach((v, k) => { respHeaders[k] = v; }); clonedResponse.text().then(bodyText => { const size = new Blob([bodyText]).size; store.updateNetworkEntry(entryId, { status: response.status, statusText: response.statusText || '', responseHeaders: respHeaders, responseBody: safeStringify(bodyText, MAX_BODY_PREVIEW), endTime: endTime, duration: Math.round(endTime - startTime), size: size, }); }).catch(() => { store.updateNetworkEntry(entryId, { status: response.status, statusText: response.statusText || '', responseHeaders: respHeaders, responseBody: '[Unable to read response body]', endTime: endTime, duration: Math.round(endTime - startTime), size: 0, }); }); return response; }).catch(error => { const endTime = performance.now(); store.updateNetworkEntry(entryId, { status: 0, statusText: 'Fetch Error: ' + (error.message || 'Unknown'), endTime: endTime, duration: Math.round(endTime - startTime), }); throw error; }); }; } // Console 拦截 const consoleMethods = ['log', 'error', 'warn', 'info', 'debug']; consoleMethods.forEach(method => { const origMethod = console[method]; if (typeof origMethod === 'function') { console[method] = function () { try { origMethod.apply(console, arguments); } catch (e) { /* ignore */ } let stack = null; if (method === 'error' || method === 'warn') { try { const err = new Error(); stack = err.stack ? err.stack.split('\n').slice(2, 8).join('\n') : null; } catch (e) { } } const args = Array.from(arguments).map(a => safeStringify(a, 3000)); store.addConsoleEntry({ level: method, args: args, timestamp: Date.now(), stack: stack, }); }; } }); window.addEventListener('error', function (e) { store.addConsoleEntry({ level: 'error', args: [safeStringify({ message: e.message, filename: e.filename, lineno: e.lineno, colno: e.colno, type: 'UncaughtError', })], timestamp: Date.now(), stack: e.error && e.error.stack ? e.error.stack : null, }); }); window.addEventListener('unhandledrejection', function (e) { store.addConsoleEntry({ level: 'error', args: [safeStringify({ message: 'Unhandled Promise Rejection', reason: e.reason, })], timestamp: Date.now(), stack: e.reason && e.reason.stack ? e.reason.stack : null, }); }); } function parseHeaderString(headerStr) { const headers = {}; if (!headerStr) return headers; const lines = headerStr.split('\r\n'); for (const line of lines) { const idx = line.indexOf(': '); if (idx > 0) { headers[line.slice(0, idx)] = line.slice(idx + 2); } } return headers; } setupInterceptors(); // ==================== UI 构建 ==================== function waitForDOM(callback) { if (document.body) { callback(); } else if (document.documentElement) { const observer = new MutationObserver(() => { if (document.body) { observer.disconnect(); callback(); } }); observer.observe(document.documentElement, { childList: true }); } else { document.addEventListener('DOMContentLoaded', callback, { once: true }); } } function createUI() { if (document.getElementById('__devtools_f12_root__')) return; const settings = store.settings; const savedPos = store.btnPosition; const styleEl = document.createElement('style'); styleEl.id = '__devtools_dynamic_style__'; styleEl.textContent = ` #__devtools_f12_root__ { font-family: 'Segoe UI', 'SF Mono', 'Consolas', 'Monaco', monospace; font-size:12px; z-index:2147483647; } #__devtools_f12_btn__ { position: fixed; border-radius: 50%; background: ${settings.buttonColor}; color: #fff; border: none; cursor: pointer; font-weight: bold; font-size: 15px; letter-spacing: 1px; box-shadow: 0 4px 16px rgba(0,0,0,0.4); z-index: 2147483647; display: flex; align-items: center; justify-content: center; user-select: none; -webkit-user-select: none; transition: box-shadow 0.2s, transform 0.15s, left 0.3s ease, top 0.3s ease, opacity 0.3s ease; touch-action: none; width: 52px; height: 52px; } #__devtools_f12_btn__:hover { box-shadow: 0 6px 22px rgba(0,0,0,0.55); transform: scale(1.06); } #__devtools_f12_btn__.dragging { transform: scale(1.12); box-shadow: 0 8px 28px rgba(0,0,0,0.6); cursor: grabbing; transition: none; } #__devtools_f12_btn__.auto-hidden { opacity: ${settings.autoHideOpacity}; } #__devtools_f12_btn__.auto-hidden:hover { opacity: 1; left: auto !important; right: auto !important; } #__devtools_panel__ { position: fixed; left: 0; right: 0; bottom: 0; z-index: 2147483646; background: #1e1e1e; color: #d4d4d4; border-top: 2px solid #444; display: flex; flex-direction: column; font-family: 'Segoe UI', 'SF Mono', 'Consolas', 'Monaco', monospace; font-size: 12px; box-shadow: 0 -4px 24px rgba(0,0,0,0.6); transition: height 0.15s ease; } #__devtools_panel_resizer__ { height: 6px; cursor: ns-resize; background: #3c3c3c; flex-shrink: 0; } #__devtools_panel_resizer__:hover { background: #007acc; } #__devtools_tabs__ { display: flex; background: #2d2d2d; flex-shrink: 0; align-items: center; padding: 0 4px; border-bottom: 1px solid #3c3c3c; flex-wrap: wrap; } .__devtools_tab__ { padding: 7px 12px; cursor: pointer; color: #999; border-bottom: 2px solid transparent; font-weight: 500; transition: color 0.15s, border-color 0.15s; font-size: 13px; user-select: none; -webkit-user-select: none; white-space: nowrap; } .__devtools_tab__:hover { color: #ddd; } .__devtools_tab__.active { color: #fff; border-bottom-color: #007acc; } #__devtools_tab_spacer__ { flex: 1; } #__devtools_close_btn__ { width: 28px; height: 28px; cursor: pointer; color: #999; display: flex; align-items: center; justify-content: center; border-radius: 4px; font-size: 18px; line-height: 1; user-select: none; -webkit-user-select: none; } #__devtools_close_btn__:hover { color: #fff; background: #c42b1c; } #__devtools_content__ { flex: 1; overflow: hidden; display: flex; flex-direction: column; min-height: 0; } .__devtools_panel_content__ { display: none; flex: 1; overflow: hidden; flex-direction: column; min-height: 0; } .__devtools_panel_content__.active { display: flex; } /* Network 操作栏 */ .__devtools_network_toolbar__ { display: flex; gap: 6px; padding: 6px 10px; background: #252525; flex-shrink: 0; align-items: center; } .__devtools_network_search__ { flex: 1; background: #1a1a1a; color: #ddd; border: 1px solid #555; padding: 6px 10px; font-size: 12px; border-radius: 4px; outline: none; min-width: 80px; } .__devtools_network_search__::placeholder { color: #888; } .__devtools_toolbar_btn__ { background: #3a3a3a; color: #ddd; border: 1px solid #555; padding: 5px 10px; cursor: pointer; border-radius: 4px; font-size: 11px; white-space: nowrap; } .__devtools_toolbar_btn__:hover { background: #4a4a4a; } #__devtools_network_list__ { flex: 1; overflow-y: auto; min-height: 0; } .__devtools_network_row__ { display: flex; align-items: center; padding: 4px 10px; border-bottom: 1px solid #2a2a2a; cursor: pointer; font-size: 11px; line-height: 1.4; transition: background 0.1s; } .__devtools_network_row__:hover { background: #2a2d2e; } .__devtools_network_row__.selected { background: #094771; } .__devtools_network_method__ { width: 48px; font-weight: bold; flex-shrink: 0; font-size: 10px; } .__devtools_network_url__ { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 0 8px; color: #ccc; } .__devtools_network_status__ { width: 40px; flex-shrink: 0; text-align: center; font-weight: bold; } .__devtools_network_type__ { width: 40px; flex-shrink: 0; text-align: center; color: #888; } .__devtools_network_size__ { width: 55px; flex-shrink: 0; text-align: right; color: #999; } .__devtools_network_time__ { width: 55px; flex-shrink: 0; text-align: right; color: #999; } .__devtools_network_detail__ { display: none; border-top: 2px solid #007acc; max-height: 45%; overflow-y: auto; background: #1a1a1a; padding: 10px; font-size: 11px; position: relative; } .__devtools_network_detail__.open { display: block; } .__devtools_detail_close_btn__ { position: sticky; top: 5px; float: right; width: 22px; height: 22px; background: #444; color: #fff; border: none; border-radius: 3px; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 14px; line-height: 1; z-index: 5; margin-bottom: -26px; } .__devtools_detail_close_btn__:hover { background: #c42b1c; } .__devtools_detail_section__ { margin-bottom: 10px; } .__devtools_detail_section__ h4 { color: #569cd6; margin: 0 0 4px; font-size: 12px; } .__devtools_detail_section__ pre { background: #111; padding: 8px; border-radius: 4px; overflow-x: auto; white-space: pre-wrap; word-break: break-all; max-height: 200px; overflow-y: auto; margin: 0; color: #ce9178; font-size: 11px; } /* Console */ .__devtools_console_toolbar__ { display: flex; gap: 6px; padding: 4px 10px; background: #252525; flex-shrink: 0; align-items: center; } #__devtools_console_output__ { flex: 1; overflow-y: auto; min-height: 0; padding: 4px 0; } .__devtools_console_entry__ { padding: 3px 10px; border-bottom: 1px solid #2a2a2a; font-size: 11px; line-height: 1.5; font-family: 'SF Mono', 'Consolas', 'Monaco', monospace; } .__devtools_console_entry__.level-error { border-left: 3px solid #f44747; color: #f48771; } .__devtools_console_entry__.level-warn { border-left: 3px solid #e2b93b; color: #e2b93b; } .__devtools_console_entry__.level-info { border-left: 3px solid #75beff; color: #ccc; } .__devtools_console_entry__.level-debug { border-left: 3px solid #888; color: #999; } .__devtools_console_entry__.level-log { border-left: 3px solid transparent; color: #d4d4d4; } .__devtools_console_ts__ { color: #666; margin-right: 8px; font-size: 10px; } .__devtools_console_args__ { white-space: pre-wrap; word-break: break-all; } #__devtools_console_input_row__ { display: flex; border-top: 1px solid #3c3c3c; flex-shrink: 0; } #__devtools_console_input__ { flex: 1; background: #1a1a1a; color: #fff; border: none; padding: 8px 10px; font-family: inherit; font-size: 12px; outline: none; } #__devtools_console_exec_btn__ { background: #007acc; color: #fff; border: none; padding: 8px 16px; cursor: pointer; font-weight: bold; font-size: 12px; } #__devtools_console_exec_btn__:hover { background: #005a9e; } /* Settings */ #__devtools_settings_content__ { padding: 16px; overflow-y: auto; } .__devtools_setting_row__ { margin-bottom: 14px; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; } .__devtools_setting_row__ label { color: #ccc; font-size: 12px; min-width: 80px; } .__devtools_setting_row__ input[type="color"] { width: 40px; height: 30px; border: none; cursor: pointer; background: transparent; padding: 0; } .__devtools_setting_row__ input[type="range"] { width: 140px; } .__devtools_setting_row__ input[type="checkbox"] { width: 16px; height: 16px; cursor: pointer; } .__devtools_setting_row__ select { background: #333; color: #fff; border: 1px solid #555; padding: 4px 8px; border-radius: 3px; } .__devtools_setting_row__ button { background: #333; color: #fff; border: 1px solid #555; padding: 6px 14px; cursor: pointer; border-radius: 3px; font-size: 12px; margin-right: 6px; } .__devtools_setting_row__ button:hover { background: #444; } .__devtools_setting_row__ .btn-danger { background: #5a1d1d; border-color: #c42b1c; } .__devtools_setting_row__ .btn-danger:hover { background: #7a2828; } /* Scrollbar */ #__devtools_panel__ ::-webkit-scrollbar { width: 6px; height: 6px; } #__devtools_panel__ ::-webkit-scrollbar-track { background: #1e1e1e; } #__devtools_panel__ ::-webkit-scrollbar-thumb { background: #555; border-radius: 3px; } #__devtools_panel__ ::-webkit-scrollbar-thumb:hover { background: #777; } /* Status bar */ #__devtools_statusbar__ { height: 22px; background: #007acc; color: #fff; font-size: 10px; display: flex; align-items: center; padding: 0 10px; flex-shrink: 0; gap: 16px; } /* 移动端优化 */ @media (max-width: 768px) { #__devtools_f12_btn__ { width: 44px; height: 44px; font-size: 13px; } #__devtools_panel__ { height: ${settings.mobilePanelHeight}px !important; } .__devtools_tab__ { padding: 5px 10px; font-size: 11px; } .__devtools_network_search__ { font-size: 11px; padding: 5px 8px; } .__devtools_toolbar_btn__ { padding: 4px 8px; font-size: 10px; } .__devtools_network_row__ { font-size: 10px; padding: 3px 8px; } .__devtools_network_method__ { width: 40px; } .__devtools_network_status__ { width: 32px; } .__devtools_network_type__ { display: none; } .__devtools_network_size__ { width: 45px; } .__devtools_network_time__ { width: 45px; } } `; document.head.appendChild(styleEl); // ============ 创建DOM结构 ============ const root = document.createElement('div'); root.id = '__devtools_f12_root__'; const btn = document.createElement('div'); btn.id = '__devtools_f12_btn__'; btn.textContent = 'F12'; btn.title = 'DevTools - 长按拖动 | 点击打开面板'; const panel = document.createElement('div'); panel.id = '__devtools_panel__'; panel.style.display = 'none'; const initPanelHeight = isMobile() ? settings.mobilePanelHeight : settings.panelHeight; panel.style.height = initPanelHeight + 'px'; const resizer = document.createElement('div'); resizer.id = '__devtools_panel_resizer__'; const tabs = document.createElement('div'); tabs.id = '__devtools_tabs__'; const tabNetwork = document.createElement('div'); tabNetwork.className = '__devtools_tab__ active'; tabNetwork.textContent = 'Network'; tabNetwork.dataset.tab = 'network'; const tabConsole = document.createElement('div'); tabConsole.className = '__devtools_tab__'; tabConsole.textContent = 'Console'; tabConsole.dataset.tab = 'console'; const tabSettings = document.createElement('div'); tabSettings.className = '__devtools_tab__'; tabSettings.textContent = '⚙ Settings'; tabSettings.dataset.tab = 'settings'; const tabSpacer = document.createElement('div'); tabSpacer.id = '__devtools_tab_spacer__'; const closeBtn = document.createElement('div'); closeBtn.id = '__devtools_close_btn__'; closeBtn.textContent = '✕'; closeBtn.title = '关闭面板'; tabs.append(tabNetwork, tabConsole, tabSettings, tabSpacer, closeBtn); const content = document.createElement('div'); content.id = '__devtools_content__'; // Network 面板 const networkPanel = document.createElement('div'); networkPanel.className = '__devtools_panel_content__ active'; networkPanel.id = '__devtools_network_panel__'; const networkToolbar = document.createElement('div'); networkToolbar.className = '__devtools_network_toolbar__'; const searchInput = document.createElement('input'); searchInput.type = 'text'; searchInput.className = '__devtools_network_search__'; searchInput.placeholder = '搜索请求...'; const copyAllNetworkBtn = document.createElement('button'); copyAllNetworkBtn.className = '__devtools_toolbar_btn__'; copyAllNetworkBtn.textContent = '📋 复制全部'; copyAllNetworkBtn.title = '复制所有网络日志(含详细内容)'; const clearNetworkBtn = document.createElement('button'); clearNetworkBtn.className = '__devtools_toolbar_btn__ btn-danger'; clearNetworkBtn.textContent = '🗑 清除'; clearNetworkBtn.title = '清空网络日志'; networkToolbar.append(searchInput, copyAllNetworkBtn, clearNetworkBtn); const networkList = document.createElement('div'); networkList.id = '__devtools_network_list__'; const networkDetail = document.createElement('div'); networkDetail.className = '__devtools_network_detail__'; networkDetail.id = '__devtools_network_detail__'; // 详情关闭按钮 const detailCloseBtn = document.createElement('button'); detailCloseBtn.className = '__devtools_detail_close_btn__'; detailCloseBtn.textContent = '✕'; detailCloseBtn.title = '关闭详情'; networkDetail.appendChild(detailCloseBtn); networkPanel.append(networkToolbar, networkList, networkDetail); // Console 面板 const consolePanel = document.createElement('div'); consolePanel.className = '__devtools_panel_content__'; consolePanel.id = '__devtools_console_panel__'; const consoleToolbar = document.createElement('div'); consoleToolbar.className = '__devtools_console_toolbar__'; const copyAllConsoleBtn = document.createElement('button'); copyAllConsoleBtn.className = '__devtools_toolbar_btn__'; copyAllConsoleBtn.textContent = '📋 复制全部'; copyAllConsoleBtn.title = '复制所有控制台日志'; const clearConsoleBtn = document.createElement('button'); clearConsoleBtn.className = '__devtools_toolbar_btn__ btn-danger'; clearConsoleBtn.textContent = '🗑 清除'; clearConsoleBtn.title = '清空控制台日志'; consoleToolbar.append(copyAllConsoleBtn, clearConsoleBtn); const consoleOutput = document.createElement('div'); consoleOutput.id = '__devtools_console_output__'; const consoleInputRow = document.createElement('div'); consoleInputRow.id = '__devtools_console_input_row__'; const consoleInput = document.createElement('input'); consoleInput.id = '__devtools_console_input__'; consoleInput.type = 'text'; consoleInput.placeholder = '输入JavaScript代码后按Enter执行...'; const consoleExecBtn = document.createElement('button'); consoleExecBtn.id = '__devtools_console_exec_btn__'; consoleExecBtn.textContent = '执行'; consoleInputRow.append(consoleInput, consoleExecBtn); consolePanel.append(consoleToolbar, consoleOutput, consoleInputRow); // Settings 面板 const settingsPanel = document.createElement('div'); settingsPanel.className = '__devtools_panel_content__'; settingsPanel.id = '__devtools_settings_content__'; buildSettingsContent(settingsPanel); content.append(networkPanel, consolePanel, settingsPanel); const statusBar = document.createElement('div'); statusBar.id = '__devtools_statusbar__'; statusBar.innerHTML = '🔍 0 requests📋 0 logs'; panel.append(resizer, tabs, content, statusBar); root.append(btn, panel); document.body.appendChild(root); // ============ 状态变量 ============ let panelVisible = false; let activeTab = 'network'; let selectedNetworkId = null; let isDragging = false; let dragStartX, dragStartY, dragStartTime; let btnStartLeft, btnStartTop; let hasMoved = false; const DRAG_THRESHOLD = 4; const LONG_PRESS_TIME = 280; let autoHideTimer = null; let userInteractedRecently = false; // ============ 按钮位置与吸附逻辑 ============ function updateButtonStyle() { btn.style.background = store.settings.buttonColor; const opacity = store.settings.autoHideOpacity; const styleTag = document.getElementById('__devtools_dynamic_style__'); if (styleTag) { styleTag.textContent = styleTag.textContent.replace( /\.auto-hidden\s*\{[^}]*\}/, `.auto-hidden { opacity: ${opacity}; }` ); } } function setBtnPositionAbsolute(left, top) { btn.style.right = 'auto'; btn.style.bottom = 'auto'; btn.style.left = left + 'px'; btn.style.top = top + 'px'; } function resetToNormalPosition() { const normalX = store.btnPosition.normalX; const normalY = store.btnPosition.normalY; if (normalX !== null && normalY !== null) { const maxX = window.innerWidth - btn.offsetWidth; const maxY = window.innerHeight - btn.offsetHeight; const safeX = Math.max(0, Math.min(maxX, normalX)); const safeY = Math.max(0, Math.min(maxY, normalY)); setBtnPositionAbsolute(safeX, safeY); } else { btn.style.right = '20px'; btn.style.bottom = '20px'; btn.style.left = 'auto'; btn.style.top = 'auto'; } btn.classList.remove('auto-hidden'); } function applyAdsorbState(adsorbed) { if (adsorbed === 'left' || adsorbed === 'right') { snapToEdge(adsorbed); } } // 初始加载 (function initBtnPosition() { if (savedPos.normalX !== null || savedPos.normalY !== null) { resetToNormalPosition(); } else { btn.style.right = '20px'; btn.style.bottom = '20px'; } if (savedPos.adsorbed) { applyAdsorbState(savedPos.adsorbed); } resetAutoHideTimer(); })(); function getBtnRect() { const r = btn.getBoundingClientRect(); return { left: r.left, top: r.top, width: r.width, height: r.height }; } function saveCurrentBtnPosition() { const rect = getBtnRect(); const isAdsorbed = btn.classList.contains('auto-hidden'); const side = isAdsorbed ? (parseFloat(btn.style.left) < 0 ? 'left' : 'right') : false; const normalPos = isAdsorbed ? { x: store.btnPosition.normalX, y: store.btnPosition.normalY } : { x: rect.left, y: rect.top }; store.saveBtnPosition({ normalX: normalPos.x, normalY: normalPos.y, adsorbed: side }); } function resetAutoHideTimer() { userInteractedRecently = true; if (btn.classList.contains('auto-hidden')) { restoreFromAdsorb(); } if (autoHideTimer) { clearTimeout(autoHideTimer); autoHideTimer = null; } const hideDelay = (store.settings.autoHideTime || 0) * 1000; if (store.settings.autoHideEnabled && hideDelay > 0 && !panelVisible) { autoHideTimer = setTimeout(() => { if (!panelVisible && !isDragging) { snapToEdge(); } userInteractedRecently = false; }, hideDelay); } } function restoreFromAdsorb() { if (!btn.classList.contains('auto-hidden')) return; btn.classList.remove('auto-hidden'); const normalX = store.btnPosition.normalX; const normalY = store.btnPosition.normalY; if (normalX !== null && normalY !== null) { const maxX = window.innerWidth - btn.offsetWidth; const maxY = window.innerHeight - btn.offsetHeight; const safeX = Math.max(0, Math.min(maxX, normalX)); const safeY = Math.max(0, Math.min(maxY, normalY)); setBtnPositionAbsolute(safeX, safeY); } else { btn.style.right = '20px'; btn.style.bottom = '20px'; btn.style.left = 'auto'; btn.style.top = 'auto'; } saveCurrentBtnPosition(); } function snapToEdge(side) { const rect = getBtnRect(); const centerX = rect.left + rect.width / 2; const screenWidth = window.innerWidth; const revealWidth = store.settings.autoHideRevealWidth; let targetSide = side; if (!targetSide) { targetSide = centerX < screenWidth / 2 ? 'left' : 'right'; } if (!btn.classList.contains('auto-hidden')) { store.btnPosition.normalX = rect.left; store.btnPosition.normalY = rect.top; } if (targetSide === 'left') { const hiddenLeft = - (rect.width - revealWidth); setBtnPositionAbsolute(hiddenLeft, rect.top); } else { const hiddenRight = - (rect.width - revealWidth); btn.style.right = hiddenRight + 'px'; btn.style.left = 'auto'; btn.style.top = rect.top + 'px'; btn.style.bottom = 'auto'; } btn.classList.add('auto-hidden'); store.btnPosition.adsorbed = targetSide; store.saveBtnPosition({ normalX: store.btnPosition.normalX, normalY: store.btnPosition.normalY, adsorbed: targetSide }); } ['mousemove', 'mousedown', 'keydown', 'scroll', 'touchstart', 'touchmove'].forEach(evt => { document.addEventListener(evt, (e) => { if (e.target === btn || btn.contains(e.target)) return; resetAutoHideTimer(); }, { passive: true }); }); // ============ 按钮事件 ============ btn.addEventListener('mousedown', onPointerDown, { passive: false }); btn.addEventListener('touchstart', onPointerDown, { passive: false }); document.addEventListener('mousemove', onPointerMove, { passive: false }); document.addEventListener('touchmove', onPointerMove, { passive: false }); document.addEventListener('mouseup', onPointerUp); document.addEventListener('touchend', onPointerUp); document.addEventListener('touchcancel', onPointerUp); function getEventPos(e) { if (e.touches && e.touches.length > 0) return { x: e.touches[0].clientX, y: e.touches[0].clientY }; if (e.changedTouches && e.changedTouches.length > 0) return { x: e.changedTouches[0].clientX, y: e.changedTouches[0].clientY }; return { x: e.clientX, y: e.clientY }; } function onPointerDown(e) { if (e.target.closest('#__devtools_panel__')) return; const pos = getEventPos(e); dragStartX = pos.x; dragStartY = pos.y; dragStartTime = Date.now(); if (btn.classList.contains('auto-hidden')) { restoreFromAdsorb(); const rect = getBtnRect(); btnStartLeft = rect.left; btnStartTop = rect.top; } else { const rect = getBtnRect(); btnStartLeft = rect.left; btnStartTop = rect.top; } isDragging = false; hasMoved = false; btn.classList.remove('dragging'); if (autoHideTimer) { clearTimeout(autoHideTimer); autoHideTimer = null; } e.preventDefault(); } function onPointerMove(e) { if (dragStartTime === 0 || dragStartX === undefined) return; const pos = getEventPos(e); const dx = pos.x - dragStartX; const dy = pos.y - dragStartY; const dist = Math.sqrt(dx * dx + dy * dy); const elapsed = Date.now() - dragStartTime; if (!isDragging && (dist > DRAG_THRESHOLD || elapsed > LONG_PRESS_TIME)) { isDragging = true; btn.classList.add('dragging'); const rect = getBtnRect(); btnStartLeft = rect.left; btnStartTop = rect.top; dragStartX = pos.x; dragStartY = pos.y; } if (isDragging) { hasMoved = true; const newLeft = btnStartLeft + (pos.x - dragStartX); const newTop = btnStartTop + (pos.y - dragStartY); const maxX = window.innerWidth - btn.offsetWidth; const maxY = window.innerHeight - btn.offsetHeight; const clampedLeft = Math.max(0, Math.min(maxX, newLeft)); const clampedTop = Math.max(0, Math.min(maxY, newTop)); setBtnPositionAbsolute(clampedLeft, clampedTop); e.preventDefault(); } } function onPointerUp(e) { btn.classList.remove('dragging'); if (isDragging && hasMoved) { const rect = getBtnRect(); store.btnPosition.normalX = rect.left; store.btnPosition.normalY = rect.top; store.btnPosition.adsorbed = false; store.saveBtnPosition({ normalX: rect.left, normalY: rect.top, adsorbed: false }); resetAutoHideTimer(); } else if (!hasMoved && !isDragging && Date.now() - dragStartTime < LONG_PRESS_TIME + 100) { togglePanel(); } isDragging = false; hasMoved = false; dragStartTime = 0; dragStartX = undefined; dragStartY = undefined; } // ============ 面板切换 ============ function togglePanel() { panelVisible = !panelVisible; panel.style.display = panelVisible ? 'flex' : 'none'; if (panelVisible) { refreshNetworkList(); refreshConsoleOutput(); updateStatusBar(); if (btn.classList.contains('auto-hidden')) { restoreFromAdsorb(); } } resetAutoHideTimer(); } closeBtn.addEventListener('click', function () { panelVisible = false; panel.style.display = 'none'; resetAutoHideTimer(); }); // ============ 标签切换 ============ tabs.addEventListener('click', function (e) { const tab = e.target.closest('.__devtools_tab__'); if (!tab || tab === tabSpacer) return; const targetTab = tab.dataset.tab; if (!targetTab) return; switchTab(targetTab); }); function switchTab(tabName) { activeTab = tabName; [tabNetwork, tabConsole, tabSettings].forEach(t => t.classList.remove('active')); [networkPanel, consolePanel, settingsPanel].forEach(p => p.classList.remove('active')); const targetTabEl = document.querySelector(`.__devtools_tab__[data-tab="${tabName}"]`); if (targetTabEl) targetTabEl.classList.add('active'); if (tabName === 'network') networkPanel.classList.add('active'); if (tabName === 'console') consolePanel.classList.add('active'); if (tabName === 'settings') settingsPanel.classList.add('active'); if (tabName === 'console') { consoleInput.focus(); refreshConsoleOutput(); } if (tabName === 'network') refreshNetworkList(); } // ============ 面板大小调整 ============ let resizingPanel = false; let resizeStartY, resizeStartHeight; resizer.addEventListener('mousedown', function (e) { resizingPanel = true; resizeStartY = e.clientY; resizeStartHeight = panel.offsetHeight; document.body.style.userSelect = 'none'; document.body.style.cursor = 'ns-resize'; e.preventDefault(); }); document.addEventListener('mousemove', function (e) { if (!resizingPanel) return; const dy = resizeStartY - e.clientY; const maxHeight = window.innerHeight * 0.85; const minHeight = isMobile() ? 160 : 200; const newHeight = Math.max(minHeight, Math.min(maxHeight, resizeStartHeight + dy)); panel.style.height = newHeight + 'px'; store.settings.panelHeight = newHeight; store._saveSettings(); }); document.addEventListener('mouseup', function () { if (resizingPanel) { resizingPanel = false; document.body.style.userSelect = ''; document.body.style.cursor = ''; } }); resizer.addEventListener('touchstart', function (e) { resizingPanel = true; resizeStartY = e.touches[0].clientY; resizeStartHeight = panel.offsetHeight; e.preventDefault(); }); document.addEventListener('touchmove', function (e) { if (!resizingPanel) return; const dy = resizeStartY - e.touches[0].clientY; const maxHeight = window.innerHeight * 0.85; const minHeight = isMobile() ? 160 : 200; const newHeight = Math.max(minHeight, Math.min(maxHeight, resizeStartHeight + dy)); panel.style.height = newHeight + 'px'; store.settings.panelHeight = newHeight; store._saveSettings(); }); document.addEventListener('touchend', function () { if (resizingPanel) { resizingPanel = false; } }); // ============ Network 面板功能 ============ function refreshNetworkList() { const logs = store.networkLogs; let filtered = applyNetworkFilter(logs); const searchTerm = searchInput.value.trim().toLowerCase(); if (searchTerm) { filtered = filtered.filter(log => { const scope = store.settings.searchScope || 'url'; if (scope === 'url') { return log.url.toLowerCase().includes(searchTerm); } else { const searchable = [ log.url, log.method, String(log.status), log.statusText || '', log.type, JSON.stringify(log.requestHeaders || {}), JSON.stringify(log.responseHeaders || {}), log.requestBody || '', log.responseBody || '', ].join(' ').toLowerCase(); return searchable.includes(searchTerm); } }); } const recentLogs = filtered.slice(-200); networkList.innerHTML = ''; if (recentLogs.length === 0) { networkList.innerHTML = '
暂无匹配的请求记录
'; return; } const displayLogs = [...recentLogs].reverse(); displayLogs.forEach(log => { const row = document.createElement('div'); row.className = '__devtools_network_row__'; if (log.id === selectedNetworkId) row.classList.add('selected'); row.dataset.networkId = log.id; const methodColor = { 'GET': '#61affe', 'POST': '#49cc90', 'PUT': '#fca130', 'DELETE': '#f93e3e', 'PATCH': '#c586c0', 'HEAD': '#aaa', 'OPTIONS': '#aaa' }[log.method] || '#ccc'; const statusColor = log.status >= 500 ? '#f93e3e' : log.status >= 400 ? '#f93e3e' : log.status >= 300 ? '#fca130' : log.status >= 200 ? '#49cc90' : '#999'; row.innerHTML = ` ${escapeHtml(log.method)} ${escapeHtml(truncateUrl(log.url, 70))} ${log.status || '—'} ${log.type} ${formatSize(log.size)} ${log.duration > 0 ? log.duration + 'ms' : '...'} `; row.addEventListener('click', () => { if (selectedNetworkId === log.id) { // 再次点击同一行:关闭详情 closeNetworkDetail(); } else { // 显示新详情 selectedNetworkId = log.id; showNetworkDetail(log); networkList.querySelectorAll('.__devtools_network_row__').forEach(r => r.classList.remove('selected')); row.classList.add('selected'); } }); networkList.appendChild(row); }); if (store.settings.autoScroll !== false) { networkList.scrollTop = 0; } } function closeNetworkDetail() { selectedNetworkId = null; networkDetail.classList.remove('open'); networkList.querySelectorAll('.__devtools_network_row__').forEach(r => r.classList.remove('selected')); } function showNetworkDetail(log) { // 先清除关闭按钮之外的内容 while (networkDetail.childNodes.length > 1) { networkDetail.removeChild(networkDetail.lastChild); } const detailContent = document.createElement('div'); detailContent.innerHTML = `

📋 请求概览

${escapeHtml(log.method)} ${escapeHtml(log.url)}
状态: ${log.status} ${escapeHtml(log.statusText || '')}
耗时: ${log.duration > 0 ? log.duration + 'ms' : 'Pending...'}
大小: ${formatSize(log.size)}
时间: ${formatTimestamp(log.timestamp)}

📤 请求头

${escapeHtml(safeStringify(log.requestHeaders, 3000))}
${log.requestBody ? `

📤 请求体

${escapeHtml(log.requestBody)}
` : ''}

📥 响应头

${escapeHtml(safeStringify(log.responseHeaders, 3000))}
${log.responseBody ? `

📥 响应体

${escapeHtml(log.responseBody)}
` : ''} `; networkDetail.appendChild(detailContent); networkDetail.classList.add('open'); } // 关闭按钮点击事件 detailCloseBtn.addEventListener('click', function (e) { e.stopPropagation(); closeNetworkDetail(); }); searchInput.addEventListener('input', () => { refreshNetworkList(); }); copyAllNetworkBtn.addEventListener('click', () => { const logs = applyNetworkFilter(store.networkLogs); if (logs.length === 0) { alert('没有可复制的网络日志'); return; } const text = logs.map((log, index) => { const separator = `\n${'='.repeat(40)} Request ${index + 1} ${'='.repeat(40)}\n`; let details = []; details.push(`${log.method} ${log.url}`); details.push(`Status: ${log.status} ${log.statusText || ''}`); details.push(`Duration: ${log.duration}ms`); details.push(`Size: ${formatSize(log.size)}`); details.push(`Timestamp: ${formatTimestamp(log.timestamp)}`); details.push(`Type: ${log.type}`); details.push(`\n--- Request Headers ---`); details.push(JSON.stringify(log.requestHeaders || {}, null, 2)); if (log.requestBody) { details.push(`\n--- Request Body ---`); details.push(log.requestBody); } details.push(`\n--- Response Headers ---`); details.push(JSON.stringify(log.responseHeaders || {}, null, 2)); if (log.responseBody) { details.push(`\n--- Response Body ---`); details.push(log.responseBody); } return separator + details.join('\n'); }).join('\n'); copyToClipboard(text, '网络日志(含请求/响应详情)'); }); clearNetworkBtn.addEventListener('click', () => { if (store.networkLogs.length > 0 && !confirm('确定要清空所有网络日志吗?')) return; store.clearNetwork(); closeNetworkDetail(); refreshNetworkList(); updateStatusBar(); }); // ============ Console 面板功能 ============ function refreshConsoleOutput() { const logs = store.consoleLogs; let filtered = logs; const filter = store.settings.consoleFilter || 'all'; if (filter !== 'all') filtered = logs.filter(l => l.level === filter); const recentLogs = filtered.slice(-400); consoleOutput.innerHTML = ''; if (recentLogs.length === 0) { consoleOutput.innerHTML = '
暂无控制台日志
'; return; } const displayLogs = [...recentLogs].reverse(); displayLogs.forEach(log => { const entry = document.createElement('div'); entry.className = '__devtools_console_entry__ level-' + log.level; const ts = formatTimestamp(log.timestamp); const argsStr = log.args.map(a => { if (typeof a === 'string') return a; return safeStringify(a, 1500); }).join(' '); entry.innerHTML = ` ${ts} ${escapeHtml(argsStr)} ${log.stack ? '
' + escapeHtml(log.stack) + '
' : ''} `; consoleOutput.appendChild(entry); }); if (store.settings.autoScroll !== false) { consoleOutput.scrollTop = 0; } } function executeConsoleCode(code) { store.addConsoleEntry({ level: 'info', args: ['> ' + code], timestamp: Date.now(), stack: null, }); try { const result = eval.call(window, code); store.addConsoleEntry({ level: 'log', args: [safeStringify(result, 5000)], timestamp: Date.now(), stack: null, }); } catch (e) { store.addConsoleEntry({ level: 'error', args: [safeStringify({ message: e.message, stack: e.stack })], timestamp: Date.now(), stack: e.stack || null, }); } refreshConsoleOutput(); updateStatusBar(); } consoleExecBtn.addEventListener('click', function () { const code = consoleInput.value.trim(); if (!code) return; executeConsoleCode(code); consoleInput.value = ''; }); consoleInput.addEventListener('keydown', function (e) { if (e.key === 'Enter') { const code = consoleInput.value.trim(); if (!code) return; executeConsoleCode(code); consoleInput.value = ''; } if (e.key === 'ArrowUp') { const lastExec = store.consoleLogs.filter(l => l.args[0] && l.args[0].startsWith('> ')).pop(); if (lastExec) { consoleInput.value = lastExec.args[0].replace('> ', ''); } } }); copyAllConsoleBtn.addEventListener('click', () => { const filter = store.settings.consoleFilter || 'all'; let logs = store.consoleLogs; if (filter !== 'all') logs = logs.filter(l => l.level === filter); const text = logs.map(log => { const ts = formatTimestamp(log.timestamp); const args = log.args.join(' '); return `[${ts}] [${log.level}] ${args}`; }).join('\n'); copyToClipboard(text, '所有控制台日志'); }); clearConsoleBtn.addEventListener('click', () => { if (store.consoleLogs.length > 0 && !confirm('确定要清空所有控制台日志吗?')) return; store.clearConsole(); refreshConsoleOutput(); updateStatusBar(); }); function copyToClipboard(text, label) { if (!text) { alert('没有可复制的' + label); return; } navigator.clipboard.writeText(text).then(() => { alert(label + '已复制到剪贴板'); }).catch(() => { const textarea = document.createElement('textarea'); textarea.value = text; document.body.appendChild(textarea); textarea.select(); document.execCommand('copy'); document.body.removeChild(textarea); alert(label + '已复制到剪贴板'); }); } function applyNetworkFilter(logs) { const filter = store.settings.networkFilter || 'all'; if (filter === 'xhr') return logs.filter(l => l.type === 'xhr'); if (filter === 'fetch') return logs.filter(l => l.type === 'fetch'); if (filter === 'error') return logs.filter(l => l.status >= 400 || l.status === 0); return logs; } // ============ 设置面板 ============ function buildSettingsContent(container) { container.innerHTML = ''; const rows = [ { label: '按钮颜色', html: ``, }, { label: '面板高度(桌面)', html: ` ${store.settings.panelHeight}px`, }, { label: '面板高度(移动)', html: ` ${store.settings.mobilePanelHeight}px`, }, { label: '保留日志', html: ` 刷新页面时保留数据`, }, { label: '自动滚动', html: ``, }, { label: 'Network过滤', html: ``, }, { label: 'Console过滤', html: ``, }, { label: '搜索范围', html: ` 搜索请求时包含头/体`, }, { label: '自动吸附', html: ` 启用`, }, { label: '吸附延迟(秒)', html: ``, }, { label: '吸附不透明度', html: ` ${store.settings.autoHideOpacity}`, }, { label: '吸附露出宽度(px)', html: ` ${store.settings.autoHideRevealWidth}px`, }, { label: '操作', html: ` `, }, ]; rows.forEach(row => { const div = document.createElement('div'); div.className = '__devtools_setting_row__'; div.innerHTML = `${row.html}`; container.appendChild(div); }); // 绑定设置事件 const btnColorInput = container.querySelector('#__devtools_setting_btncolor__'); const heightInput = container.querySelector('#__devtools_setting_height__'); const heightVal = container.querySelector('#__devtools_setting_height_val__'); const mobileHeightInput = container.querySelector('#__devtools_setting_mobile_height__'); const mobileHeightVal = container.querySelector('#__devtools_setting_mobile_height_val__'); const preserveCheck = container.querySelector('#__devtools_setting_preserve__'); const autoScrollCheck = container.querySelector('#__devtools_setting_autoscroll__'); const netFilter = container.querySelector('#__devtools_setting_netfilter__'); const conFilter = container.querySelector('#__devtools_setting_confilter__'); const searchScope = container.querySelector('#__devtools_setting_searchscope__'); const autoHideEnabled = container.querySelector('#__devtools_setting_autohide_enabled__'); const autoHideTime = container.querySelector('#__devtools_setting_autohide_time__'); const autoHideOpacity = container.querySelector('#__devtools_setting_autohide_opacity__'); const autoHideOpacityVal = container.querySelector('#__devtools_setting_autohide_opacity_val__'); const autoHideWidth = container.querySelector('#__devtools_setting_autohide_width__'); const autoHideWidthVal = container.querySelector('#__devtools_setting_autohide_width_val__'); const clearAllBtn = container.querySelector('#__devtools_clear_all__'); const resetPosBtn = container.querySelector('#__devtools_reset_btnpos__'); btnColorInput.addEventListener('input', function () { btn.style.background = this.value; store.updateSettings({ buttonColor: this.value }); }); heightInput.addEventListener('input', function () { const h = parseInt(this.value); panel.style.height = h + 'px'; heightVal.textContent = h + 'px'; store.updateSettings({ panelHeight: h }); }); mobileHeightInput.addEventListener('input', function () { const h = parseInt(this.value); mobileHeightVal.textContent = h + 'px'; store.updateSettings({ mobilePanelHeight: h }); if (isMobile()) { panel.style.height = h + 'px'; } }); preserveCheck.addEventListener('change', function () { store.updateSettings({ preserveLog: this.checked }); }); autoScrollCheck.addEventListener('change', function () { store.updateSettings({ autoScroll: this.checked }); }); netFilter.addEventListener('change', function () { store.updateSettings({ networkFilter: this.value }); refreshNetworkList(); }); conFilter.addEventListener('change', function () { store.updateSettings({ consoleFilter: this.value }); refreshConsoleOutput(); }); searchScope.addEventListener('change', function () { store.updateSettings({ searchScope: this.value }); refreshNetworkList(); }); autoHideEnabled.addEventListener('change', function () { store.updateSettings({ autoHideEnabled: this.checked }); resetAutoHideTimer(); }); autoHideTime.addEventListener('change', function () { let val = parseInt(this.value) || 0; if (val < 0) val = 0; this.value = val; store.updateSettings({ autoHideTime: val }); resetAutoHideTimer(); }); autoHideOpacity.addEventListener('input', function () { const val = parseFloat(this.value); autoHideOpacityVal.textContent = val; store.updateSettings({ autoHideOpacity: val }); const styleTag = document.getElementById('__devtools_dynamic_style__'); if (styleTag) { styleTag.textContent = styleTag.textContent.replace( /\.auto-hidden\s*\{[^}]*\}/, `.auto-hidden { opacity: ${val}; }` ); } if (btn.classList.contains('auto-hidden')) { btn.style.opacity = val; } }); autoHideWidth.addEventListener('input', function () { const val = parseInt(this.value); autoHideWidthVal.textContent = val + 'px'; store.updateSettings({ autoHideRevealWidth: val }); if (btn.classList.contains('auto-hidden')) { const side = store.btnPosition.adsorbed; snapToEdge(side); } }); clearAllBtn.addEventListener('click', function () { if (!confirm('确定要清空所有网络日志和控制台日志吗?')) return; store.clearNetwork(); store.clearConsole(); closeNetworkDetail(); refreshNetworkList(); refreshConsoleOutput(); updateStatusBar(); }); resetPosBtn.addEventListener('click', function () { btn.style.right = '20px'; btn.style.bottom = '20px'; btn.style.left = 'auto'; btn.style.top = 'auto'; btn.classList.remove('auto-hidden'); store.btnPosition = { normalX: null, normalY: null, adsorbed: false }; store.saveBtnPosition({ normalX: null, normalY: null, adsorbed: false }); resetAutoHideTimer(); }); } // ============ 状态栏更新 ============ function updateStatusBar() { const netSpan = document.getElementById('__devtools_status_network__'); const conSpan = document.getElementById('__devtools_status_console__'); if (netSpan) netSpan.textContent = '🔍 ' + store.networkLogs.length + ' requests'; if (conSpan) conSpan.textContent = '📋 ' + store.consoleLogs.length + ' logs'; } // ============ 定期刷新 ============ let refreshTimer = null; function scheduleRefresh() { if (refreshTimer) return; refreshTimer = setTimeout(() => { refreshTimer = null; if (panelVisible) { if (activeTab === 'network') refreshNetworkList(); if (activeTab === 'console') refreshConsoleOutput(); updateStatusBar(); } }, 300); } const origAddNetwork = store.addNetworkEntry.bind(store); store.addNetworkEntry = function (entry) { const result = origAddNetwork(entry); scheduleRefresh(); return result; }; const origUpdateNetwork = store.updateNetworkEntry.bind(store); store.updateNetworkEntry = function (id, updates) { const result = origUpdateNetwork(id, updates); scheduleRefresh(); return result; }; const origAddConsole = store.addConsoleEntry.bind(store); store.addConsoleEntry = function (entry) { const result = origAddConsole(entry); scheduleRefresh(); return result; }; // ============ 键盘快捷键 ============ document.addEventListener('keydown', function (e) { if (e.key === 'F12' && !e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey) { e.preventDefault(); togglePanel(); } if (e.key === 'Escape' && panelVisible) { panelVisible = false; panel.style.display = 'none'; resetAutoHideTimer(); } }); // ============ 页面卸载时保存 ============ window.addEventListener('beforeunload', () => { store._saveNetwork(); store._saveConsole(); store._saveSettings(); saveCurrentBtnPosition(); }); window.__devtools__ = { store: store, refreshNetworkList: refreshNetworkList, refreshConsoleOutput: refreshConsoleOutput, togglePanel: togglePanel, switchTab: switchTab, updateStatusBar: updateStatusBar, }; console.log('%c🛠️ DevTools F12 Pro Max 已就绪 %c| 网络详情可关闭/切换', 'color:#ff6b35;font-weight:bold;', 'color:#aaa;'); } waitForDOM(createUI); })();