diff --git a/web/index.html b/web/index.html index 1cdeb9d..f3cb412 100644 --- a/web/index.html +++ b/web/index.html @@ -275,125 +275,131 @@ gap: 10px; } - /* JSON 树形展示样式 */ + /* JSON 卡片式展示 */ .json-viewer { background: #f8f9fa; border-radius: 8px; - padding: 20px; - font-family: 'Courier New', monospace; - font-size: 14px; - line-height: 1.8; - overflow-x: auto; + padding: 0; max-height: 60vh; + overflow-y: auto; } - .json-line { - display: flex; - align-items: center; - padding: 2px 0; - position: relative; + .json-section { + margin-bottom: 10px; } - .json-line:hover { - background: rgba(102, 126, 234, 0.05); - } - - .json-indent { - display: inline-block; - width: 20px; - flex-shrink: 0; - } - - .json-toggle { - display: inline-block; - width: 20px; - height: 20px; - text-align: center; - cursor: pointer; - user-select: none; - color: #999; - flex-shrink: 0; - font-size: 12px; - line-height: 20px; - } - - .json-toggle:hover { - color: #667eea; - } - - .json-toggle.collapsed::before { - content: '▶'; - } - - .json-toggle.expanded::before { - content: '▼'; - } - - .json-key { - color: #0066cc; + .json-section-title { + background: #667eea; + color: white; + padding: 12px 20px; font-weight: bold; - margin-right: 8px; + font-size: 16px; + border-radius: 8px 8px 0 0; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + user-select: none; } - .json-separator { - color: #999; - margin: 0 4px; + .json-section-title:hover { + background: #5568d3; } - .json-string { + .json-section-toggle { + font-size: 12px; + } + + .json-section-body { + background: white; + border-radius: 0 0 8px 8px; + } + + .json-section-body.collapsed { + display: none; + } + + .json-field { + display: flex; + padding: 12px 20px; + border-bottom: 1px solid #eee; + align-items: flex-start; + } + + .json-field:last-child { + border-bottom: none; + } + + .json-field:hover { + background: #f8f9fa; + } + + .json-field-label { + min-width: 200px; + font-weight: bold; + color: #0066cc; + padding-right: 20px; + flex-shrink: 0; + } + + .json-field-value { + flex: 1; + word-break: break-all; + color: #333; + font-family: 'Courier New', monospace; + font-size: 13px; + } + + .json-field-value.string { color: #22863a; } - .json-number { + .json-field-value.number { color: #005cc5; } - .json-boolean { + .json-field-value.boolean { color: #d73a49; } - .json-null { + .json-field-value.null { color: #6f42c1; } - .json-bracket { - color: #666; - font-weight: bold; + .json-field-actions { + margin-left: 10px; + display: flex; + gap: 5px; } .json-copy-btn { - opacity: 0; - margin-left: 10px; - padding: 2px 8px; - font-size: 11px; + padding: 4px 10px; + font-size: 12px; background: #667eea; color: white; border: none; - border-radius: 3px; + border-radius: 4px; cursor: pointer; - transition: opacity 0.2s; - } - - .json-line:hover .json-copy-btn { - opacity: 1; + white-space: nowrap; } .json-copy-btn:hover { background: #5568d3; } - .json-children { - display: block; + .json-raw-btn { + padding: 4px 10px; + font-size: 12px; + background: #6c757d; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + white-space: nowrap; } - .json-children.collapsed { - display: none; - } - - .json-summary { - color: #999; - font-style: italic; - margin-left: 8px; + .json-raw-btn:hover { + background: #5a6268; } /* Toast 通知 */ @@ -613,122 +619,106 @@ document.getElementById(modalId).classList.remove('active'); } - // JSON 树形渲染 - function renderJSONTree(obj, level = 0) { - const lines = []; - const indent = ' '.repeat(level * 2); + // JSON 卡片式渲染 + function renderJSONCard(obj, parentPath = '') { + let html = ''; - if (obj === null) { - return `null`; + if (typeof obj !== 'object' || obj === null) { + return ''; } - if (typeof obj !== 'object') { - if (typeof obj === 'string') { - return `"${escapeHtml(obj)}"`; - } else if (typeof obj === 'number') { - return `${obj}`; - } else if (typeof obj === 'boolean') { - return `${obj}`; + for (const [key, value] of Object.entries(obj)) { + const path = parentPath ? `${parentPath}.${key}` : key; + + if (value !== null && typeof value === 'object') { + // 对象或数组 - 创建折叠区域 + const isArray = Array.isArray(value); + const itemCount = isArray ? value.length : Object.keys(value).length; + const sectionId = 'section-' + Math.random().toString(36).substr(2, 9); + + html += ` +
+
+ ${key} ${isArray ? `[${itemCount} items]` : `{${itemCount} keys}`} + +
+
+ ${renderJSONFields(value, path)} +
+
+ `; } } - const isArray = Array.isArray(obj); - const entries = isArray ? obj.map((v, i) => [i, v]) : Object.entries(obj); - const isEmpty = entries.length === 0; + return html; + } + + function renderJSONFields(obj, parentPath = '') { + let html = ''; - if (isEmpty) { - return isArray ? - `[]` : - `{}`; + if (typeof obj !== 'object' || obj === null) { + return ''; } - const id = 'json-' + Math.random().toString(36).substr(2, 9); - const summary = isArray ? - `${entries.length} items` : - `${entries.length} keys`; - - let html = `
`; - for (let i = 0; i < level; i++) { - html += ``; - } - html += ``; - html += `${isArray ? '[' : '{'}`; - html += summary; - html += `
`; - - html += `
`; - - entries.forEach(([key, value], index) => { - const isLast = index === entries.length - 1; - const valueIsObject = value !== null && typeof value === 'object'; + for (const [key, value] of Object.entries(obj)) { + const path = parentPath ? `${parentPath}.${key}` : key; - html += `
`; - for (let i = 0; i <= level; i++) { - html += ``; - } - - if (!isArray) { - html += `"${escapeHtml(key)}"`; - html += `:`; - } - - if (valueIsObject) { - html += renderJSONTree(value, level + 1); - } else { - if (typeof value === 'string') { - const displayValue = value.length > 100 ? - escapeHtml(value.substr(0, 100)) + '...' : - escapeHtml(value); - html += `"${displayValue}"`; - html += ``; - } else if (typeof value === 'number') { - html += `${value}`; - } else if (typeof value === 'boolean') { - html += `${value}`; - } else if (value === null) { - html += `null`; + if (value === null || typeof value !== 'object') { + // 基本类型 - 渲染字段 + const typeClass = value === null ? 'null' : typeof value; + let displayValue = ''; + let copyValue = ''; + + if (value === null) { + displayValue = 'null'; + copyValue = 'null'; + } else if (typeof value === 'string') { + displayValue = value; + copyValue = value; + } else if (typeof value === 'number' || typeof value === 'boolean') { + displayValue = String(value); + copyValue = String(value); } + + html += ` +
+
${escapeHtml(key)}
+
${escapeHtml(displayValue)}
+
+ +
+
+ `; + } else { + // 嵌套对象/数组 - 递归渲染 + html += renderJSONCard({[key]: value}, parentPath); } - - if (!isLast) { - html += `,`; - } - - html += `
`; - }); - - html += `
`; - - html += `
`; - for (let i = 0; i < level; i++) { - html += ``; } - html += `${isArray ? ']' : '}'}`; - html += `
`; return html; } // HTML 转义 function escapeHtml(str) { + if (typeof str !== 'string') { + str = String(str); + } const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } - // 切换折叠/展开 - function toggleJSON(id) { + // 切换区域折叠/展开 + function toggleSection(id) { const element = document.getElementById(id); - const toggle = element.previousElementSibling.querySelector('.json-toggle'); + const toggle = element.previousElementSibling.querySelector('.json-section-toggle'); if (element.classList.contains('collapsed')) { element.classList.remove('collapsed'); - toggle.classList.remove('collapsed'); - toggle.classList.add('expanded'); + toggle.textContent = '▼'; } else { element.classList.add('collapsed'); - toggle.classList.remove('expanded'); - toggle.classList.add('collapsed'); + toggle.textContent = '▶'; } } @@ -843,7 +833,7 @@ currentViewData = data; document.getElementById('view-modal-title').textContent = `查看数据 - ${tag}`; - document.getElementById('json-content').innerHTML = renderJSONTree(data); + document.getElementById('json-content').innerHTML = renderJSONCard(data); openModal('view-modal'); } else {