Json 转 Table,BP 响应包可以直接转,工具这个东西,还是自己写的比较有成就感
本文最后更新于 35 天前,其中的信息可能已经有所发展或是发生改变。

怎么说呢,工具这个东西,就是出自懒人之手
老规矩,先交代一下事件背景。

背景

某次在渗透测试的时候,发现一个接口,泄露了几千条个人信息。包含姓名、工号、手机号、邮箱等信息。
巧的是,另一个接口(一个类似于用户绑定的接口)也存在一个漏洞,如果用户满足某个条件,可以越权。
所以,我需要的就是,将泄露的这几千条手机号提取出来,然后用作第二个接口的爆破字典。

JSON2Table 工具

其实网上有不少 Json 转 table 的工具,但是,别人的哪有自己写的有成就感呢?
于是我决定自己写一个 JSON 转 Table 的工具,并且,要支持一键复制列,最好,还能自动识别 Burp 的响应包并且提取出 JSON 格式的 response。

看看效果(代码在文末)

file

在线体验

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);
}
学海无涯,回头是岸。 --- hola
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇