diff --git a/web/index.html b/web/index.html index fec3cb1..1cdeb9d 100644 --- a/web/index.html +++ b/web/index.html @@ -275,21 +275,69 @@ gap: 10px; } - /* JSON 展示样式 */ + /* JSON 树形展示样式 */ .json-viewer { background: #f8f9fa; border-radius: 8px; padding: 20px; font-family: 'Courier New', monospace; - font-size: 13px; - line-height: 1.6; + font-size: 14px; + line-height: 1.8; overflow-x: auto; max-height: 60vh; } + .json-line { + display: flex; + align-items: center; + padding: 2px 0; + position: relative; + } + + .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; font-weight: bold; + margin-right: 8px; + } + + .json-separator { + color: #999; + margin: 0 4px; } .json-string { @@ -308,6 +356,46 @@ color: #6f42c1; } + .json-bracket { + color: #666; + font-weight: bold; + } + + .json-copy-btn { + opacity: 0; + margin-left: 10px; + padding: 2px 8px; + font-size: 11px; + background: #667eea; + color: white; + border: none; + border-radius: 3px; + cursor: pointer; + transition: opacity 0.2s; + } + + .json-line:hover .json-copy-btn { + opacity: 1; + } + + .json-copy-btn:hover { + background: #5568d3; + } + + .json-children { + display: block; + } + + .json-children.collapsed { + display: none; + } + + .json-summary { + color: #999; + font-style: italic; + margin-left: 8px; + } + /* Toast 通知 */ .toast { position: fixed; @@ -525,28 +613,131 @@ document.getElementById(modalId).classList.remove('active'); } - // JSON 格式化显示 - function syntaxHighlight(json) { - if (typeof json !== 'string') { - json = JSON.stringify(json, null, 2); + // JSON 树形渲染 + function renderJSONTree(obj, level = 0) { + const lines = []; + const indent = ' '.repeat(level * 2); + + if (obj === null) { + return `null`; } - json = json.replace(/&/g, '&').replace(//g, '>'); - - return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) { - let cls = 'json-number'; - if (/^"/.test(match)) { - if (/:$/.test(match)) { - cls = 'json-key'; - } else { - cls = 'json-string'; - } - } else if (/true|false/.test(match)) { - cls = 'json-boolean'; - } else if (/null/.test(match)) { - cls = 'json-null'; + 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}`; } - return '' + match + ''; + } + + const isArray = Array.isArray(obj); + const entries = isArray ? obj.map((v, i) => [i, v]) : Object.entries(obj); + const isEmpty = entries.length === 0; + + if (isEmpty) { + return isArray ? + `[]` : + `{}`; + } + + const id = 'json-' + Math.random().toString(36).substr(2, 9); + const summary = isArray ? + `${entries.length} items` : + `${entries.length} keys`; + + let html = `