init
This commit is contained in:
396
src/components/JsonTreeNode.vue
Normal file
396
src/components/JsonTreeNode.vue
Normal file
@@ -0,0 +1,396 @@
|
||||
<template>
|
||||
<div class="json-tree-node" :class="{ 'is-matched': isMatched, 'is-filtered': isFiltered }">
|
||||
<div
|
||||
class="node-line"
|
||||
:class="{ 'has-children': hasChildren, 'is-expanded': isExpanded, 'is-matched': isMatched }"
|
||||
@click="toggle"
|
||||
>
|
||||
<span v-if="hasChildren" class="expand-icon">
|
||||
<i :class="isExpanded ? 'fas fa-chevron-down' : 'fas fa-chevron-right'"></i>
|
||||
</span>
|
||||
<span v-else class="expand-placeholder"></span>
|
||||
<span v-if="showPath && nodePath" class="node-path">{{ nodePath }}:</span>
|
||||
<span class="node-key" v-if="key !== null">
|
||||
<span class="key-name" :class="{ 'matched': isMatched }">{{ key }}</span>:
|
||||
</span>
|
||||
<span class="node-value" :class="[valueType, { 'matched': isMatched }]">
|
||||
<span v-if="valueType === 'string'">"{{ displayValue }}"</span>
|
||||
<span v-else>{{ displayValue }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="hasChildren && isExpanded && shouldShowChildren" class="node-children">
|
||||
<JsonTreeNode
|
||||
v-for="(child, index) in filteredChildren"
|
||||
:key="index"
|
||||
:data="child.value"
|
||||
:key-name="child.key"
|
||||
:path="child.path"
|
||||
:expanded="expanded"
|
||||
:matched-paths="matchedPaths"
|
||||
:json-path-query="jsonPathQuery"
|
||||
@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
|
||||
},
|
||||
matchedPaths: {
|
||||
type: Set,
|
||||
default: () => new Set()
|
||||
},
|
||||
jsonPathQuery: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
showPath: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
nodePath: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
// 判断当前节点是否匹配 JSONPath
|
||||
const isMatched = computed(() => {
|
||||
if (!props.jsonPathQuery || !props.jsonPathQuery.trim()) return false
|
||||
return props.matchedPaths.has(currentPath.value)
|
||||
})
|
||||
|
||||
// 判断是否有筛选
|
||||
const hasFilter = computed(() => {
|
||||
return props.jsonPathQuery && props.jsonPathQuery.trim() !== ''
|
||||
})
|
||||
|
||||
// 判断是否应该显示子节点
|
||||
const shouldShowChildren = computed(() => {
|
||||
if (!hasFilter.value) return true
|
||||
// 如果节点匹配,显示子节点(展开匹配节点的内容)
|
||||
if (isMatched.value) return true
|
||||
// 如果当前节点是匹配节点的子节点,显示所有子节点
|
||||
if (isChildOfMatched.value) return true
|
||||
// 检查是否有子节点匹配
|
||||
return hasMatchingChildren.value
|
||||
})
|
||||
|
||||
// 检查是否有子节点匹配
|
||||
const hasMatchingChildren = computed(() => {
|
||||
if (!hasFilter.value) return true
|
||||
if (!hasChildren.value) return false
|
||||
|
||||
const basePath = props.path === '' && props.keyName === null ? 'root' : currentPath.value
|
||||
|
||||
if (Array.isArray(props.data)) {
|
||||
return props.data.some((item, index) => {
|
||||
const childPath = `${basePath}[${index}]`
|
||||
return props.matchedPaths.has(childPath) || checkChildrenMatch(item, childPath)
|
||||
})
|
||||
}
|
||||
|
||||
if (typeof props.data === 'object' && props.data !== null) {
|
||||
return Object.keys(props.data).some(key => {
|
||||
const childPath = basePath === 'root' ? `root.${key}` : `${basePath}.${key}`
|
||||
return props.matchedPaths.has(childPath) || checkChildrenMatch(props.data[key], childPath)
|
||||
})
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
// 递归检查子节点是否匹配
|
||||
const checkChildrenMatch = (obj, path) => {
|
||||
if (props.matchedPaths.has(path)) return true
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.some((item, index) => {
|
||||
const childPath = `${path}[${index}]`
|
||||
return props.matchedPaths.has(childPath) || checkChildrenMatch(item, childPath)
|
||||
})
|
||||
}
|
||||
|
||||
if (typeof obj === 'object' && obj !== null) {
|
||||
return Object.keys(obj).some(key => {
|
||||
const childPath = `${path}.${key}`
|
||||
return props.matchedPaths.has(childPath) || checkChildrenMatch(obj[key], childPath)
|
||||
})
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
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 filteredChildren = computed(() => {
|
||||
if (!hasFilter.value) return children.value
|
||||
|
||||
// 如果当前节点匹配,显示所有子节点(不进行过滤)
|
||||
if (isMatched.value) return children.value
|
||||
|
||||
// 如果当前节点是匹配节点的子节点,显示所有子节点(不进行过滤)
|
||||
if (isChildOfMatched.value) return children.value
|
||||
|
||||
// 否则,只显示匹配的子节点或其子节点匹配的子节点
|
||||
return children.value.filter(child => {
|
||||
const childPath = child.path === 'root'
|
||||
? (typeof child.key === 'number' ? `root[${child.key}]` : `root.${child.key}`)
|
||||
: (typeof child.key === 'number' ? `${child.path}[${child.key}]` : `${child.path}.${child.key}`)
|
||||
|
||||
// 如果子节点匹配,显示
|
||||
if (props.matchedPaths.has(childPath)) return true
|
||||
|
||||
// 如果子节点的子节点匹配,也显示
|
||||
return checkChildrenMatch(child.value, childPath)
|
||||
})
|
||||
})
|
||||
|
||||
// 检查当前节点是否是匹配节点的子节点
|
||||
const isChildOfMatched = computed(() => {
|
||||
if (!hasFilter.value || isMatched.value) return false
|
||||
|
||||
// 检查当前路径是否以任何匹配路径开头(作为前缀)
|
||||
for (const matchedPath of props.matchedPaths) {
|
||||
if (matchedPath === 'root') continue
|
||||
|
||||
// 如果当前路径以匹配路径开头,且后面跟着 . 或 [,说明是子节点
|
||||
const prefix = matchedPath + '.'
|
||||
const prefixBracket = matchedPath + '['
|
||||
if (currentPath.value.startsWith(prefix) || currentPath.value.startsWith(prefixBracket)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
// 判断是否被筛选(有筛选但当前节点不匹配且没有匹配的子节点,且不是匹配节点的子节点)
|
||||
const isFiltered = computed(() => {
|
||||
if (!hasFilter.value) return false
|
||||
if (isMatched.value) return false
|
||||
if (isChildOfMatched.value) return false
|
||||
return !hasMatchingChildren.value
|
||||
})
|
||||
|
||||
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-icon i {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.expand-placeholder {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.node-path {
|
||||
color: #666666;
|
||||
font-size: 0.8125rem;
|
||||
margin-right: 8px;
|
||||
font-family: 'Courier New', monospace;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
/* 匹配的节点高亮样式 */
|
||||
.json-tree-node.is-matched .node-line {
|
||||
background: #fff9e6;
|
||||
border-left: 3px solid #ff9800;
|
||||
padding-left: 5px;
|
||||
margin-left: -8px;
|
||||
}
|
||||
|
||||
.json-tree-node.is-matched .key-name.matched {
|
||||
color: #ff6f00;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.json-tree-node.is-matched .node-value.matched {
|
||||
color: #ff6f00;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 被筛选掉的节点隐藏 */
|
||||
.json-tree-node.is-filtered {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user