119 lines
3.3 KiB
Vue
119 lines
3.3 KiB
Vue
<script setup lang="ts">
|
|
defineOptions({ name: 'NestedJsonViewer' })
|
|
|
|
interface Props {
|
|
data: any
|
|
depth?: number
|
|
}
|
|
|
|
const props = defineProps<Props>()
|
|
|
|
const depth = computed(() => props.depth || 0)
|
|
|
|
// Font size decreases with depth: 12px base, -1px per level
|
|
const fontSize = computed(() => Math.max(10, 12 - depth.value * 1))
|
|
|
|
// Padding decreases with depth
|
|
const padding = computed(() => Math.max(1, 4 - depth.value))
|
|
|
|
function isObject(value: any): boolean {
|
|
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
}
|
|
|
|
function isArray(value: any): boolean {
|
|
return Array.isArray(value)
|
|
}
|
|
|
|
function isDateString(value: string): boolean {
|
|
if (typeof value !== 'string') return false
|
|
return /^\d{4}-\d{2}-\d{2}/.test(value) && !isNaN(Date.parse(value))
|
|
}
|
|
|
|
function formatValue(value: any): string {
|
|
if (value === null || value === undefined) return '—'
|
|
if (typeof value === 'boolean') return value ? 'Yes' : 'No'
|
|
if (typeof value === 'number') return value.toLocaleString()
|
|
if (isDateString(value)) {
|
|
try {
|
|
return new Date(value).toLocaleDateString('es-PA', {
|
|
day: '2-digit',
|
|
month: 'short',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
})
|
|
} catch {
|
|
return value
|
|
}
|
|
}
|
|
return String(value)
|
|
}
|
|
|
|
function formatKey(key: string): string {
|
|
return key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="nested-json-viewer" :style="{ fontSize: fontSize + 'px' }">
|
|
<!-- Object: render as table -->
|
|
<table v-if="isObject(data) && Object.keys(data).length > 0" class="json-table border rounded overflow-hidden mb-2">
|
|
<tbody>
|
|
<tr v-for="(value, key) in data" :key="key" class="border-t border-gray-200 hover:bg-gray-50">
|
|
<td class="field-name px-3 py-2 font-medium text-gray-600 align-top whitespace-nowrap" :style="{ padding: padding + 'px', fontSize: (fontSize - 1) + 'px' }">
|
|
{{ formatKey(key) }}
|
|
</td>
|
|
<td class="field-value px-3 py-2 text-gray-900 align-top" :style="{ padding: padding + 'px' }">
|
|
<NestedJsonViewer :data="value" :depth="depth + 1" />
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<div v-else-if="isObject(data) && Object.keys(data).length === 0" class="text-gray-400 italic" :style="{ fontSize: fontSize + 'px' }">
|
|
Empty object
|
|
</div>
|
|
|
|
<!-- Array -->
|
|
<div v-else-if="isArray(data)" class="array-container">
|
|
<div v-for="(item, index) in data" :key="index" class="array-item mb-2 border rounded p-2">
|
|
<div class="text-xs font-semibold text-gray-500 mb-1">[{{ index }}]</div>
|
|
<NestedJsonViewer :data="item" :depth="depth + 1" />
|
|
</div>
|
|
<div v-if="data.length === 0" class="text-gray-400 italic" :style="{ fontSize: fontSize + 'px' }">
|
|
Empty array
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Primitive -->
|
|
<span v-else class="primitive-value" :class="{ 'text-gray-400': data === '—' }">
|
|
{{ formatValue(data) }}
|
|
</span>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.nested-json-viewer {
|
|
width: 100%;
|
|
}
|
|
|
|
.json-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
background: white;
|
|
}
|
|
|
|
.field-name {
|
|
min-width: 120px;
|
|
background: #f9fafb;
|
|
}
|
|
|
|
.field-value {
|
|
word-break: break-word;
|
|
}
|
|
|
|
.array-item {
|
|
background: white;
|
|
border-color: var(--card-border, #e5e7eb);
|
|
}
|
|
</style>
|