You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Vue.js中展示完整对象:动态嵌套JSON树形目录展示与编辑问题

在Vue.js中实现动态可编辑的JSON树形展示

我之前也遇到过类似的需求,这种结构不固定、嵌套层级未知的JSON树形展示+编辑,核心思路就是用递归组件——让组件自己调用自己来渲染每一层节点。下面给你一套可直接复用的实现方案:

1. 核心递归组件实现

我们先写一个名为JsonTreeNode的递归组件,它会根据当前节点的类型(对象/普通值)来渲染不同的内容,同时支持展开/折叠和编辑功能。

Vue3 组合式API版本

<template>
  <div class="json-tree-node">
    <!-- 如果是对象类型,渲染折叠/展开按钮和子节点 -->
    <div v-if="isObject(value)" class="node-header">
      <button @click="toggleExpand" class="expand-btn">
        {{ isExpanded ? '−' : '+' }}
      </button>
      <span class="node-key">{{ keyName }}</span>
      <!-- 展开状态下才渲染子节点 -->
      <div v-if="isExpanded" class="children">
        <json-tree-node
          v-for="(childValue, childKey) in value"
          :key="childKey"
          :key-name="childKey"
          :value="childValue"
          :model-value="modelValue"
          @update:model-value="handleUpdate"
        />
      </div>
    </div>
    <!-- 如果是普通值,渲染编辑输入框 -->
    <div v-else class="node-value">
      <span class="node-key">{{ keyName }}</span>
      <input
        v-model="editableValue"
        @input="handleInput"
        class="edit-input"
      />
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'

const props = defineProps({
  keyName: {
    type: String,
    required: true
  },
  value: {
    type: [Object, String, Number, Boolean],
    required: true
  },
  modelValue: {
    type: Object,
    required: true
  }
})

const emit = defineEmits(['update:modelValue'])

// 记录当前节点的展开状态
const isExpanded = ref(false)
// 判断当前值是否是对象(排除数组和null)
const isObject = (val) => {
  return typeof val === 'object' && val !== null && !Array.isArray(val)
}

// 切换展开/折叠状态
const toggleExpand = () => {
  isExpanded.value = !isExpanded.value
}

// 处理普通值的编辑输入
const editableValue = computed({
  get() {
    return props.value
  },
  set(newVal) {
    handleInput(newVal)
  }
})

const handleInput = (newVal) => {
  // 构造新对象保证响应式,递归更新对应属性
  const updateNestedObj = (obj, key, newValue) => {
    if (obj[key] && typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
      return { ...obj, [key]: updateNestedObj(obj[key], key, newValue) }
    } else {
      return { ...obj, [key]: newValue }
    }
  }
  emit('update:modelValue', updateNestedObj(props.modelValue, props.keyName, newVal))
}

// 接收子组件的更新事件并向上传递
const handleUpdate = (updatedModel) => {
  emit('update:modelValue', updatedModel)
}
</script>

<style scoped>
.json-tree-node {
  margin-left: 16px;
}
.node-header {
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
}
.expand-btn {
  width: 20px;
  height: 20px;
  padding: 0;
  border: none;
  background: #f0f0f0;
  border-radius: 4px;
  cursor: pointer;
}
.node-key {
  font-weight: 500;
}
.children {
  margin-top: 4px;
}
.node-value {
  display: flex;
  align-items: center;
  gap: 8px;
}
.edit-input {
  padding: 4px 8px;
  border: 1px solid #ddd;
  border-radius: 4px;
  min-width: 100px;
}
</style>

Vue2 选项式API版本(适配Vue2项目)

<template>
  <div class="json-tree-node">
    <div v-if="isObject(value)" class="node-header">
      <button @click="toggleExpand" class="expand-btn">
        {{ isExpanded ? '−' : '+' }}
      </button>
      <span class="node-key">{{ keyName }}</span>
      <div v-if="isExpanded" class="children">
        <json-tree-node
          v-for="(childValue, childKey) in value"
          :key="childKey"
          :key-name="childKey"
          :value="childValue"
          :model="model"
          @update="handleUpdate"
        />
      </div>
    </div>
    <div v-else class="node-value">
      <span class="node-key">{{ keyName }}</span>
      <input
        v-model="editableValue"
        @input="handleInput"
        class="edit-input"
      />
    </div>
  </div>
</template>

<script>
export default {
  name: 'JsonTreeNode', // 必须指定name才能递归调用
  props: {
    keyName: {
      type: String,
      required: true
    },
    value: {
      type: [Object, String, Number, Boolean],
      required: true
    },
    model: {
      type: Object,
      required: true
    }
  },
  data() {
    return {
      isExpanded: false,
      editableValue: this.value
    }
  },
  methods: {
    isObject(val) {
      return typeof val === 'object' && val !== null && !Array.isArray(val)
    },
    toggleExpand() {
      this.isExpanded = !this.isExpanded
    },
    handleInput(newVal) {
      // Vue2中用$set保证响应式更新
      const updatedModel = JSON.parse(JSON.stringify(this.model))
      this.updateNestedObj(updatedModel, this.keyName, newVal)
      this.$emit('update', updatedModel)
    },
    updateNestedObj(obj, key, newValue) {
      if (obj[key] && typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
        obj[key] = this.updateNestedObj(obj[key], key, newValue)
      } else {
        this.$set(obj, key, newValue)
      }
      return obj
    },
    handleUpdate(updatedModel) {
      this.$emit('update', updatedModel)
    }
  },
  watch: {
    value(newVal) {
      this.editableValue = newVal
    }
  }
}
</script>

<style scoped>
/* 样式和Vue3版本一致 */
.json-tree-node {
  margin-left: 16px;
}
.node-header {
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
}
.expand-btn {
  width: 20px;
  height: 20px;
  padding: 0;
  border: none;
  background: #f0f0f0;
  border-radius: 4px;
  cursor: pointer;
}
.node-key {
  font-weight: 500;
}
.children {
  margin-top: 4px;
}
.node-value {
  display: flex;
  align-items: center;
  gap: 8px;
}
.edit-input {
  padding: 4px 8px;
  border: 1px solid #ddd;
  border-radius: 4px;
  min-width: 100px;
}
</style>

2. 在父组件中使用递归组件

现在你可以在父组件中引入这个组件,把解析后的JSON对象传进去即可:

Vue3 父组件示例

<template>
  <div class="json-tree-container">
    <h2>可编辑JSON树形展示</h2>
    <json-tree-node
      v-for="(value, key) in jsonData"
      :key="key"
      :key-name="key"
      :value="value"
      v-model="jsonData"
    />
    <!-- 实时预览修改后的JSON -->
    <div class="preview">
      <h3>当前JSON预览</h3>
      <pre>{{ JSON.stringify(jsonData, null, 2) }}</pre>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import JsonTreeNode from './JsonTreeNode.vue'

// 替换成你解析后的JSON对象
const jsonData = ref({
  buttons: { login: "LOGIN", register: "REGISTER", next: "Next" },
  nav: { account: "Acount", login: "Login" }
})
</script>

<style scoped>
.json-tree-container {
  padding: 20px;
}
.preview {
  margin-top: 20px;
  padding: 10px;
  background: #f5f5f5;
  border-radius: 8px;
}
pre {
  white-space: pre-wrap;
  word-wrap: break-word;
}
</style>

3. 关键细节说明

  • 递归组件核心:Vue中组件必须指定name属性(Vue3的<script setup>会自动推断文件名,但显式指定更稳妥),才能在组件内部调用自身。
  • 响应式处理:Vue3中直接解构对象修改即可保持响应式;Vue2中需要用this.$set修改对象属性,否则无法触发视图更新。
  • 展开状态管理:每个节点独立维护展开/折叠状态,避免全局状态混乱,适配复杂嵌套结构。
  • 扩展性:如果需要支持数组类型,只需修改isObject方法添加数组判断,渲染时用数组索引作为key即可。

这样就能完美处理结构不固定、嵌套层级未知的JSON数据,实现树形展示和实时编辑功能了!

内容的提问来源于stack exchange,提问作者Michał Kruk

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 08:58:38