feat: JSON 数据树形展示,可折叠/展开
改进: - 树形结构展示 JSON 数据,层级清晰 - 点击三角形折叠/展开节点 - 悬停显示复制按钮,可复制单个字段 - 长字符串自动截断(超过 100 字符) - 显示数组/对象的项数统计 - 更好的可读性和交互体验
This commit is contained in:
+214
-23
@@ -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 `<span class="json-null">null</span>`;
|
||||
}
|
||||
|
||||
json = json.replace(/&/g, '&').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 `<span class="json-string">"${escapeHtml(obj)}"</span>`;
|
||||
} else if (typeof obj === 'number') {
|
||||
return `<span class="json-number">${obj}</span>`;
|
||||
} else if (typeof obj === 'boolean') {
|
||||
return `<span class="json-boolean">${obj}</span>`;
|
||||
}
|
||||
return '<span class="' + cls + '">' + match + '</span>';
|
||||
}
|
||||
|
||||
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 ?
|
||||
`<span class="json-bracket">[]</span>` :
|
||||
`<span class="json-bracket">{}</span>`;
|
||||
}
|
||||
|
||||
const id = 'json-' + Math.random().toString(36).substr(2, 9);
|
||||
const summary = isArray ?
|
||||
`<span class="json-summary">${entries.length} items</span>` :
|
||||
`<span class="json-summary">${entries.length} keys</span>`;
|
||||
|
||||
let html = `<div class="json-line">`;
|
||||
for (let i = 0; i < level; i++) {
|
||||
html += `<span class="json-indent"></span>`;
|
||||
}
|
||||
html += `<span class="json-toggle expanded" onclick="toggleJSON('${id}')"></span>`;
|
||||
html += `<span class="json-bracket">${isArray ? '[' : '{'}</span>`;
|
||||
html += summary;
|
||||
html += `</div>`;
|
||||
|
||||
html += `<div class="json-children" id="${id}">`;
|
||||
|
||||
entries.forEach(([key, value], index) => {
|
||||
const isLast = index === entries.length - 1;
|
||||
const valueIsObject = value !== null && typeof value === 'object';
|
||||
|
||||
html += `<div class="json-line">`;
|
||||
for (let i = 0; i <= level; i++) {
|
||||
html += `<span class="json-indent"></span>`;
|
||||
}
|
||||
|
||||
if (!isArray) {
|
||||
html += `<span class="json-key">"${escapeHtml(key)}"</span>`;
|
||||
html += `<span class="json-separator">:</span>`;
|
||||
}
|
||||
|
||||
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 += `<span class="json-string">"${displayValue}"</span>`;
|
||||
html += `<button class="json-copy-btn" onclick="copyValue('${escapeHtml(value).replace(/'/g, "\\'")}')">复制</button>`;
|
||||
} else if (typeof value === 'number') {
|
||||
html += `<span class="json-number">${value}</span>`;
|
||||
} else if (typeof value === 'boolean') {
|
||||
html += `<span class="json-boolean">${value}</span>`;
|
||||
} else if (value === null) {
|
||||
html += `<span class="json-null">null</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isLast) {
|
||||
html += `<span class="json-separator">,</span>`;
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
});
|
||||
|
||||
html += `</div>`;
|
||||
|
||||
html += `<div class="json-line">`;
|
||||
for (let i = 0; i < level; i++) {
|
||||
html += `<span class="json-indent"></span>`;
|
||||
}
|
||||
html += `<span class="json-bracket">${isArray ? ']' : '}'}</span>`;
|
||||
html += `</div>`;
|
||||
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user