vue · 4427 bytes Raw Blame History
1 <template>
2 <div class="json-viewer">
3 <div class="json-content text-sm font-mono">
4 <JsonNode
5 :data="data"
6 :key-name="''"
7 :level="0"
8 :is-root="true"
9 />
10 </div>
11 </div>
12 </template>
13
14 <script setup lang="ts">
15 import { defineComponent } from 'vue'
16
17 interface Props {
18 data: any
19 }
20
21 defineProps<Props>()
22
23 // Recursive JSON node component
24 const JsonNode = defineComponent({
25 name: 'JsonNode',
26 props: {
27 data: { required: true },
28 keyName: { type: String, default: '' },
29 level: { type: Number, default: 0 },
30 isRoot: { type: Boolean, default: false },
31 },
32 data() {
33 return {
34 collapsed: this.level > 2, // Auto-collapse deep objects
35 }
36 },
37 computed: {
38 dataType() {
39 if (this.data === null) return 'null'
40 if (Array.isArray(this.data)) return 'array'
41 return typeof this.data
42 },
43 isCollapsible() {
44 return this.dataType === 'object' || this.dataType === 'array'
45 },
46 keys() {
47 if (this.dataType === 'object') {
48 return Object.keys(this.data)
49 }
50 if (this.dataType === 'array') {
51 return this.data.map((_: any, index: number) => index.toString())
52 }
53 return []
54 },
55 hasChildren() {
56 return this.keys.length > 0
57 },
58 indentStyle() {
59 return {
60 paddingLeft: `${this.level * 20}px`,
61 }
62 },
63 },
64 methods: {
65 toggleCollapse() {
66 if (this.isCollapsible) {
67 this.collapsed = !this.collapsed
68 }
69 },
70 formatValue(value: any) {
71 if (value === null) return 'null'
72 if (typeof value === 'string') return `"${value}"`
73 if (typeof value === 'boolean') return value.toString()
74 if (typeof value === 'number') return value.toString()
75 return String(value)
76 },
77 getValueClass(value: any) {
78 if (value === null) return 'text-gray-500'
79 if (typeof value === 'string') return 'text-green-600 dark:text-green-400'
80 if (typeof value === 'boolean') return 'text-blue-600 dark:text-blue-400'
81 if (typeof value === 'number') return 'text-purple-600 dark:text-purple-400'
82 return 'text-gray-700 dark:text-gray-300'
83 },
84 },
85 template: `
86 <div class="json-node">
87 <div
88 v-if="!isRoot"
89 class="json-line flex items-start hover:bg-gray-100 dark:hover:bg-gray-700 rounded px-1"
90 :style="indentStyle"
91 >
92 <!-- Toggle button -->
93 <button
94 v-if="isCollapsible"
95 @click="toggleCollapse"
96 class="flex-shrink-0 w-4 h-4 mt-0.5 mr-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
97 >
98 <svg class="w-3 h-3 transition-transform" :class="{ 'rotate-90': !collapsed }" fill="currentColor" viewBox="0 0 20 20">
99 <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 111.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path>
100 </svg>
101 </button>
102 <span v-else class="w-4 mr-1"></span>
103
104 <!-- Key name -->
105 <span v-if="keyName" class="text-blue-700 dark:text-blue-300 mr-2">
106 "{{ keyName }}":
107 </span>
108
109 <!-- Value for primitives -->
110 <span
111 v-if="!isCollapsible"
112 :class="getValueClass(data)"
113 >
114 {{ formatValue(data) }}
115 </span>
116
117 <!-- Container indicators -->
118 <span v-else class="text-gray-600 dark:text-gray-400">
119 <span v-if="dataType === 'array'">
120 [{{ collapsed ? \`\${keys.length} items\` : '' }}
121 </span>
122 <span v-else-if="dataType === 'object'">
123 {{{ collapsed ? \`\${keys.length} keys\` : '' }}
124 </span>
125 </span>
126 </div>
127
128 <!-- Children -->
129 <div v-if="(isRoot || !collapsed) && hasChildren">
130 <JsonNode
131 v-for="key in keys"
132 :key="key"
133 :data="data[key]"
134 :key-name="key"
135 :level="isRoot ? 0 : level + 1"
136 />
137 </div>
138
139 <!-- Closing brackets -->
140 <div
141 v-if="(isRoot || !collapsed) && isCollapsible && !isRoot"
142 class="text-gray-600 dark:text-gray-400 px-1"
143 :style="indentStyle"
144 >
145 {{ dataType === 'array' ? ']' : '}' }}
146 </div>
147 </div>
148 `,
149 })
150 </script>
151
152 <style scoped>
153 .json-viewer {
154 @apply text-sm;
155 }
156
157 .json-line {
158 @apply py-0.5;
159 }
160
161 .json-content {
162 @apply leading-relaxed;
163 }
164 </style>