Files
ToolBox/src/components/JsonTreeNode.vue
2026-01-17 15:57:06 +08:00

225 lines
4.3 KiB
Vue

<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>