226 lines
4.6 KiB
Vue
226 lines
4.6 KiB
Vue
<template>
|
|
<div class="tree-node">
|
|
<template v-if="isObject">
|
|
<div class="node-bracket">{</div>
|
|
<div class="node-children">
|
|
<div v-for="(value, key) in data" :key="key" class="node-item">
|
|
<span class="node-key">"{{ key }}"</span>
|
|
<span class="node-colon">:</span>
|
|
<JsonTreeNode
|
|
:data="value"
|
|
:path="buildPath(key)"
|
|
:fields="fields"
|
|
:selected-paths="selectedPaths"
|
|
@select="$emit('select', $event)"
|
|
/>
|
|
<span v-if="!isLastKey(key)" class="node-comma">,</span>
|
|
</div>
|
|
</div>
|
|
<div class="node-bracket">}</div>
|
|
</template>
|
|
|
|
<template v-else-if="isArray">
|
|
<div class="node-bracket">[</div>
|
|
<div class="node-children">
|
|
<div v-for="(item, index) in data" :key="index" class="node-item">
|
|
<span class="array-index">[{{ index }}]</span>
|
|
<JsonTreeNode
|
|
:data="item"
|
|
:path="buildArrayPath(index)"
|
|
:fields="fields"
|
|
:selected-paths="selectedPaths"
|
|
@select="$emit('select', $event)"
|
|
/>
|
|
<span v-if="index < data.length - 1" class="node-comma">,</span>
|
|
</div>
|
|
</div>
|
|
<div class="node-bracket">]</div>
|
|
</template>
|
|
|
|
<template v-else>
|
|
<span
|
|
class="node-value"
|
|
:class="[valueType, { selected: isSelected, clickable: true }]"
|
|
@click="handleClick"
|
|
:title="'点击选择: ' + path"
|
|
>
|
|
<template v-if="isString">"{{ data }}"</template>
|
|
<template v-else-if="isNull">null</template>
|
|
<template v-else>{{ data }}</template>
|
|
<span v-if="isSelected" class="selected-badge">{{ selectedFieldLabel }}</span>
|
|
</span>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed } from 'vue'
|
|
|
|
const props = defineProps({
|
|
data: {
|
|
type: [Object, Array, String, Number, Boolean, null],
|
|
default: null
|
|
},
|
|
path: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
fields: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
selectedPaths: {
|
|
type: Object,
|
|
default: () => ({})
|
|
}
|
|
})
|
|
|
|
const emit = defineEmits(['select'])
|
|
|
|
const isObject = computed(() => {
|
|
return props.data !== null && typeof props.data === 'object' && !Array.isArray(props.data)
|
|
})
|
|
|
|
const isArray = computed(() => {
|
|
return Array.isArray(props.data)
|
|
})
|
|
|
|
const isString = computed(() => {
|
|
return typeof props.data === 'string'
|
|
})
|
|
|
|
const isNull = computed(() => {
|
|
return props.data === null
|
|
})
|
|
|
|
const valueType = computed(() => {
|
|
if (props.data === null) return 'null'
|
|
if (typeof props.data === 'string') return 'string'
|
|
if (typeof props.data === 'number') return 'number'
|
|
if (typeof props.data === 'boolean') return 'boolean'
|
|
return 'unknown'
|
|
})
|
|
|
|
const isSelected = computed(() => {
|
|
return Object.values(props.selectedPaths).includes(props.path)
|
|
})
|
|
|
|
const selectedFieldLabel = computed(() => {
|
|
for (const [key, value] of Object.entries(props.selectedPaths)) {
|
|
if (value === props.path) {
|
|
const field = props.fields.find(f => f.key === key)
|
|
return field ? field.label.split(' ')[0] : key
|
|
}
|
|
}
|
|
return ''
|
|
})
|
|
|
|
const buildPath = (key) => {
|
|
return props.path ? `${props.path}.${key}` : key
|
|
}
|
|
|
|
const buildArrayPath = (index) => {
|
|
return `${props.path}[${index}]`
|
|
}
|
|
|
|
const isLastKey = (key) => {
|
|
const keys = Object.keys(props.data)
|
|
return keys[keys.length - 1] === key
|
|
}
|
|
|
|
const handleClick = () => {
|
|
if (props.path) {
|
|
emit('select', { path: props.path, value: props.data })
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.tree-node {
|
|
display: inline;
|
|
}
|
|
|
|
.node-bracket {
|
|
color: #8c8c8c;
|
|
display: inline;
|
|
}
|
|
|
|
.node-children {
|
|
padding-left: 20px;
|
|
display: block;
|
|
}
|
|
|
|
.node-item {
|
|
display: block;
|
|
}
|
|
|
|
.node-key {
|
|
color: #9254de;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.node-colon {
|
|
color: #8c8c8c;
|
|
margin: 0 4px;
|
|
}
|
|
|
|
.node-comma {
|
|
color: #8c8c8c;
|
|
}
|
|
|
|
.array-index {
|
|
color: #8c8c8c;
|
|
font-size: 11px;
|
|
margin-right: 4px;
|
|
}
|
|
|
|
.node-value {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 1px 4px;
|
|
border-radius: 3px;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.node-value.clickable {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.node-value.clickable:hover {
|
|
background: #e6f7ff;
|
|
outline: 1px solid #1890ff;
|
|
}
|
|
|
|
.node-value.string {
|
|
color: #389e0d;
|
|
}
|
|
|
|
.node-value.number {
|
|
color: #1890ff;
|
|
}
|
|
|
|
.node-value.boolean {
|
|
color: #eb2f96;
|
|
}
|
|
|
|
.node-value.null {
|
|
color: #8c8c8c;
|
|
font-style: italic;
|
|
}
|
|
|
|
.node-value.selected {
|
|
background: #bae7ff;
|
|
outline: 2px solid #1890ff;
|
|
}
|
|
|
|
.selected-badge {
|
|
font-size: 10px;
|
|
background: #1890ff;
|
|
color: #fff;
|
|
padding: 1px 6px;
|
|
border-radius: 10px;
|
|
font-weight: 500;
|
|
}
|
|
</style>
|