本文最后更新于 35 天前,其中的信息可能已经有所发展或是发生改变。
怎么说呢,工具这个东西,就是出自懒人之手
老规矩,先交代一下事件背景。
背景
某次在渗透测试的时候,发现一个接口,泄露了几千条个人信息。包含姓名、工号、手机号、邮箱等信息。
巧的是,另一个接口(一个类似于用户绑定的接口)也存在一个漏洞,如果用户满足某个条件,可以越权。
所以,我需要的就是,将泄露的这几千条手机号提取出来,然后用作第二个接口的爆破字典。
JSON2Table 工具
其实网上有不少 Json 转 table 的工具,但是,别人的哪有自己写的有成就感呢?
于是我决定自己写一个 JSON 转 Table 的工具,并且,要支持一键复制列,最好,还能自动识别 Burp 的响应包并且提取出 JSON 格式的 response。
看看效果(代码在文末)
在线体验
https://guide.hola-security.cn/mytools/json2table-v2.1/index.html
HTML 代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>JSON转换工具</title>
<link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css">
<link rel="stylesheet" href="./json2table.index.css">
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script src="https://unpkg.com/element-plus/dist/index.full.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
<script src="https://unpkg.com/@element-plus/icons-vue"></script>
</head>
<body>
<div id="app">
<div class="left-panel">
<div class="panel-header">
<h3 class="panel-title">
<el-icon><Document /></el-icon>
<span>JSON数据处理</span>
</h3>
</div>
<div class="action-buttons">
<el-upload
:auto-upload="false"
:show-file-list="false"
:on-change="handleFileChange"
>
<el-button type="primary" size="small">
<el-icon><Upload /></el-icon>
上传文件
</el-button>
</el-upload>
<el-button
type="success"
size="small"
:loading="state.treeLoading"
@click="parseJSON"
>
<el-icon><Sunrise /></el-icon>
解析JSON
</el-button>
</div>
<el-input
v-model="state.jsonInput"
type="textarea"
:rows="8"
placeholder="粘贴 JSON 内容"
@keyup.ctrl.enter="parseJSON"
@paste="handlePaste"
></el-input>
<div class="panel-header">
<h3 class="panel-title">
<el-icon><List /></el-icon>
<span>JSON结构树</span>
</h3>
</div>
<!-- 字段选择树 -->
<el-tree
ref="fieldTreeRef"
v-loading="state.treeLoading"
v-show="state.rootNode"
class="json-tree"
:data="treeData"
node-key="path"
:props="treeProps"
:expand-on-click-node="false"
:default-expanded-keys="defaultExpandedKeys"
:load="loadTreeNode"
lazy
@node-click="handleNodeClick"
@node-expand="handleNodeExpand"
@node-collapse="handleNodeCollapse"
>
<!-- 自定义节点内容 -->
<template #default="{ node, data }">
<div class="tree-node-content">
<!-- 添加展开/折叠图标 -->
<span
class="expand-icon"
@click.stop="toggleNodeExpand(node)"
>
<el-icon v-if="node.expanded">
<ArrowDown />
</el-icon>
<el-icon v-else>
<ArrowRight />
</el-icon>
</span>
<span class="node-name" @click.stop="handleNodeSelect(data, node)">
{{ data.name }}
</span>
<!-- 显示类型标签 -->
<el-tag v-if="data.type" size="small" class="type-tag" :type="getTagType(data.type)">
{{ getTypeLabel(data.type) }}
</el-tag>
<!-- 对象预览 -->
<span v-if="data.type === 'object' && data.keys" class="node-value-preview">
{{ `{ ${data.keys ? data.keys.length : 0} keys }` }}
</span>
<!-- 数组预览 -->
<span v-if="data.type === 'array'" class="node-value-preview">
{{ `[ ${data.length} items ]` }}
</span>
<!-- 基础值预览 -->
<span v-if="isBasicType(data.type) && data.value !== undefined" class="node-value-preview">
{{ formatPreviewValue(data.value) }}
</span>
<!-- 如果是数组元素,显示索引 -->
<span v-if="data.isArrayItem" class="array-index">[{{ data.index }}]</span>
</div>
</template>
</el-tree>
</div>
<!-- 右侧表格区域 -->
<div class="right-panel">
<div class="panel-header">
<h3 class="panel-title">
<el-icon><List /></el-icon>
<span>数据视图</span>
</h3>
</div>
<!-- 独立提示容器 -->
<div class="alert-container">
<el-alert
v-if="state.errorMsg"
:title="state.errorMsg"
type="error"
show-icon
closable
@close="state.errorMsg = ''"
/>
</div>
<div class="data-section">
<!-- 主数据视图 -->
<template v-if="!state.currentNode">
<!-- 当数据是对象且不是数组时,使用描述列表展示 -->
<div v-if="state.isObjectView" class="detail-view">
<h3>根节点 <el-tag size="small">对象</el-tag></h3>
<div class="descriptions-view">
<div class="descriptions-header">属性列表</div>
<template v-for="(value, key) in state.objectData" :key="key">
<div class="descriptions-item">
<div class="descriptions-item-label">{{ key }}</div>
<div class="descriptions-item-value">
<span>{{ formatObjectValue(value) }}</span>
<el-tag v-if="value !== null" size="mini">{{ getValueType(value) }}</el-tag>
</div>
</div>
</template>
</div>
</div>
<!-- 当数据是数组时,使用表格展示 -->
<template v-else-if="state.isArrayView">
<div class="table-container">
<el-table
v-loading="state.tableLoading"
:data="state.tableData"
border
stripe
style="width: 100%; height: 100%"
>
<template #empty>
<div class="custom-empty">
<el-icon class="custom-empty-icon"><Document /></el-icon>
<p>暂无数据,请先输入或上传JSON文件</p>
</div>
</template>
<el-table-column
v-for="field in state.selectedFields"
:key="field"
:prop="field"
>
<!-- 使用内联SVG图标 -->
<template #header="{ column }">
<div class="column-header">
<span>{{ formatHeader(field) }}</span>
<el-tooltip
effect="dark"
content="复制整列"
placement="top"
>
<span
class="copy-column-btn"
@click="copyColumnData(field)"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="1em" height="1em" fill="currentColor">
<path d="M8 4v12c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2h-8c-1.1 0-2 .9-2 2zm10 4v12h-8V4h8zM4 8h2v10h10v2H6c-1.1 0-2-.9-2-2V8z"/>
</svg>
</span>
</el-tooltip>
</div>
</template>
<template #default="{ row }">
<template v-if="row[field] !== undefined">
<span v-if="!isComplexValue(row[field])">
{{ formatSimpleValue(row[field]) }}
</span>
<el-tooltip
v-else
effect="light"
placement="top"
:disabled="!isComplexValue(row[field])"
>
<span class="nested-cell">
{{ formatComplexValue(row[field]) }}
</span>
<template #content>
<pre>{{ formatTooltip(row[field]) }}</pre>
</template>
</el-tooltip>
</template>
<span v-else class="null-value">N/A</span>
</template>
</el-table-column>
</el-table>
</div>
</template>
</template>
<!-- 节点详情视图 -->
<div v-if="state.currentNode" class="detail-view">
<h3>{{ state.currentNode.name }} <el-tag size="small">{{ state.currentNode.type }}</el-tag></h3>
<!-- 对象类型的值使用描述列表展示 -->
<div v-if="state.currentNode.type === 'object' && state.currentNode.value" class="descriptions-view">
<div class="descriptions-header">属性列表</div>
<template v-for="(value, key) in state.currentNode.value" :key="key">
<div class="descriptions-item">
<div class="descriptions-item-label">{{ key }}</div>
<div class="descriptions-item-value">
<span>{{ formatObjectValue(value) }}</span>
<el-tag v-if="value !== null" size="mini">{{ getValueType(value) }}</el-tag>
</div>
</div>
</template>
</div>
<!-- 数组类型的值使用表格展示 -->
<div v-else-if="state.currentNode.type === 'array' && Array.isArray(state.currentNode.value)">
<div class="table-container">
<el-table
:data="state.currentNode.value"
border
stripe
style="width: 100%"
>
<el-table-column type="index" width="50" label="序号">
<template #default="{ $index }">
{{ $index + 1 }}
</template>
</el-table-column>
<el-table-column v-if="isSimpleArray(state.currentNode.value)" label="值">
<template #default="{ row }">
{{ row }}
</template>
</el-table-column>
<template v-else>
<!-- 修复:正确遍历键名数组 -->
<el-table-column
v-for="(key, index) in getFirstArrayItemKeys(state.currentNode.value)"
:key="index"
:prop="key"
:label="key"
>
<template #header="{ column }">
<div class="column-header">
<span>{{ key }}</span>
<el-tooltip
effect="dark"
content="复制整列"
placement="top"
>
<span
class="copy-column-btn"
@click="copyColumnData(key)"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="1em" height="1em" fill="currentColor">
<path d="M8 4v12c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2h-8c-1.1 0-2 .9-2 2zm10 4v12h-8V4h8zM4 8h2v10h10v2H6c-1.1 0-2-.9-2-2V8z"/>
</svg>
</span>
</el-tooltip>
</div>
</template>
<template #default="{ row }">
{{ formatObjectValue(row[key]) }}
</template>
</el-table-column>
</template>
</el-table>
</div>
</div>
<!-- 基本类型的值直接显示 -->
<div v-else-if="isBasicType(state.currentNode.type)" style="margin-top: 15px;">
<div class="descriptions-item">
<div class="descriptions-item-label">值</div>
<div class="descriptions-item-value">
<span>{{ state.currentNode.value }}</span>
</div>
</div>
<div class="descriptions-item">
<div class="descriptions-item-label">类型</div>
<div class="descriptions-item-value">
<el-tag size="small">{{ state.currentNode.type }}</el-tag>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="perf-warning" class="large-data-warning" style="display: none; position: fixed; bottom: 20px; right: 20px;">
<el-icon><Warning /></el-icon>
<span>检测到大数据集处理中,请稍候...</span>
</div>
</div>
<script src="./json2table.index.js"></script>
<script>
// 创建用于解析的 Web Worker
window.jsonParserWorker = new Worker(URL.createObjectURL(new Blob([`
self.addEventListener('message', function(e) {
try {
const jsonData = JSON.parse(e.data);
postMessage({ rootNode: createRootNode(jsonData) });
} catch (error) {
postMessage({ type: 'error', message: 'Worker解析失败: ' + error.message });
}
function createRootNode(rawData) {
const detectType = (value) => {
if (Array.isArray(value)) return 'array';
if (value === null) return 'null';
if (typeof value === 'object') return 'object';
return typeof value;
};
const createNode = (path, name, type, value, parentPath = '', isArrayItem = false, index = -1) => {
const node = {
path,
name,
type,
value,
parentPath,
isArrayItem,
index,
loaded: false,
children: [],
keys: type === 'object' && value ? Object.keys(value) : null,
length: type === 'array' && value ? value.length : 0,
hasChildren: false
};
// 安全处理null值
if (value === null) {
node.isLeaf = true;
node.hasChildren = false;
return node;
}
if (type === 'array' && value && value.length > 0) {
node.hasChildren = true;
node.isLeaf = false;
} else if (type === 'object' && value && Object.keys(value).length > 0) {
node.hasChildren = true;
node.isLeaf = false;
} else {
node.isLeaf = true;
node.hasChildren = false;
}
return node;
};
const dataType = detectType(rawData);
let rootNode = createNode('root', '根节点', dataType, rawData);
// 创建初始子节点
if (dataType === 'array' && Array.isArray(rawData)) {
// 处理数组根节点
rootNode.children = rawData.slice(0, 100).map((item, idx) => {
const childType = detectType(item);
const childPath = \`root[\${idx}]\`;
const childName = \`[\${idx}]\`;
return createNode(childPath, childName, childType, item, 'root', true, idx);
});
if (rawData.length > 100) {
rootNode.children.push({
path: 'root.more',
name: \`[查看更多...共\${rawData.length}项]\`,
type: 'more',
parentPath: 'root',
isLeaf: true,
totalItems: rawData.length
});
}
} else if (dataType === 'object' && rawData) {
// 处理对象根节点
const entries = Object.entries(rawData);
rootNode.children = entries.slice(0, 100).map(([key, val]) => {
const childType = detectType(val);
const childPath = \`root.\${key}\`;
return createNode(childPath, key, childType, val, 'root');
});
if (entries.length > 100) {
rootNode.children.push({
path: 'root.more',
name: \`{查看更多...共\${entries.length}键}\`,
type: 'more',
parentPath: 'root',
isLeaf: true,
totalItems: entries.length
});
}
}
return rootNode;
}
});
`])));
</script>
</body>
</html>
JS 代码
const { createApp, reactive, ref, nextTick, onBeforeUnmount } = Vue;
const { ElMessage, ElMessageBox, ElLoading } = ElementPlus;
const app = createApp({
components: {
Document: window.ElementPlusIconsVue.Document,
List: window.ElementPlusIconsVue.List,
ArrowLeft: window.ElementPlusIconsVue.ArrowLeft,
Upload: window.ElementPlusIconsVue.Upload,
Sunrise: window.ElementPlusIconsVue.Sunrise,
Warning: window.ElementPlusIconsVue.WarningFilled,
},
setup() {
const fieldTreeRef = ref(null);
const state = reactive({
jsonInput: '',
rootNode: null,
selectedFields: [],
tableData: [],
loading: false,
errorMsg: '',
currentNode: null,
objectData: {},
isArrayView: false,
isObjectView: false,
treeLoading: false,
tableLoading: false,
lastPasteTime: 0,
perfWarning: false,
debugMode: true
});
// 树组件相关状态
const treeData = ref([]);
const defaultExpandedKeys = ref([]);
const logs = ref([]);
// 添加日志记录
const log = (message, type = 'info') => {
logs.value.push({
time: new Date().toLocaleTimeString(),
message,
type
});
// 保持日志在100条以内
if (logs.value.length > 100) logs.value.shift();
};
// 树组件配置
const treeProps = {
label: 'name',
children: 'children',
isLeaf: 'isLeaf'
};
// 处理文件上传
const handleFileChange = (file) => {
log(`上传JSON文件: ${file.name}`, 'info');
const reader = new FileReader();
reader.onload = e => {
state.jsonInput = e.target.result;
parseJSON();
};
reader.readAsText(file.raw);
};
// 处理粘贴事件(添加粘贴时的加载提示)
const handlePaste = (event) => {
event.preventDefault();
const pasteText = (event.clipboardData || window.clipboardData).getData('text');
const now = Date.now();
if (now - state.lastPasteTime < 500) return;
state.lastPasteTime = now;
state.jsonInput = '';
// 显示粘贴加载提示
const pasteLoading = ElLoading.service({
lock: true,
text: '处理粘贴内容...',
background: 'rgba(255, 255, 255, 0.9)',
spinner: false,
customClass: 'paste-loading'
});
nextTick(() => {
state.jsonInput = pasteText;
log('从剪贴板粘贴JSON内容', 'info');
// 关闭粘贴加载提示
pasteLoading.close();
// 开始解析JSON
parseJSON();
});
};
// 检测数据类型
const detectType = (value) => {
if (Array.isArray(value)) return 'array';
if (value === null) return 'null';
if (typeof value === 'object') return 'object';
return typeof value;
};
// 创建节点
const createNode = (path, name, type, value, parentPath = '', isArrayItem = false, index = -1) => {
const node = {
path,
name,
type,
value,
parentPath,
isArrayItem,
index,
loaded: false,
children: [],
keys: type === 'object' && value ? Object.keys(value) : null,
length: type === 'array' && value ? value.length : 0,
hasChildren: false
};
// 安全处理null值
if (value === null) {
node.isLeaf = true;
node.hasChildren = false;
return node;
}
if (type === 'array' && value && value.length > 0) {
node.hasChildren = true;
node.isLeaf = false;
} else if (type === 'object' && value && Object.keys(value).length > 0) {
node.hasChildren = true;
node.isLeaf = false;
} else {
node.isLeaf = true;
node.hasChildren = false;
}
return node;
};
// 节点懒加载
const loadTreeNode = async (node, resolve) => {
try {
if (node.level === 0) {
log(`加载根节点子节点`);
return resolve(state.rootNode?.children || []);
}
const data = node.data;
log(`加载节点: ${data.path}, 类型: ${data.type}, 层级: ${node.level}`);
if (data.loaded) {
log(`节点已加载,返回缓存子节点`, 'success');
return resolve(data.children || []);
}
if (data.hasChildren && !data.loaded) {
log(`正在加载子节点...`);
// 模拟延迟加载
await new Promise(resolve => setTimeout(resolve, 30));
let children = [];
// 处理数组类型节点
if (data.type === 'array' && Array.isArray(data.value)) {
if (data.value && data.value.length > 0) {
children = data.value.slice(0, 100).map((item, idx) => {
const childType = detectType(item);
const childPath = `${data.path}[${idx}]`;
const childName = `[${idx}]`;
return createNode(childPath, childName, childType, item, data.path, true, idx);
});
// 添加"查看更多"节点
if (data.value.length > 100) {
children.push({
path: `${data.path}.more`,
name: `[查看更多...共${data.value.length}项]`,
type: 'more',
parentPath: data.path,
isLeaf: true,
totalItems: data.value.length
});
}
} else {
// 空数组处理
node.hasChildren = false;
node.isLeaf = true;
}
}
// 处理对象类型节点
else if (data.type === 'object' && data.value) {
if (data.value && typeof data.value === 'object') {
const entries = Object.entries(data.value);
if (entries.length > 0) {
children = entries.slice(0, 100).map(([key, val]) => {
const childType = detectType(val);
const childPath = `${data.path}.${key}`;
return createNode(childPath, key, childType, val, data.path);
});
// 添加"查看更多"节点
if (entries.length > 100) {
children.push({
path: `${data.path}.more`,
name: `{查看更多...共${entries.length}键}`,
type: 'more',
parentPath: data.path,
isLeaf: true,
totalItems: entries.length
});
}
} else {
// 空对象处理
node.hasChildren = false;
node.isLeaf = true;
}
} else {
// 值为 null 的对象处理
node.hasChildren = false;
node.isLeaf = true;
}
} else {
// 非对象/数组处理
node.hasChildren = false;
node.isLeaf = true;
}
// 更新节点状态
data.children = children;
data.loaded = true;
log(`成功加载 ${children.length} 个子节点`, 'success');
resolve(children);
} else {
log(`节点没有子节点可加载`);
resolve([]);
}
} catch (error) {
log(`节点加载失败: ${error.message}`, 'error');
resolve([]);
} finally {
node.loading = false;
}
};
// 处理节点点击
const handleNodeClick = (data, node) => {
log(`点击节点: ${data.path}, 类型: ${data.type}`);
// 自动展开/折叠点击的节点
if (node.expanded) {
node.collapse();
} else {
node.expand();
}
state.currentNode = data;
if (data.type === 'array' && Array.isArray(data.value)) {
state.tableData = data.value;
log(`更新表格数据: ${data.value.length} 条记录`);
}
};
// 处理节点展开
const handleNodeExpand = (data, node) => {
log(`展开节点: ${data.path}`);
defaultExpandedKeys.value.push(data.path);
};
// 处理节点折叠
const handleNodeCollapse = (data, node) => {
log(`折叠节点: ${data.path}`);
const index = defaultExpandedKeys.value.indexOf(data.path);
if (index > -1) {
defaultExpandedKeys.value.splice(index, 1);
}
};
// 主线程解析JSON
const parseInMainThread = () => {
try {
if (!state.jsonInput.trim()) {
throw new Error('JSON内容不能为空');
}
let rawData;
try {
rawData = JSON.parse(state.jsonInput);
} catch (e) {
const jsonStart = state.jsonInput.indexOf('{');
const jsonEnd = state.jsonInput.lastIndexOf('}') + 1;
if (jsonStart !== -1 && jsonEnd !== -1 && jsonEnd > jsonStart) {
const jsonStr = state.jsonInput.substring(jsonStart, jsonEnd);
rawData = JSON.parse(jsonStr);
} else {
throw new Error('无法找到有效的JSON内容');
}
}
const dataType = detectType(rawData);
log(`主线程解析成功,类型: ${dataType}`);
// 创建根节点
const rootNode = createNode('root', '根节点', dataType, rawData);
// 为根节点添加子节点
if (dataType === 'array' && Array.isArray(rawData)) {
rootNode.children = rawData.slice(0, 100).map((item, idx) => {
const childType = detectType(item);
const childPath = `root[${idx}]`;
const childName = `[${idx}]`;
return createNode(childPath, childName, childType, item, 'root', true, idx);
});
if (rawData.length > 100) {
rootNode.children.push({
path: 'root.more',
name: `[查看更多...共${rawData.length}项]`,
type: 'more',
parentPath: 'root',
isLeaf: true,
totalItems: rawData.length
});
}
} else if (dataType === 'object' && rawData) {
const entries = Object.entries(rawData);
rootNode.children = entries.slice(0, 100).map(([key, val]) => {
const childType = detectType(val);
const childPath = `root.${key}`;
return createNode(childPath, key, childType, val, 'root');
});
if (entries.length > 100) {
rootNode.children.push({
path: 'root.more',
name: `{查看更多...共${entries.length}键}`,
type: 'more',
parentPath: 'root',
isLeaf: true,
totalItems: entries.length
});
}
}
return rootNode;
} catch (e) {
throw new Error('解析失败: ' + e.message);
}
};
// 使用Worker处理JSON解析
const startWorkerProcessing = (jsonInput) => {
return new Promise((resolve, reject) => {
if (!window.jsonParserWorker) {
reject('浏览器不支持Web Worker,将使用主线程处理');
return;
}
const worker = window.jsonParserWorker;
worker.onmessage = (e) => {
if (e.data.type === 'error') {
reject(e.data.message);
} else {
resolve(e.data.rootNode);
}
};
worker.onerror = (error) => {
reject('Worker处理错误: ' + error.message);
};
worker.postMessage(jsonInput);
});
};
// 解析JSON主入口
const parseJSON = async () => {
log('开始解析JSON...');
// 显示简洁的文字加载提示
const loading = ElLoading.service({
lock: true,
text: '解析中...',
background: 'rgba(255, 255, 255, 0.9)',
spinner: false,
customClass: 'simple-loading'
});
// 强制重绘确保loading显示
await new Promise(resolve => {
requestAnimationFrame(() => {
requestAnimationFrame(resolve);
});
});
try {
// 设置加载状态
state.treeLoading = true;
state.errorMsg = '';
logs.value = [];
// 重置所有状态
state.rootNode = null;
state.currentNode = null;
state.perfWarning = false;
defaultExpandedKeys.value = ['root'];
if (!state.jsonInput.trim()) {
throw new Error('JSON内容不能为空');
}
// 检查是否是大数据集
const useWorker = state.jsonInput.length > 50000;
let rootNode;
// 显示性能警告
if (useWorker) {
log('检测到大尺寸JSON数据,使用Worker处理');
state.perfWarning = true;
}
if (useWorker && window.jsonParserWorker) {
try {
rootNode = await startWorkerProcessing(state.jsonInput);
log('Worker处理完成');
} catch (e) {
log('Worker处理失败,使用主线程: ' + e.message, 'warning');
rootNode = parseInMainThread();
}
} else {
log('使用主线程处理');
rootNode = parseInMainThread();
}
// 更新状态
state.rootNode = rootNode;
treeData.value = [state.rootNode];
log('JSON解析完成,根节点加载成功');
// 默认展开根节点
defaultExpandedKeys.value = ['root'];
// 默认选中根节点
state.currentNode = state.rootNode;
log('默认选中根节点');
// 如果根节点是数组,更新表格数据
if (state.rootNode.type === 'array' && Array.isArray(state.rootNode.value)) {
state.tableData = state.rootNode.value;
log(`更新表格数据: ${state.rootNode.value.length} 条记录`);
}
// 如果根节点是对象,更新对象数据
if (state.rootNode.type === 'object' && state.rootNode.value) {
state.objectData = state.rootNode.value;
state.isObjectView = true;
log(`更新对象数据: ${Object.keys(state.rootNode.value).length} 个属性`);
}
} catch (e) {
log('解析失败: ' + e.message, 'error');
state.errorMsg = e.message;
} finally {
state.treeLoading = false;
state.perfWarning = false;
loading.close();
}
};
// 格式化值显示
const formatValue = (value) => {
if (value === null) return 'null';
if (value === undefined) return 'undefined';
return value;
};
const formatComplexValue = (value) => {
if (Array.isArray(value)) return `[数组 (${value.length}项)]`;
if (typeof value === 'object') return `{对象 (${Object.keys(value).length}键)}`;
return value;
};
const formatTooltip = (value) => {
return JSON.stringify(value, null, 2);
};
const isObject = (value) => {
return typeof value === 'object' && value !== null;
};
const getNestedValue = (obj, path) => {
return path.split('.').reduce((o, p) => o?.[p], obj);
};
const generateTableData = (data) => {
return data.map(item => {
const row = {};
state.selectedFields.forEach(field => {
const value = _.get(item, field);
row[field] = value === undefined ? '--' : value;
});
return row;
});
};
const formatHeader = (path) => {
return path
.replace(/\./g, ' › ')
.replace(/(\$\$\$\$)/g, '')
.replace(/_/g, ' ');
};
const isComplexValue = (value) => {
return typeof value === 'object' && value !== null;
};
const formatSimpleValue = (value) => {
if (value === null) return 'null';
if (value === undefined) return 'undefined';
return value;
};
// 复制整列数据
const copyColumnData = (fieldPath) => {
try {
state.errorMsg = '';
// 确定要复制的数据源
let dataSource = [];
if (!state.currentNode) {
// 主视图中的表格数据
dataSource = state.tableData;
} else if (state.currentNode.type === 'array' && Array.isArray(state.currentNode.value)) {
// 节点详情视图中的表格数据
dataSource = state.currentNode.value;
} else {
throw new Error('没有可复制的数据');
}
const columnValues = dataSource.map(row => {
const value = row[fieldPath];
if (value === undefined) return 'N/A';
if (isComplexValue(value)) {
return formatComplexValue(value);
}
return formatSimpleValue(value);
});
const textToCopy = columnValues.join('\n');
navigator.clipboard.writeText(textToCopy).then(() => {
// 使用Element Plus的Message组件显示成功提示
ElMessage({
message: `已复制 ${formatHeader(fieldPath)} 列内容`,
type: 'success',
duration: 3000,
showClose: true,
offset: 80
});
}).catch(err => {
state.errorMsg = `复制失败: ${err.message}`;
});
} catch (error) {
state.errorMsg = `复制失败: ${error.message}`;
}
};
// 辅助方法:检查是否基础类型
const isBasicType = (type) => {
return ['string', 'number', 'boolean', 'null'].includes(type);
};
// 格式化为预览值
const formatPreviewValue = (value) => {
if (value === null) return 'null';
if (typeof value === 'string') {
return value.length > 20 ? value.substring(0, 17) + '...' : value;
}
return value;
};
// 获取类型标签
const getTypeLabel = (type) => {
const labels = {
object: '对象',
array: '列表',
string: '文本',
number: '数字',
boolean: '布尔',
null: '空值'
};
return labels[type] || type;
};
// 获取标签类型
const getTagType = (type) => {
const types = {
object: 'primary',
array: 'warning',
string: '',
number: 'success',
boolean: 'danger',
null: 'info'
};
return types[type] || '';
};
// 格式化对象值显示
const formatObjectValue = (value) => {
if (value === null) return 'null';
if (Array.isArray(value)) return `[数组 (${value.length}项)]`;
if (typeof value === 'object') return `{对象 (${Object.keys(value).length}键)}`;
return value;
};
// 获取值类型
const getValueType = (value) => {
if (Array.isArray(value)) return 'array';
if (value === null) return 'null';
if (typeof value === 'object') return 'object';
return typeof value;
};
// 检查是否为简单数组
const isSimpleArray = (arr) => {
return arr.length > 0 &&
arr.every(item =>
!(item instanceof Object) ||
item === null
);
};
// 获取数组元素的键
const getFirstArrayItemKeys = (arr) => {
if (arr.length === 0) return [];
const firstItem = arr[0];
if (firstItem && typeof firstItem === 'object' && !Array.isArray(firstItem)) {
return Object.keys(firstItem);
}
return [];
};
// 清理Web Worker
onBeforeUnmount(() => {
if (window.jsonParserWorker) {
window.jsonParserWorker.terminate();
}
});
// 切换节点展开状态
const toggleNodeExpand = (node) => {
if (node.expanded) {
node.collapse();
} else {
node.expand();
}
};
// 处理节点选择(不展开/折叠)
const handleNodeSelect = (data, node) => {
log(`选择节点: ${data.path}, 类型: ${data.type}`);
state.currentNode = data;
if (data.type === 'array' && Array.isArray(data.value)) {
state.tableData = data.value;
log(`更新表格数据: ${data.value.length} 条记录`);
}
};
return {
state,
treeProps,
treeData,
fieldTreeRef,
logs,
defaultExpandedKeys,
log,
handleFileChange,
parseJSON,
handlePaste,
loadTreeNode,
handleNodeClick,
handleNodeExpand,
handleNodeCollapse,
isBasicType,
formatPreviewValue,
getTypeLabel,
getTagType,
formatObjectValue,
getValueType,
isSimpleArray,
getFirstArrayItemKeys,
formatValue,
formatComplexValue,
formatTooltip,
isObject,
generateTableData,
formatHeader,
isComplexValue,
formatSimpleValue,
copyColumnData,
toggleNodeExpand,
handleNodeSelect
};
}
});
app.config.globalProperties._ = _;
app.use(ElementPlus)
.mount('#app');
CSS 代码
/* 全局样式优化 */
:root {
--primary-color: #409eff;
--success-color: #67c23a;
--warning-color: #e6a23c;
--danger-color: #f56c6c;
--info-color: #909399;
--bg-color: rgba(255, 255, 255, 0.85);
--border-color: #ebeef5;
--text-color: #606266;
--light-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
--font-size-sm: 12px;
--font-size-md: 13px;
--font-size-lg: 14px;
}
/* 添加全局磨砂背景 */
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('https://www.transparenttextures.com/patterns/light-wool.png');
background-size: 300px;
opacity: 0.2;
z-index: -1;
}
body {
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
background: linear-gradient(135deg, #f5f7fa 0%, #e4e7ed 100%);
color: var(--text-color);
font-size: var(--font-size-md);
}
#app {
height: 100vh;
display: flex;
overflow: hidden;
box-sizing: border-box;
position: relative;
backdrop-filter: blur(2px);
}
/* 面板样式优化 */
.left-panel {
width: 320px;
background: var(--bg-color);
border-right: 1px solid var(--border-color);
padding: 15px;
overflow: auto;
box-sizing: border-box;
display: flex;
flex-direction: column;
box-shadow: var(--light-shadow);
backdrop-filter: blur(5px);
border-radius: 0 8px 8px 0;
}
.right-panel {
flex: 1;
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
background: var(--bg-color);
backdrop-filter: blur(5px);
}
.panel-header {
margin: 10px 5px 0px 5px;
}
.panel-title {
font-size: 14px;
font-weight: 600;
color: var(--text-color);
display: flex;
align-items: center;
gap: 8px;
margin: 5px 0;
}
/* 按钮样式优化 */
.action-buttons {
margin: 10px 0;
display: flex;
gap: 8px;
justify-content: center;
}
.el-button {
font-weight: 500;
border-radius: 6px;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
display: flex;
align-items: center;
gap: 6px;
transition: all 0.3s ease;
padding: 8px 12px;
font-size: var(--font-size-sm);
}
.el-button:hover {
transform: translateY(-1px);
box-shadow: 0 3px 8px rgba(0,0,0,0.1);
}
.el-button--primary {
background: linear-gradient(to right, var(--primary-color), #66b1ff);
border: none;
}
.el-button--success {
background: linear-gradient(to right, var(--success-color), #85ce61);
border: none;
}
/* 输入框样式优化 */
.el-textarea {
border-radius: 6px;
overflow: hidden;
margin-bottom: 15px;
box-shadow: var(--light-shadow);
}
.el-textarea__inner {
font-family: 'Fira Code', monospace;
padding: 12px;
font-size: var(--font-size-sm);
transition: all 0.3s;
border: none;
background: rgba(255, 255, 255, 0.7);
}
.el-textarea__inner:focus {
outline: none;
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
}
/* 树形控件优化 */
.json-tree {
margin-top: 10px;
flex: 1;
border-radius: 6px;
overflow: hidden;
background: rgba(255, 255, 255, 0.7);
box-shadow: var(--light-shadow);
}
.el-tree {
background: transparent;
font-size: var(--font-size-sm);
}
.el-tree-node__content {
height: 32px;
border-radius: 4px;
transition: all 0.2s;
}
.el-tree-node__content:hover {
background-color: rgba(240, 247, 255, 0.7);
}
.tree-node-content {
display: flex;
align-items: center;
width: 100%;
padding: 0 4px;
font-size: var(--font-size-sm);
}
.type-tag {
margin-left: 6px;
font-size: 10px;
padding: 1px 5px;
border-radius: 3px;
}
.node-value-preview {
font-size: 11px;
color: var(--info-color);
margin-left: 6px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 150px;
display: inline-block;
vertical-align: bottom;
}
.array-index {
font-size: 11px;
color: var(--info-color);
margin-left: 4px;
background: rgba(245, 247, 250, 0.7);
padding: 0 3px;
border-radius: 2px;
}
/* 右侧面板样式优化 */
.alert-container {
flex-shrink: 0;
/* padding: 8px 15px; */
padding: 4px 0px;
}
.data-section {
display: flex;
flex-direction: column;
height: 100%;
padding: 0 15px 15px;
}
.detail-view {
padding: 15px;
background: rgba(255, 255, 255, 0.7);
border-radius: 8px;
box-shadow: var(--light-shadow);
margin: 8px 0;
overflow: auto;
/* height: calc(100% - 70px); */
height: calc(100% - 0px);
border: 1px solid var(--border-color);
font-size: var(--font-size-sm);
}
.detail-view h3 {
margin-top: 0;
display: flex;
align-items: center;
gap: 8px;
color: var(--text-color);
padding-bottom: 10px;
border-bottom: 1px solid var(--border-color);
font-size: var(--font-size-md);
margin-bottom: 12px;
}
.table-container {
flex: 1;
height: 100%;
overflow: hidden;
border-radius: 6px;
box-shadow: var(--light-shadow);
background: rgba(255, 255, 255, 0.7);
}
.el-table {
border-radius: 6px;
overflow: hidden;
font-size: var(--font-size-sm);
}
.el-table__header {
background: rgba(245, 247, 250, 0.7);
font-size: var(--font-size-sm);
}
/* 描述列表样式优化 */
.descriptions-view {
margin-top: 15px;
border: 1px solid var(--border-color);
border-radius: 6px;
overflow: hidden;
background: rgba(255, 255, 255, 0.7);
font-size: var(--font-size-sm);
}
.descriptions-header {
background: rgba(248, 250, 252, 0.7);
padding: 10px 12px;
font-weight: 500;
color: var(--text-color);
border-bottom: 1px solid var(--border-color);
font-size: var(--font-size-sm);
}
.descriptions-item {
display: flex;
border-bottom: 1px solid var(--border-color);
}
.descriptions-item-label {
width: 180px;
padding: 10px 12px;
background: rgba(250, 251, 252, 0.7);
color: var(--text-color);
font-weight: 500;
border-right: 1px solid var(--border-color);
font-size: var(--font-size-sm);
}
.descriptions-item-value {
flex: 1;
padding: 10px 12px;
display: flex;
align-items: center;
gap: 6px;
font-size: var(--font-size-sm);
}
.descriptions-item:last-child {
border-bottom: none;
}
/* 列头样式优化 */
.column-header {
display: flex;
align-items: center;
justify-content: space-between;
font-size: var(--font-size-sm);
}
.copy-column-btn {
cursor: pointer;
margin-left: 6px;
opacity: 0.5;
transition: opacity 0.3s;
font-size: var(--font-size-sm);
color: var(--text-color);
display: flex;
}
.el-table__header tr:hover .copy-column-btn {
opacity: 1;
}
/* 空数据样式优化 */
.custom-empty {
padding: 30px 0;
color: var(--info-color);
font-size: var(--font-size-sm);
text-align: center;
}
.custom-empty-icon {
font-size: 36px;
color: #c0c4cc;
margin-bottom: 12px;
}
/* 性能警告样式 */
.large-data-warning {
background-color: rgba(253, 246, 236, 0.9);
border-left: 4px solid var(--warning-color);
padding: 10px 15px;
border-radius: 6px;
display: none;
align-items: center;
z-index: 2000;
box-shadow: var(--light-shadow);
max-width: 300px;
position: fixed;
bottom: 15px;
right: 15px;
font-size: var(--font-size-sm);
}
.large-data-warning .el-icon {
font-size: 20px;
margin-right: 10px;
color: var(--warning-color);
}
/* 响应式调整 */
@media (max-width: 992px) {
#app {
flex-direction: column;
}
.left-panel {
width: 100%;
height: 50%;
border-right: none;
border-bottom: 1px solid var(--border-color);
border-radius: 0 0 8px 8px;
}
.right-panel {
height: 50%;
}
}
/* 树节点内容 */
.tree-node-content {
display: flex;
align-items: center;
width: 100%;
cursor: pointer;
}
/* 展开/折叠图标 */
.expand-icon {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
margin-right: 6px;
cursor: pointer;
transition: transform 0.2s;
}
.expand-icon:hover {
color: var(--primary-color);
}
/* 节点名称 */
.node-name {
flex: 1;
padding: 4px 0;
}
/* 悬停效果 */
.node-name:hover {
color: var(--primary-color);
}