init
This commit is contained in:
224
src/components/JsonTreeNode.vue
Normal file
224
src/components/JsonTreeNode.vue
Normal file
@@ -0,0 +1,224 @@
|
||||
<template>
|
||||
<div class="json-tree-node">
|
||||
<div
|
||||
class="node-line"
|
||||
:class="{ 'has-children': hasChildren, 'is-expanded': isExpanded }"
|
||||
@click="toggle"
|
||||
>
|
||||
<span v-if="hasChildren" class="expand-icon">
|
||||
{{ isExpanded ? '▼' : '▶' }}
|
||||
</span>
|
||||
<span v-else class="expand-placeholder"></span>
|
||||
<span class="node-key" v-if="key !== null">
|
||||
<span class="key-name">{{ key }}</span>:
|
||||
</span>
|
||||
<span class="node-value" :class="valueType">
|
||||
<span v-if="valueType === 'string'">"{{ displayValue }}"</span>
|
||||
<span v-else>{{ displayValue }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="hasChildren && isExpanded" class="node-children">
|
||||
<JsonTreeNode
|
||||
v-for="(child, index) in children"
|
||||
:key="index"
|
||||
:data="child.value"
|
||||
:key-name="child.key"
|
||||
:path="child.path"
|
||||
:expanded="expanded"
|
||||
@toggle="$emit('toggle', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: [Object, Array, String, Number, Boolean, null],
|
||||
required: true
|
||||
},
|
||||
keyName: {
|
||||
type: [String, Number],
|
||||
default: null
|
||||
},
|
||||
path: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
expanded: {
|
||||
type: Set,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['toggle'])
|
||||
|
||||
const key = computed(() => props.keyName)
|
||||
const currentPath = computed(() => {
|
||||
if (props.path === '' && props.keyName === null) {
|
||||
return 'root'
|
||||
}
|
||||
if (props.path === '') {
|
||||
return String(props.keyName)
|
||||
}
|
||||
if (props.path === 'root') {
|
||||
if (typeof props.keyName === 'number') {
|
||||
return `root[${props.keyName}]`
|
||||
}
|
||||
return `root.${props.keyName}`
|
||||
}
|
||||
if (typeof props.keyName === 'number') {
|
||||
return `${props.path}[${props.keyName}]`
|
||||
}
|
||||
return `${props.path}.${props.keyName}`
|
||||
})
|
||||
|
||||
const hasChildren = computed(() => {
|
||||
return (
|
||||
(typeof props.data === 'object' && props.data !== null) ||
|
||||
Array.isArray(props.data)
|
||||
)
|
||||
})
|
||||
|
||||
const isExpanded = computed(() => {
|
||||
return props.expanded.has(currentPath.value)
|
||||
})
|
||||
|
||||
const valueType = computed(() => {
|
||||
if (props.data === null) return 'null'
|
||||
if (Array.isArray(props.data)) return 'array'
|
||||
if (typeof props.data === 'object') return 'object'
|
||||
return typeof props.data
|
||||
})
|
||||
|
||||
const displayValue = computed(() => {
|
||||
if (props.data === null) return 'null'
|
||||
if (Array.isArray(props.data)) {
|
||||
return `Array(${props.data.length})`
|
||||
}
|
||||
if (typeof props.data === 'object') {
|
||||
const keys = Object.keys(props.data)
|
||||
return `Object(${keys.length})`
|
||||
}
|
||||
if (typeof props.data === 'string') {
|
||||
return props.data
|
||||
}
|
||||
return String(props.data)
|
||||
})
|
||||
|
||||
const children = computed(() => {
|
||||
if (!hasChildren.value) return []
|
||||
|
||||
const basePath = props.path === '' && props.keyName === null ? 'root' : currentPath.value
|
||||
|
||||
if (Array.isArray(props.data)) {
|
||||
return props.data.map((item, index) => ({
|
||||
key: index,
|
||||
value: item,
|
||||
path: basePath
|
||||
}))
|
||||
}
|
||||
|
||||
if (typeof props.data === 'object' && props.data !== null) {
|
||||
return Object.keys(props.data).map(key => ({
|
||||
key: key,
|
||||
value: props.data[key],
|
||||
path: basePath
|
||||
}))
|
||||
}
|
||||
|
||||
return []
|
||||
})
|
||||
|
||||
const toggle = () => {
|
||||
if (hasChildren.value) {
|
||||
emit('toggle', currentPath.value)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.json-tree-node {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.node-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px 0;
|
||||
cursor: default;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.node-line.has-children {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.node-line.has-children:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.expand-icon {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 10px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.expand-placeholder {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.node-key {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.key-name {
|
||||
color: #881391;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.node-value {
|
||||
color: #1a1aa6;
|
||||
}
|
||||
|
||||
.node-value.string {
|
||||
color: #0b7500;
|
||||
}
|
||||
|
||||
.node-value.number {
|
||||
color: #1a1aa6;
|
||||
}
|
||||
|
||||
.node-value.boolean {
|
||||
color: #1a1aa6;
|
||||
}
|
||||
|
||||
.node-value.null {
|
||||
color: #808080;
|
||||
}
|
||||
|
||||
.node-value.object {
|
||||
color: #1a1aa6;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.node-value.array {
|
||||
color: #1a1aa6;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.node-children {
|
||||
margin-left: 20px;
|
||||
border-left: 1px solid #e0e0e0;
|
||||
padding-left: 8px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user