From c3e8ead8c02d1d1e14e3f4131a13ef72c01a07d3 Mon Sep 17 00:00:00 2001 From: K-hermes Date: Sat, 15 Aug 2026 12:35:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20JSON=20=E6=95=B0=E6=8D=AE=E6=A0=91?= =?UTF-8?q?=E5=BD=A2=E5=B1=95=E7=A4=BA=EF=BC=8C=E5=8F=AF=E6=8A=98=E5=8F=A0?= =?UTF-8?q?/=E5=B1=95=E5=BC=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 改进: - 树形结构展示 JSON 数据,层级清晰 - 点击三角形折叠/展开节点 - 悬停显示复制按钮,可复制单个字段 - 长字符串自动截断(超过 100 字符) - 显示数组/对象的项数统计 - 更好的可读性和交互体验 --- web/index.html | 237 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 214 insertions(+), 23 deletions(-) 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 = `
`; + 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'; + + 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 (!isLast) { + html += `,`; + } + + html += `
`; + }); + + html += `
`; + + html += `
`; + for (let i = 0; i < level; i++) { + html += ``; + } + html += `${isArray ? ']' : '}'}`; + html += `
`; + + return html; + } + + // HTML 转义 + function escapeHtml(str) { + const div = document.createElement('div'); + div.textContent = str; + return div.innerHTML; + } + + // 切换折叠/展开 + function toggleJSON(id) { + const element = document.getElementById(id); + const toggle = element.previousElementSibling.querySelector('.json-toggle'); + + if (element.classList.contains('collapsed')) { + element.classList.remove('collapsed'); + toggle.classList.remove('collapsed'); + toggle.classList.add('expanded'); + } else { + element.classList.add('collapsed'); + toggle.classList.remove('expanded'); + toggle.classList.add('collapsed'); + } + } + + // 复制单个值 + function copyValue(value) { + navigator.clipboard.writeText(value).then(() => { + showToast('已复制'); + }).catch(() => { + showToast('复制失败', 'error'); }); } @@ -652,7 +843,7 @@ currentViewData = data; document.getElementById('view-modal-title').textContent = `查看数据 - ${tag}`; - document.getElementById('json-content').innerHTML = syntaxHighlight(data); + document.getElementById('json-content').innerHTML = renderJSONTree(data); openModal('view-modal'); } else {