方案一:使用 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 当:
- 需要轻量级解决方案
- 对性能有较高要求
- 只需要基础JSON展示功能
- 希望完全自定义样式
选择 vue-json-viewer 当:
- 需要丰富的交互功能
- 需要内置的复制、展开/折叠功能
- 需要更美观的默认样式
- 需要完整的点击事件系统
- 需要显示数组索引
两种组件都能很好地完成JSON可视化任务,具体选择取决于项目需求和个人偏好。