refactor: 改为卡片式 JSON 展示
改进: - 采用卡片式布局,每个字段单独一行 - 左侧标签(字段名)+ 右侧值 + 复制按钮 - 对象/数组折叠为蓝色标题区域,点击展开/折叠 - 清晰的视觉层次,更易阅读 - 保留语法高亮(字符串绿色、数字蓝色、布尔值红色等)
This commit is contained in:
+153
-163
@@ -275,125 +275,131 @@
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* JSON 树形展示样式 */
|
/* JSON 卡片式展示 */
|
||||||
.json-viewer {
|
.json-viewer {
|
||||||
background: #f8f9fa;
|
background: #f8f9fa;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 20px;
|
padding: 0;
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.8;
|
|
||||||
overflow-x: auto;
|
|
||||||
max-height: 60vh;
|
max-height: 60vh;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.json-line {
|
.json-section {
|
||||||
display: flex;
|
margin-bottom: 10px;
|
||||||
align-items: center;
|
|
||||||
padding: 2px 0;
|
|
||||||
position: relative;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.json-line:hover {
|
.json-section-title {
|
||||||
background: rgba(102, 126, 234, 0.05);
|
background: #667eea;
|
||||||
}
|
color: white;
|
||||||
|
padding: 12px 20px;
|
||||||
.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;
|
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 {
|
.json-section-title:hover {
|
||||||
color: #999;
|
background: #5568d3;
|
||||||
margin: 0 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.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;
|
color: #22863a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.json-number {
|
.json-field-value.number {
|
||||||
color: #005cc5;
|
color: #005cc5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.json-boolean {
|
.json-field-value.boolean {
|
||||||
color: #d73a49;
|
color: #d73a49;
|
||||||
}
|
}
|
||||||
|
|
||||||
.json-null {
|
.json-field-value.null {
|
||||||
color: #6f42c1;
|
color: #6f42c1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.json-bracket {
|
.json-field-actions {
|
||||||
color: #666;
|
margin-left: 10px;
|
||||||
font-weight: bold;
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.json-copy-btn {
|
.json-copy-btn {
|
||||||
opacity: 0;
|
padding: 4px 10px;
|
||||||
margin-left: 10px;
|
font-size: 12px;
|
||||||
padding: 2px 8px;
|
|
||||||
font-size: 11px;
|
|
||||||
background: #667eea;
|
background: #667eea;
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 3px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: opacity 0.2s;
|
white-space: nowrap;
|
||||||
}
|
|
||||||
|
|
||||||
.json-line:hover .json-copy-btn {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.json-copy-btn:hover {
|
.json-copy-btn:hover {
|
||||||
background: #5568d3;
|
background: #5568d3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.json-children {
|
.json-raw-btn {
|
||||||
display: block;
|
padding: 4px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.json-children.collapsed {
|
.json-raw-btn:hover {
|
||||||
display: none;
|
background: #5a6268;
|
||||||
}
|
|
||||||
|
|
||||||
.json-summary {
|
|
||||||
color: #999;
|
|
||||||
font-style: italic;
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Toast 通知 */
|
/* Toast 通知 */
|
||||||
@@ -613,122 +619,106 @@
|
|||||||
document.getElementById(modalId).classList.remove('active');
|
document.getElementById(modalId).classList.remove('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSON 树形渲染
|
// JSON 卡片式渲染
|
||||||
function renderJSONTree(obj, level = 0) {
|
function renderJSONCard(obj, parentPath = '') {
|
||||||
const lines = [];
|
let html = '';
|
||||||
const indent = ' '.repeat(level * 2);
|
|
||||||
|
|
||||||
if (obj === null) {
|
if (typeof obj !== 'object' || obj === null) {
|
||||||
return `<span class="json-null">null</span>`;
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof obj !== 'object') {
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
if (typeof obj === 'string') {
|
const path = parentPath ? `${parentPath}.${key}` : key;
|
||||||
return `<span class="json-string">"${escapeHtml(obj)}"</span>`;
|
|
||||||
} else if (typeof obj === 'number') {
|
if (value !== null && typeof value === 'object') {
|
||||||
return `<span class="json-number">${obj}</span>`;
|
// 对象或数组 - 创建折叠区域
|
||||||
} else if (typeof obj === 'boolean') {
|
const isArray = Array.isArray(value);
|
||||||
return `<span class="json-boolean">${obj}</span>`;
|
const itemCount = isArray ? value.length : Object.keys(value).length;
|
||||||
|
const sectionId = 'section-' + Math.random().toString(36).substr(2, 9);
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="json-section">
|
||||||
|
<div class="json-section-title" onclick="toggleSection('${sectionId}')">
|
||||||
|
<span>${key} ${isArray ? `[${itemCount} items]` : `{${itemCount} keys}`}</span>
|
||||||
|
<span class="json-section-toggle">▼</span>
|
||||||
|
</div>
|
||||||
|
<div class="json-section-body" id="${sectionId}">
|
||||||
|
${renderJSONFields(value, path)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isArray = Array.isArray(obj);
|
return html;
|
||||||
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);
|
function renderJSONFields(obj, parentPath = '') {
|
||||||
const summary = isArray ?
|
let html = '';
|
||||||
`<span class="json-summary">${entries.length} items</span>` :
|
|
||||||
`<span class="json-summary">${entries.length} keys</span>`;
|
|
||||||
|
|
||||||
let html = `<div class="json-line">`;
|
if (typeof obj !== 'object' || obj === null) {
|
||||||
for (let i = 0; i < level; i++) {
|
return '';
|
||||||
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) {
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
html += `<span class="json-key">"${escapeHtml(key)}"</span>`;
|
const path = parentPath ? `${parentPath}.${key}` : key;
|
||||||
html += `<span class="json-separator">:</span>`;
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (valueIsObject) {
|
html += `
|
||||||
html += renderJSONTree(value, level + 1);
|
<div class="json-field">
|
||||||
|
<div class="json-field-label">${escapeHtml(key)}</div>
|
||||||
|
<div class="json-field-value ${typeClass}">${escapeHtml(displayValue)}</div>
|
||||||
|
<div class="json-field-actions">
|
||||||
|
<button class="json-copy-btn" onclick="copyValue(\`${escapeHtml(copyValue).replace(/`/g, '\\`')}\`)">复制</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
} else {
|
} else {
|
||||||
if (typeof value === 'string') {
|
// 嵌套对象/数组 - 递归渲染
|
||||||
const displayValue = value.length > 100 ?
|
html += renderJSONCard({[key]: value}, parentPath);
|
||||||
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;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTML 转义
|
// HTML 转义
|
||||||
function escapeHtml(str) {
|
function escapeHtml(str) {
|
||||||
|
if (typeof str !== 'string') {
|
||||||
|
str = String(str);
|
||||||
|
}
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.textContent = str;
|
div.textContent = str;
|
||||||
return div.innerHTML;
|
return div.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换折叠/展开
|
// 切换区域折叠/展开
|
||||||
function toggleJSON(id) {
|
function toggleSection(id) {
|
||||||
const element = document.getElementById(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')) {
|
if (element.classList.contains('collapsed')) {
|
||||||
element.classList.remove('collapsed');
|
element.classList.remove('collapsed');
|
||||||
toggle.classList.remove('collapsed');
|
toggle.textContent = '▼';
|
||||||
toggle.classList.add('expanded');
|
|
||||||
} else {
|
} else {
|
||||||
element.classList.add('collapsed');
|
element.classList.add('collapsed');
|
||||||
toggle.classList.remove('expanded');
|
toggle.textContent = '▶';
|
||||||
toggle.classList.add('collapsed');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -843,7 +833,7 @@
|
|||||||
currentViewData = data;
|
currentViewData = data;
|
||||||
|
|
||||||
document.getElementById('view-modal-title').textContent = `查看数据 - ${tag}`;
|
document.getElementById('view-modal-title').textContent = `查看数据 - ${tag}`;
|
||||||
document.getElementById('json-content').innerHTML = renderJSONTree(data);
|
document.getElementById('json-content').innerHTML = renderJSONCard(data);
|
||||||
|
|
||||||
openModal('view-modal');
|
openModal('view-modal');
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user