- Slide grid now 1-2-3 columns (4x larger thumbnails) - Delete button left, fullscreen right, always visible (no hover) - Upload area appears inline as last grid item (no side-by-side layout) - SlideUploader supports inline mode for grid integration - Add recursive collapsible JsonTreeViewer component (no external deps) - Replace raw JSON pre tags in API logs with tree viewer
158 lines
6.3 KiB
Vue
158 lines
6.3 KiB
Vue
<script setup>
|
|
import { ref, computed } from 'vue'
|
|
|
|
const props = defineProps({
|
|
data: { type: [Object, Array, String, Number, Boolean, null], default: null },
|
|
expandDepth: { type: Number, default: 2 },
|
|
label: { type: String, default: '' },
|
|
depth: { type: Number, default: 0 },
|
|
isRoot: { type: Boolean, default: true },
|
|
})
|
|
|
|
const expanded = ref(props.depth < props.expandDepth)
|
|
|
|
const dataType = computed(() => {
|
|
if (props.data === null || props.data === undefined) return 'null'
|
|
if (Array.isArray(props.data)) return 'array'
|
|
return typeof props.data
|
|
})
|
|
|
|
const isExpandable = computed(() => {
|
|
return dataType.value === 'object' || dataType.value === 'array'
|
|
})
|
|
|
|
const entries = computed(() => {
|
|
if (!isExpandable.value || !props.data) return []
|
|
return Object.entries(props.data)
|
|
})
|
|
|
|
const previewText = computed(() => {
|
|
if (dataType.value === 'array') return `Array[${props.data.length}]`
|
|
if (dataType.value === 'object') {
|
|
const keys = Object.keys(props.data)
|
|
return `{${keys.length}}`
|
|
}
|
|
return ''
|
|
})
|
|
|
|
function formatValue(val) {
|
|
if (val === null || val === undefined) return 'null'
|
|
if (typeof val === 'string') return `"${val}"`
|
|
if (typeof val === 'boolean') return val ? 'true' : 'false'
|
|
return String(val)
|
|
}
|
|
|
|
function valueClass(val) {
|
|
if (val === null || val === undefined) return 'text-gray-400 italic'
|
|
if (typeof val === 'string') return 'text-emerald-700'
|
|
if (typeof val === 'number') return 'text-blue-700'
|
|
if (typeof val === 'boolean') return 'text-amber-700'
|
|
return 'text-gray-700'
|
|
}
|
|
|
|
function toggle() {
|
|
if (isExpandable.value) {
|
|
expanded.value = !expanded.value
|
|
}
|
|
}
|
|
|
|
function copyToClipboard() {
|
|
const text = JSON.stringify(props.data, null, 2)
|
|
navigator.clipboard.writeText(text).catch(() => {})
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div :class="['json-tree', { 'json-tree--root': isRoot }]">
|
|
<!-- Root-level copy button -->
|
|
<div v-if="isRoot && data && typeof data === 'object'" class="flex items-center justify-end mb-1.5">
|
|
<button
|
|
@click="copyToClipboard"
|
|
class="flex items-center gap-1 rounded-md px-2 py-0.5 text-[10px] font-medium text-gray-400 transition hover:bg-gray-100 hover:text-gray-600"
|
|
title="JSON kopieren"
|
|
>
|
|
<svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9.75a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184" />
|
|
</svg>
|
|
Kopieren
|
|
</button>
|
|
</div>
|
|
|
|
<!-- String data (not object/array) at root level -->
|
|
<pre v-if="isRoot && typeof data === 'string'" class="text-xs text-gray-700 whitespace-pre-wrap leading-relaxed">{{ data }}</pre>
|
|
|
|
<!-- Null/empty at root level -->
|
|
<span v-else-if="isRoot && (data === null || data === undefined)" class="text-sm italic text-gray-400">Keine Daten</span>
|
|
|
|
<!-- Primitive at root level -->
|
|
<span v-else-if="isRoot && !isExpandable" :class="['text-xs font-mono', valueClass(data)]">{{ formatValue(data) }}</span>
|
|
|
|
<!-- Tree view for objects/arrays -->
|
|
<div v-else-if="isExpandable" class="font-mono text-xs leading-relaxed">
|
|
<!-- Expandable node header -->
|
|
<div
|
|
class="json-node-header group flex items-center gap-1 cursor-pointer rounded px-1 -mx-1 transition-colors hover:bg-gray-50"
|
|
@click="toggle"
|
|
>
|
|
<!-- Triangle -->
|
|
<svg
|
|
class="h-3 w-3 shrink-0 text-gray-400 transition-transform duration-150"
|
|
:class="{ 'rotate-90': expanded }"
|
|
fill="currentColor"
|
|
viewBox="0 0 20 20"
|
|
>
|
|
<path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" />
|
|
</svg>
|
|
|
|
<!-- Key label -->
|
|
<span v-if="label" class="text-purple-700 font-medium">{{ label }}</span>
|
|
<span v-if="label" class="text-gray-400">:</span>
|
|
|
|
<!-- Collapsed preview -->
|
|
<span class="text-gray-400">
|
|
{{ dataType === 'array' ? '[' : '{' }}
|
|
</span>
|
|
<span v-if="!expanded" class="text-gray-400">
|
|
{{ previewText }}
|
|
{{ dataType === 'array' ? ']' : '}' }}
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Children -->
|
|
<div v-if="expanded" class="ml-4 border-l border-gray-200 pl-3">
|
|
<template v-for="([key, val], idx) in entries" :key="key">
|
|
<!-- Nested expandable -->
|
|
<JsonTreeViewer
|
|
v-if="val !== null && typeof val === 'object'"
|
|
:data="val"
|
|
:expand-depth="expandDepth"
|
|
:label="key"
|
|
:depth="depth + 1"
|
|
:is-root="false"
|
|
/>
|
|
<!-- Leaf value -->
|
|
<div v-else class="flex items-center gap-1 py-0.5 px-1 -mx-1 rounded transition-colors hover:bg-gray-50">
|
|
<span class="h-3 w-3 shrink-0" />
|
|
<span class="text-purple-700 font-medium">{{ key }}</span>
|
|
<span class="text-gray-400">:</span>
|
|
<span :class="valueClass(val)">{{ formatValue(val) }}</span>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- Closing bracket -->
|
|
<div v-if="expanded" class="text-gray-400 px-1 -mx-1">
|
|
<span class="ml-0">{{ dataType === 'array' ? ']' : '}' }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.json-tree--root {
|
|
padding: 8px;
|
|
border-radius: 8px;
|
|
background: rgb(249 250 251 / 0.5);
|
|
}
|
|
</style>
|