澄迈县中国灵车网

Vue实现JSON数据可视化的两种组件实战解析

2026-03-26 08:47:01 浏览次数:0
详细信息

方案一:使用 vue-json-pretty(轻量级方案)

安装

npm install vue-json-pretty

基础实现

<template>
  <div class="json-viewer">
    <h3>JSON 可视化查看器</h3>

    <!-- 基础使用 -->
    <vue-json-pretty
      :data="jsonData"
      :deep="3"
      :show-length="true"
    />

    <!-- 自定义主题 -->
    <vue-json-pretty
      :data="jsonData"
      :deep="collapsed ? 1 : 10"
      :show-length="true"
      :show-icon="true"
      :highlight-mouseover-node="true"
      :highlight-selected-node="true"
      :path="'res'"
      class="json-theme-custom"
      @click="handleNodeClick"
    />

    <!-- 带搜索功能 -->
    <div class="json-container">
      <input
        v-model="searchText"
        placeholder="搜索JSON内容..."
        class="search-input"
      />
      <vue-json-pretty
        :data="filteredJsonData"
        :deep="expandAll ? 10 : 2"
        :highlight-mouseover-node="true"
        :show-double-quotes="false"
        :collapsed-on-click-brackets="true"
      />
    </div>
  </div>
</template>

<script>
import VueJsonPretty from 'vue-json-pretty'
import 'vue-json-pretty/lib/styles.css'

export default {
  name: 'JsonViewerBasic',
  components: {
    VueJsonPretty
  },
  data() {
    return {
      jsonData: {
        status: 'success',
        data: {
          user: {
            id: 1,
            name: '张三',
            age: 28,
            email: 'zhangsan@example.com',
            address: {
              city: '北京',
              district: '海淀区',
              street: '中关村大街'
            },
            hobbies: ['编程', '阅读', '篮球'],
            scores: {
              math: 95,
              english: 88,
              physics: 92
            }
          },
          metadata: {
            createdAt: '2024-01-01T10:00:00Z',
            updatedAt: '2024-01-15T14:30:00Z',
            version: '1.2.3'
          }
        },
        code: 200,
        message: '请求成功'
      },
      searchText: '',
      collapsed: false,
      expandAll: false
    }
  },
  computed: {
    filteredJsonData() {
      if (!this.searchText) return this.jsonData

      const search = this.searchText.toLowerCase()
      return this.filterObject(this.jsonData, search)
    }
  },
  methods: {
    handleNodeClick(path, data) {
      console.log('点击节点:', path, data)
      // 可以在此处实现复制节点值、展开/折叠等操作
      this.$copyText(JSON.stringify(data, null, 2))
        .then(() => {
          this.$message.success('已复制到剪贴板')
        })
    },

    filterObject(obj, search) {
      if (typeof obj !== 'object' || obj === null) {
        return obj
      }

      if (Array.isArray(obj)) {
        return obj
          .map(item => this.filterObject(item, search))
          .filter(item => {
            if (typeof item === 'object') {
              return Object.keys(item).length > 0
            }
            return String(item).toLowerCase().includes(search)
          })
      }

      const filtered = {}
      for (const [key, value] of Object.entries(obj)) {
        const filteredValue = this.filterObject(value, search)

        // 如果键匹配或者值包含搜索文本,保留该属性
        if (
          key.toLowerCase().includes(search) ||
          (typeof filteredValue === 'string' && 
           filteredValue.toLowerCase().includes(search)) ||
          (typeof filteredValue === 'object' && 
           Object.keys(filteredValue).length > 0)
        ) {
          filtered[key] = filteredValue
        }
      }

      return filtered
    }
  }
}
</script>

<style scoped>
.json-viewer {
  padding: 20px;
  background: #f5f7fa;
  border-radius: 8px;
}

.json-container {
  margin-top: 20px;
  border: 1px solid #e4e7ed;
  border-radius: 6px;
  overflow: hidden;
}

.search-input {
  width: 100%;
  padding: 8px 12px;
  border: none;
  border-bottom: 1px solid #e4e7ed;
  outline: none;
  box-sizing: border-box;
}

/* 自定义主题 */
.json-theme-custom {
  --vjp-font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
  --vjp-font-size: 14px;
  --vjp-line-height: 1.5;
  --vjp-key-color: #881391;
  --vjp-value-color: #1a1aa6;
  --vjp-string-color: #1a1aa6;
  --vjp-number-color: #1c00cf;
  --vjp-boolean-color: #1c00cf;
  --vjp-null-color: #1c00cf;
  --vjp-arrow-color: #6a737d;
  --vjp-arrow-size: 6px;
  --vjp-border-color: #e1e4e8;
  --vjp-border-radius: 3px;
  --vjp-indent-size: 18px;
  --vjp-item-margin: 2px;
  --vjp-background-color: transparent;
  --vjp-hover-background-color: #f6f8fa;
  --vjp-selected-background-color: #0366d6;
  --vjp-selected-color: white;
}

.json-theme-custom >>> .vjs-value__string {
  color: #22863a;
}

.json-theme-custom >>> .vjs-value__number {
  color: #005cc5;
  font-weight: bold;
}

.json-theme-custom >>> .vjs-tree {
  margin: 0;
  padding: 10px;
}
</style>

方案二:使用 vue-json-viewer(功能丰富方案)

安装

npm install vue-json-viewer

高级实现

<template>
  <div class="advanced-json-viewer">
    <div class="control-panel">
      <button @click="toggleExpandAll">
        {{ isExpanded ? '全部折叠' : '全部展开' }}
      </button>
      <button @click="copyJson">复制JSON</button>
      <button @click="downloadJson">下载JSON</button>
      <button @click="toggleTheme">
        切换主题: {{ theme === 'light' ? '深色' : '浅色' }}
      </button>
      <label>
        <input v-model="showIndex" type="checkbox" />
        显示数组索引
      </label>
      <label>
        <input v-model="showIcon" type="checkbox" />
        显示图标
      </label>
    </div>

    <div class="editor-section">
      <textarea
        v-model="jsonInput"
        placeholder="输入或粘贴JSON数据..."
        class="json-input"
        @input="validateJson"
      />
      <div v-if="jsonError" class="error-message">
        {{ jsonError }}
      </div>
    </div>

    <div class="viewer-section" :class="theme">
      <vue-json-viewer
        v-if="parsedJson"
        :value="parsedJson"
        :expand-depth="expandDepth"
        :show-index="showIndex"
        :show-icon="showIcon"
        :show-double-quotes="false"
        :copyable="true"
        :expand-all="isExpanded"
        :theme="theme"
        @json-click="onJsonClick"
        @json-collapse="onJsonCollapse"
        @json-expand="onJsonExpand"
        class="json-viewer"
      />

      <div v-else class="empty-state">
        请输入有效的JSON数据
      </div>
    </div>

    <!-- 节点详情模态框 -->
    <div v-if="selectedNode" class="node-detail-modal" @click="closeModal">
      <div class="modal-content" @click.stop>
        <h3>节点详情</h3>
        <div class="detail-content">
          <div class="detail-item">
            <strong>路径:</strong> {{ selectedNode.path }}
          </div>
          <div class="detail-item">
            <strong>类型:</strong> {{ selectedNode.type }}
          </div>
          <div class="detail-item">
            <strong>值:</strong>
            <pre>{{ selectedNode.value }}</pre>
          </div>
          <button @click="copyNodeValue" class="copy-btn">
            复制值
          </button>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import VueJsonViewer from 'vue-json-viewer'

export default {
  name: 'AdvancedJsonViewer',
  components: {
    VueJsonViewer
  },
  data() {
    return {
      jsonInput: '',
      parsedJson: null,
      jsonError: '',
      isExpanded: false,
      showIndex: true,
      showIcon: true,
      theme: 'light',
      selectedNode: null,
      expandDepth: 2
    }
  },
  created() {
    // 初始化示例数据
    const exampleData = {
      api: {
        version: "1.0",
        endpoints: [
          {
            name: "getUser",
            method: "GET",
            path: "/api/users/:id",
            parameters: [
              { name: "id", type: "number", required: true },
              { name: "fields", type: "string", required: false }
            ],
            response: {
              success: {
                code: 200,
                schema: {
                  id: "number",
                  name: "string",
                  email: "string"
                }
              },
              error: {
                code: 404,
                message: "User not found"
              }
            }
          }
        ],
        config: {
          timeout: 30000,
          retry: 3,
          cache: {
            enabled: true,
            ttl: 300
          }
        }
      }
    }

    this.jsonInput = JSON.stringify(exampleData, null, 2)
    this.parsedJson = exampleData
  },
  methods: {
    validateJson() {
      try {
        if (this.jsonInput.trim()) {
          this.parsedJson = JSON.parse(this.jsonInput)
          this.jsonError = ''
        } else {
          this.parsedJson = null
        }
      } catch (error) {
        this.jsonError = `JSON解析错误: ${error.message}`
        this.parsedJson = null
      }
    },

    toggleExpandAll() {
      this.isExpanded = !this.isExpanded
      this.expandDepth = this.isExpanded ? 10 : 2
    },

    onJsonClick(path, value, type) {
      this.selectedNode = {
        path,
        value: JSON.stringify(value, null, 2),
        type
      }
      console.log('点击节点:', path, value, type)
    },

    onJsonCollapse(path) {
      console.log('折叠节点:', path)
    },

    onJsonExpand(path) {
      console.log('展开节点:', path)
    },

    copyJson() {
      if (!this.parsedJson) return

      navigator.clipboard.writeText(JSON.stringify(this.parsedJson, null, 2))
        .then(() => {
          alert('JSON已复制到剪贴板')
        })
        .catch(err => {
          console.error('复制失败:', err)
        })
    },

    copyNodeValue() {
      if (this.selectedNode) {
        navigator.clipboard.writeText(this.selectedNode.value)
          .then(() => {
            alert('节点值已复制到剪贴板')
          })
    }
  },

    downloadJson() {
      if (!this.parsedJson) return

      const dataStr = JSON.stringify(this.parsedJson, null, 2)
      const dataBlob = new Blob([dataStr], { type: 'application/json' })
      const url = URL.createObjectURL(dataBlob)

      const link = document.createElement('a')
      link.href = url
      link.download = 'data.json'
      link.click()

      URL.revokeObjectURL(url)
    },

    toggleTheme() {
      this.theme = this.theme === 'light' ? 'dark' : 'light'
    },

    closeModal() {
      this.selectedNode = null
    }
  },
  watch: {
    jsonInput: {
      immediate: true,
      handler: 'validateJson'
    }
  }
}
</script>

<style scoped>
.advanced-json-viewer {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

.control-panel {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  padding: 15px;
  background: #f8f9fa;
  border-radius: 8px;
  margin-bottom: 20px;
}

.control-panel button {
  padding: 8px 16px;
  background: #409eff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  transition: background 0.3s;
}

.control-panel button:hover {
  background: #66b1ff;
}

.control-panel label {
  display: flex;
  align-items: center;
  gap: 5px;
  cursor: pointer;
}

.editor-section {
  margin-bottom: 20px;
}

.json-input {
  width: 100%;
  height: 150px;
  padding: 12px;
  border: 2px solid #e4e7ed;
  border-radius: 6px;
  font-family: 'Monaco', 'Menlo', monospace;
  font-size: 14px;
  line-height: 1.5;
  resize: vertical;
  transition: border-color 0.3s;
}

.json-input:focus {
  outline: none;
  border-color: #409eff;
}

.error-message {
  color: #f56c6c;
  margin-top: 8px;
  font-size: 14px;
}

.viewer-section {
  border: 1px solid #e4e7ed;
  border-radius: 6px;
  overflow: hidden;
  transition: background 0.3s;
}

.viewer-section.light {
  background: white;
}

.viewer-section.dark {
  background: #1e1e1e;
}

.json-viewer {
  max-height: 500px;
  overflow-y: auto;
  padding: 15px;
}

.empty-state {
  padding: 40px;
  text-align: center;
  color: #909399;
  font-size: 16px;
}

.node-detail-modal {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
}

.modal-content {
  background: white;
  padding: 24px;
  border-radius: 8px;
  max-width: 500px;
  width: 90%;
  max-height: 80vh;
  overflow-y: auto;
}

.detail-content {
  margin-top: 16px;
}

.detail-item {
  margin-bottom: 12px;
}

.detail-item strong {
  display: inline-block;
  width: 60px;
  color: #606266;
}

.detail-item pre {
  background: #f5f7fa;
  padding: 8px;
  border-radius: 4px;
  margin-top: 4px;
  overflow-x: auto;
  font-family: monospace;
}

.copy-btn {
  margin-top: 16px;
  padding: 8px 16px;
  background: #67c23a;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.copy-btn:hover {
  background: #85ce61;
}

/* 暗色主题适配 */
.viewer-section.dark .json-viewer {
  background: #1e1e1e;
}

.viewer-section.dark .empty-state {
  color: #8c8c8c;
}

@media (max-width: 768px) {
  .control-panel {
    flex-direction: column;
    align-items: flex-start;
  }

  .json-input {
    height: 120px;
  }

  .json-viewer {
    font-size: 12px;
  }
}
</style>

两种方案对比

特性 vue-json-pretty vue-json-viewer
包大小 ~10KB (gzipped) ~20KB (gzipped)
性能 优秀 良好
功能丰富度 基础功能 高级功能
主题定制 通过CSS变量 内置light/dark主题
交互事件 基本点击事件 完整事件系统
复制功能 需手动实现 内置复制按钮
搜索功能 需手动实现 需手动实现
展开/折叠 支持 支持且更智能
数组索引 不支持 支持
节点图标 简单图标 丰富图标

选择建议

选择 vue-json-pretty 当:

选择 vue-json-viewer 当:

两种组件都能很好地完成JSON可视化任务,具体选择取决于项目需求和个人偏好。

相关推荐