diff --git a/CLAUDE.md b/CLAUDE.md index a29584f9..057f507b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -170,7 +170,7 @@ Service Worker (后台执行) 1. Context 回调中必须使用函数式更新 `setState(prev => ...)` 2. Hover 延迟操作需要正确的计时器清理 -3. 第三方窗口需用 `createPortal` 保持 React 事件流 +3. 弹出菜单/浮层需用 `createPortal` 渲染到 body,避免被父容器 overflow 截断 4. 模式切换时多个相关状态需同步更新,封装成一个函数而非直接暴露底层 setMode 5. 图标组件使用 `React.FC`,支持 `size` 属性 6. 传递组件作为 prop 时必须实例化:`icon={}` 而非 `icon={Icon}` @@ -211,6 +211,7 @@ Service Worker (后台执行) - **可点击容器**:用 `pointer-events: none` + 父容器 `onClick` 扩大交互区域 - **筛选与选中联动**:选中计数、操作都应基于筛选后的结果 - **全局配色统一**:在 `tdesign-theme.scss` 中集中覆盖第三方组件样式 +- **组件内颜色**:TDesign CSS 变量可能被覆盖,组件 scss 中应直接使用具体颜色值(如 `rgba(243, 156, 18, 0.12)`) --- diff --git a/apps/web/public/sw-debug/app.js b/apps/web/public/sw-debug/app.js index 5fc93ced..aef91a01 100644 --- a/apps/web/public/sw-debug/app.js +++ b/apps/web/public/sw-debug/app.js @@ -33,6 +33,7 @@ import { registerMessageHandlers, onControllerChange, setPostMessageLogCallback, + ensureDuplexInitialized, } from './sw-communication.js'; // Feature modules @@ -661,6 +662,7 @@ function handleStatusUpdate(data) { state.debugEnabled = updateStatusPanel(status, elements); state.swStatus = status; // Store for export updateDebugButton(elements.toggleDebugBtn, state.debugEnabled); + try { sessionStorage.setItem('sw-debug-enabled', String(state.debugEnabled)); } catch {} } // ==================== Event Listeners Setup ==================== @@ -874,6 +876,7 @@ function setupMessageHandlers() { 'SW_DEBUG_STATUS': handleStatusUpdate, 'SW_DEBUG_ENABLED': () => { state.debugEnabled = true; + try { sessionStorage.setItem('sw-debug-enabled', 'true'); } catch {} updateDebugButton(elements.toggleDebugBtn, true); // Update status panel text to show "开启" if (elements.debugMode) { @@ -888,6 +891,7 @@ function setupMessageHandlers() { }, 'SW_DEBUG_DISABLED': () => { state.debugEnabled = false; + try { sessionStorage.setItem('sw-debug-enabled', 'false'); } catch {} updateDebugButton(elements.toggleDebugBtn, false); // Update status panel text to show "关闭" if (elements.debugMode) { @@ -1178,16 +1182,18 @@ async function init() { setupMessageHandlers(); - // Auto-enable debug mode first when entering debug page - // The SW_DEBUG_ENABLED handler will then call refreshStatus() to get latest state - // This ensures proper state synchronization and avoids race conditions + // 先建立 duplex 通道并订阅广播(CONSOLE_LOG、POSTMESSAGE_LOG 等),再启用调试 + // 否则 SW 启用调试后开始转发日志时,调试页尚未在 channel-manager 中注册,收不到任何日志 + console.log('[SW Debug] Initializing duplex channel before enabling debug'); + const duplexOk = await ensureDuplexInitialized(); + if (!duplexOk) { + console.warn('[SW Debug] Duplex init failed, console/postmessage logs may not stream'); + } + + // Auto-enable debug mode when entering debug page console.log('[SW Debug] Auto-enabling debug mode'); enableDebug(); - - // 等待 SW 处理 debug mode 启用消息 - // 这确保 SW 端的 duplex channel 已经建立 - await new Promise(resolve => setTimeout(resolve, 500)); - + // Load fetch logs (existing logs from before debug mode was enabled won't exist, // but logs generated during this session will be available) loadFetchLogs(); diff --git a/apps/web/public/sw-debug/console-entry.js b/apps/web/public/sw-debug/console-entry.js index fe3cddf9..1b7f1455 100644 --- a/apps/web/public/sw-debug/console-entry.js +++ b/apps/web/public/sw-debug/console-entry.js @@ -19,6 +19,88 @@ function formatStack(stack) { }).filter(Boolean).join('\n'); } +/** + * 尝试解析 JSON 格式的日志消息 + * @param {unknown} msg + * @returns {{ parsed: boolean, message: string, stack?: string, source?: string, extra?: object }} + */ +function parseLogMessage(msg) { + if (msg == null) return { parsed: false, message: '' }; + + const msgStr = typeof msg === 'string' ? msg : ( + typeof msg === 'object' ? JSON.stringify(msg) : String(msg) + ); + + // 尝试解析 JSON 格式的消息 + if (msgStr.startsWith('{') && msgStr.endsWith('}')) { + try { + const obj = JSON.parse(msgStr); + if (obj && typeof obj === 'object') { + const result = { + parsed: true, + message: obj.message || msgStr, + stack: obj.stack || undefined, + source: obj.source || undefined, + }; + // 收集其他额外字段 + const extra = {}; + for (const key of Object.keys(obj)) { + if (!['message', 'stack', 'source'].includes(key)) { + extra[key] = obj[key]; + } + } + if (Object.keys(extra).length > 0) { + result.extra = extra; + } + return result; + } + } catch { + // 解析失败,使用原始字符串 + } + } + + return { parsed: false, message: msgStr }; +} + +/** + * 格式化 JSON 对象为带语法高亮的 HTML + * @param {unknown} obj + * @param {number} indent + * @returns {string} + */ +function formatJsonHtml(obj, indent = 0) { + const indentStr = ' '.repeat(indent); + + if (obj === null) { + return 'null'; + } + if (typeof obj === 'boolean') { + return `${obj}`; + } + if (typeof obj === 'number') { + return `${obj}`; + } + if (typeof obj === 'string') { + // 截断过长的字符串 + const displayStr = obj.length > 500 ? obj.substring(0, 500) + '...' : obj; + return `"${escapeHtml(displayStr)}"`; + } + if (Array.isArray(obj)) { + if (obj.length === 0) return '[]'; + const items = obj.map(item => `${indentStr} ${formatJsonHtml(item, indent + 1)}`); + return `[\n${items.join(',\n')}\n${indentStr}]`; + } + if (typeof obj === 'object') { + const keys = Object.keys(obj); + if (keys.length === 0) return '{}'; + const items = keys.map(key => + `${indentStr} "${escapeHtml(key)}": ${formatJsonHtml(obj[key], indent + 1)}` + ); + return `{\n${items.join(',\n')}\n${indentStr}}`; + } + return escapeHtml(String(obj)); +} + /** * Create a console log entry DOM element * Uses the same styles as Fetch logs for consistency @@ -33,8 +115,19 @@ export function createConsoleEntry(log, isExpanded = false, onToggle = null) { entry.className = `log-entry console-entry ${level}` + (isExpanded ? ' expanded' : ''); entry.dataset.id = log.id; - const hasStack = log.logStack && log.logStack.trim(); - const hasDetails = hasStack || log.logSource || log.url; + // 解析日志消息 + const parsedMsg = parseLogMessage(log.logMessage); + const displayMessage = parsedMsg.message; + + // 合并来自 log 对象和解析出的信息 + const stack = log.logStack?.trim() || parsedMsg.stack?.trim() || ''; + const source = log.logSource || parsedMsg.source || ''; + const hasStack = !!stack; + const hasSource = !!source; + const hasExtra = parsedMsg.extra && Object.keys(parsedMsg.extra).length > 0; + + // 总是显示展开按钮(消息长度超过 80 字符或有详细信息) + const hasDetails = hasStack || hasSource || log.url || hasExtra || displayMessage.length > 80; // Map log level to status class const levelStatusClass = { @@ -45,28 +138,28 @@ export function createConsoleEntry(log, isExpanded = false, onToggle = null) { 'debug': 'pending', }[level] || 'pending'; - // Show full message in header (will wrap if needed) - const messagePreview = log.logMessage || '-'; + // 头部显示截断的消息 + const headerMessage = displayMessage.length > 120 + ? displayMessage.substring(0, 120) + '...' + : displayMessage; entry.innerHTML = `
${escapeHtml(log.logMessage)}
${escapeHtml(displayMessage)}
${escapeHtml(log.logSource)}
${escapeHtml(source)}
${formatStack(log.logStack)}
${formatStack(stack)}
${formatJsonHtml(parsedMsg.extra)}
- 确定要删除 {deleteTarget?.type === 'folder' ? '文件夹' : '画板'} " - {deleteTarget?.name}" 吗? - {deleteTarget?.type === 'folder' && ( - - 仅删目录时,文件夹内的所有内容将被移动到根目录。 - - )} -
确定要删除选中的 {deleteMultipleTargets.length} 个画板吗?此操作不可撤销。
+ 确定要删除 {deleteTarget?.type === 'folder' ? '文件夹' : '画板'} " + {deleteTarget?.name}" 吗? + {deleteTarget?.type === 'folder' && ( + + 仅删目录时,文件夹内的所有内容将被移动到根目录。 + + )} +