197 lines
4.8 KiB
Vue
197 lines
4.8 KiB
Vue
|
|
<template>
|
||
|
|
<div class="json-mapping-tree-viewer">
|
||
|
|
<div class="json-tree-toolbar">
|
||
|
|
<div class="json-tree-meta">{{ metaText }}</div>
|
||
|
|
<div class="json-tree-actions">
|
||
|
|
<slot name="actions" />
|
||
|
|
<el-button type="primary" plain size="small" :disabled="!rootNode" @click="expandAll">全部展开</el-button>
|
||
|
|
<el-button plain size="small" :disabled="!rootNode" @click="collapseAll">全部收起</el-button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div v-if="rootNode" class="json-tree-body">
|
||
|
|
<JsonTreeNode :node="rootNode" :depth="0" :is-last="true" :expanded-keys="expandedKeys" @toggle="toggleNode" />
|
||
|
|
</div>
|
||
|
|
<pre v-else class="mapping-json-text">{{ source }}</pre>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script setup lang="ts">
|
||
|
|
import { computed, ref, watch } from 'vue'
|
||
|
|
import JsonTreeNode, { type JsonTreeNodeModel, type JsonValueType } from './JsonMappingTreeNode.vue'
|
||
|
|
|
||
|
|
defineOptions({
|
||
|
|
name: 'JsonMappingTree'
|
||
|
|
})
|
||
|
|
|
||
|
|
type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }
|
||
|
|
const props = defineProps<{
|
||
|
|
source: string
|
||
|
|
metaText?: string
|
||
|
|
}>()
|
||
|
|
|
||
|
|
const expandedKeys = ref<Set<string>>(new Set())
|
||
|
|
|
||
|
|
const parsedJson = computed<{ valid: true; value: JsonValue } | { valid: false }>(() => {
|
||
|
|
try {
|
||
|
|
return {
|
||
|
|
valid: true,
|
||
|
|
value: JSON.parse(props.source) as JsonValue
|
||
|
|
}
|
||
|
|
} catch {
|
||
|
|
return {
|
||
|
|
valid: false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
const rootNode = computed(() => {
|
||
|
|
if (!parsedJson.value.valid) return null
|
||
|
|
|
||
|
|
return buildJsonNode(undefined, parsedJson.value.value, '$')
|
||
|
|
})
|
||
|
|
|
||
|
|
watch(
|
||
|
|
rootNode,
|
||
|
|
node => {
|
||
|
|
expandedKeys.value = new Set(node ? collectContainerKeys(node) : [])
|
||
|
|
},
|
||
|
|
{ immediate: true }
|
||
|
|
)
|
||
|
|
|
||
|
|
function buildJsonNode(keyName: string | undefined, source: JsonValue, path: string): JsonTreeNodeModel {
|
||
|
|
if (Array.isArray(source)) {
|
||
|
|
return {
|
||
|
|
id: path,
|
||
|
|
keyName,
|
||
|
|
openToken: '[',
|
||
|
|
closeToken: ']',
|
||
|
|
summary: ` ${source.length} 项`,
|
||
|
|
children: source.map((item, index) => buildJsonNode(undefined, item, `${path}/${index}`))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (source && typeof source === 'object') {
|
||
|
|
const entries = Object.entries(source)
|
||
|
|
|
||
|
|
return {
|
||
|
|
id: path,
|
||
|
|
keyName,
|
||
|
|
openToken: '{',
|
||
|
|
closeToken: '}',
|
||
|
|
summary: ` ${entries.length} 项`,
|
||
|
|
children: entries.map(([key, value]) => buildJsonNode(key, value, `${path}/${escapePathKey(key)}`))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
id: path,
|
||
|
|
keyName,
|
||
|
|
valueText: formatPrimitiveValue(source),
|
||
|
|
valueType: getPrimitiveType(source)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function collectContainerKeys(node: JsonTreeNodeModel): string[] {
|
||
|
|
if (!node.children) return []
|
||
|
|
|
||
|
|
return [node.id, ...node.children.flatMap(collectContainerKeys)]
|
||
|
|
}
|
||
|
|
|
||
|
|
function escapePathKey(key: string) {
|
||
|
|
return key.replace(/~/g, '~0').replace(/\//g, '~1')
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatPrimitiveValue(source: JsonValue) {
|
||
|
|
if (typeof source === 'string') return JSON.stringify(source)
|
||
|
|
if (source === null) return 'null'
|
||
|
|
return String(source)
|
||
|
|
}
|
||
|
|
|
||
|
|
function getPrimitiveType(source: JsonValue): JsonValueType {
|
||
|
|
if (source === null) return 'null'
|
||
|
|
if (typeof source === 'boolean') return 'boolean'
|
||
|
|
if (typeof source === 'number') return 'number'
|
||
|
|
return 'string'
|
||
|
|
}
|
||
|
|
|
||
|
|
function toggleNode(id: string) {
|
||
|
|
const nextKeys = new Set(expandedKeys.value)
|
||
|
|
|
||
|
|
if (nextKeys.has(id)) {
|
||
|
|
nextKeys.delete(id)
|
||
|
|
} else {
|
||
|
|
nextKeys.add(id)
|
||
|
|
}
|
||
|
|
|
||
|
|
expandedKeys.value = nextKeys
|
||
|
|
}
|
||
|
|
|
||
|
|
function expandAll() {
|
||
|
|
if (!rootNode.value) return
|
||
|
|
|
||
|
|
expandedKeys.value = new Set(collectContainerKeys(rootNode.value))
|
||
|
|
}
|
||
|
|
|
||
|
|
function collapseAll() {
|
||
|
|
expandedKeys.value = new Set()
|
||
|
|
}
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<style scoped lang="scss">
|
||
|
|
.json-mapping-tree-viewer {
|
||
|
|
display: flex;
|
||
|
|
flex: 1;
|
||
|
|
flex-direction: column;
|
||
|
|
min-height: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.json-tree-toolbar {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: flex-end;
|
||
|
|
min-height: 28px;
|
||
|
|
gap: 8px;
|
||
|
|
margin-bottom: 8px;
|
||
|
|
white-space: nowrap;
|
||
|
|
}
|
||
|
|
|
||
|
|
.json-tree-meta {
|
||
|
|
flex: 1;
|
||
|
|
min-width: 0;
|
||
|
|
overflow: hidden;
|
||
|
|
color: #64748b;
|
||
|
|
font-size: 13px;
|
||
|
|
text-overflow: ellipsis;
|
||
|
|
}
|
||
|
|
|
||
|
|
.json-tree-actions {
|
||
|
|
display: flex;
|
||
|
|
flex: 0 0 auto;
|
||
|
|
align-items: center;
|
||
|
|
gap: 8px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.json-tree-body,
|
||
|
|
.mapping-json-text {
|
||
|
|
flex: 1;
|
||
|
|
min-height: 0;
|
||
|
|
margin: 0;
|
||
|
|
padding: 16px;
|
||
|
|
border: 1px solid #dbe3f0;
|
||
|
|
border-radius: 10px;
|
||
|
|
background: #ffffff;
|
||
|
|
overflow: auto;
|
||
|
|
font-family: Consolas, 'Courier New', monospace;
|
||
|
|
font-size: 13px;
|
||
|
|
line-height: 1.7;
|
||
|
|
color: #172033;
|
||
|
|
}
|
||
|
|
|
||
|
|
.mapping-json-text {
|
||
|
|
white-space: pre-wrap;
|
||
|
|
word-break: break-word;
|
||
|
|
}
|
||
|
|
|
||
|
|
</style>
|