[{"data":1,"prerenderedAt":198},["ShallowReactive",2],{"content:\u002Fposts\u002F2025\u002Fbuilding-recursive-components-in-vue":3,"surround:\u002Fposts\u002F2025\u002Fbuilding-recursive-components-in-vue":187},{"id":4,"title":5,"body":6,"canonical":16,"categories":165,"date":167,"description":25,"draft":168,"extension":169,"image":170,"meta":171,"navigation":172,"path":173,"permalink":174,"readingTime":175,"recommend":180,"references":174,"seo":181,"sitemap":182,"stem":183,"tags":184,"type":174,"updated":167,"__hash__":186},"content\u002Fposts\u002F2025\u002FBuilding-Recursive-components-in-Vue.md","【翻译】在 Vue 中构建递归组件",{"type":7,"value":8,"toc":159},"minimark",[9,20,23,26,42,45,50,57,60,70,73,79,82,88,91,99,105,111,117,123,126,135,138,144,147,150,153,156],[10,11,12,13],"p",{},"原文地址：",[14,15,19],"a",{"href":16,"rel":17},"https:\u002F\u002Fdev.to\u002Fjacobandrewsky\u002Fbuilding-recursive-components-in-vue-46cc",[18],"nofollow","Building Recursive components in Vue",[21,22],"hr",{},[10,24,25],{},"几天前，我想构建一个 Vue 组件，它能够显示嵌套节点树，但我们不知道确切的嵌套数量 - 因此它需要能够递归工作。我很惊讶网络上没有太多关于它的资源，所以我决定写一篇关于它的文章。",[10,27,28,29,33,34,37,38,41],{},"主要思想是让一个组件接受数据属性（目前数据结构未知）和另外两个属性用于",[30,31,32],"code",{"code":32},"summary"," ",[30,35,36],{"code":36},"open"," ；第一个用于定义",[30,39,40],{"code":40},"details","标签中使用的名称和显示的文本，第二个用于查看节点树是否打开。",[10,43,44],{},"我希望你会喜欢它:)",[46,47,49],"h2",{"id":48},"在-vue-中构建递归组件","在 Vue 中构建递归组件",[10,51,52,53,56],{},"在此示例中，我使用 VueUse ",[30,54,55],{"code":55},"onClickOutside"," 工具函数来处理外部单击操作（我就是喜欢这个工具！）。",[10,58,59],{},"首先，让我们定义该组件将接受的 props：",[61,62,68],"pre",{"className":63,"code":65,"language":66,"meta":67},[64],"language-ts","const props = defineProps({\n    summary: {\n        type: String,\n        default: 'disclosure'\n    },\n    open: {\n        type: Boolean,\n        default: false\n    },\n    content: {\n        type: Object,\n        default: () => ({})\n    }\n})\n","ts","",[30,69,65],{"__ignoreMap":67},[10,71,72],{},"接下来，让我们定义反应式引用和计算变量：",[61,74,77],{"className":75,"code":76,"language":66,"meta":67},[64],"const computedOpenState = ref(props.open)\nconst details = ref(null)\nconst hasNestedChildren = computed(() => props.content.children?.length)\n",[30,78,76],{"__ignoreMap":67},[10,80,81],{},"最后，在脚本标签的最后，让我们添加函数来处理切换细节和外部点击：",[61,83,86],{"className":84,"code":85,"language":66,"meta":67},[64],"function toggleDetails(event: ToggleEvent) {\n    computedOpenState.value = event.newState === 'open'\n}\n\nonClickOutside(details, () => (computedOpenState.value = false))\n",[30,87,85],{"__ignoreMap":67},[10,89,90],{},"现在，让我们向组件添加一些模板。首先，如果嵌套内容没有更多嵌套子项，我们只想显示一个带有摘要名称的段落：",[61,92,97],{"className":93,"code":95,"language":96,"meta":67},[94],"language-html","\u003Cp v-if=\"!hasNestedChildren\">\n  {{ summary }}\n\u003C\u002Fp>\n","html",[30,98,95],{"__ignoreMap":67},[10,100,101,102,104],{},"在其他情况下，我们想要利用 ",[30,103,40],{"code":40}," HTML 标签：",[61,106,109],{"className":107,"code":108,"language":96,"meta":67},[94],"\u003Cp v-if=\"!hasNestedChildren\">\n  {{ summary }}\n\u003C\u002Fp>\n\u003Cdetails\n  v-else\n  :open=\"computedOpenState\"\n  :name=\"summary\"\n  ref=\"details\"\n  @toggle=\"toggleDetails\"\n>\n  ...\n\u003C\u002Fdetails>\n",[30,110,108],{"__ignoreMap":67},[10,112,113,114,116],{},"在 ",[30,115,40],{"code":40}," 标签内，我们要添加以下代码来处理显示摘要和嵌套节点树：",[61,118,121],{"className":119,"code":120,"language":96,"meta":67},[94],"\u003Csummary>\n  {{ summary }}\n\u003C\u002Fsummary>\n\u003Ctemplate v-if=\"hasNestedChildren\">\n  \u003CNodeTree\n    v-for=\"(child, key) in content.children\"\n    :key\n    :summary=\"child.summary\"\n    :content=\"child\"\n    @click=\"toggleChildren\"\n  \u002F>\n\u003C\u002Ftemplate>\n",[30,122,120],{"__ignoreMap":67},[10,124,125],{},"下面，您可以看到该组件的完整代码：",[61,127,133],{"className":128,"code":130,"filename":131,"language":132,"meta":67},[129],"language-vue","\u003Cscript setup lang=\"ts\">\nimport { onClickOutside } from '@vueuse\u002Fcore'\nimport { computed, ref } from 'vue'\n\nconst props = defineProps({\n    summary: {\n        type: String,\n        default: 'disclosure'\n    },\n    open: {\n        type: Boolean,\n        default: false\n    },\n    content: {\n        type: Object,\n        default: () => ({})\n    }\n})\n\nconst computedOpenState = ref(props.open)\nconst details = ref(null)\nconst hasNestedChildren = computed(() => props.content.children?.length)\n\nfunction toggleDetails(event: ToggleEvent) {\n    computedOpenState.value = event.newState === 'open'\n}\n\nonClickOutside(details, () => (computedOpenState.value = false))\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n    \u003Cp v-if=\"!hasNestedChildren\">\n        {{ summary }}\n    \u003C\u002Fp>\n    \u003Cdetails\n        v-else\n        ref=\"details\"\n        :open=\"computedOpenState\"\n        :name=\"summary\"\n        @toggle=\"toggleDetails\"\n    >\n        \u003Csummary>\n            {{ summary }}\n        \u003C\u002Fsummary>\n        \u003Ctemplate v-if=\"hasNestedChildren\">\n            \u003CNodeTree\n                v-for=\"(child, key) in content.children\"\n                :key\n                :summary=\"child.summary\"\n                :content=\"child\"\n                @click=\"toggleChildren\"\n            \u002F>\n        \u003C\u002Ftemplate>\n    \u003C\u002Fdetails>\n\u003C\u002Ftemplate>\n","NodeTree.vue","vue",[30,134,130],{"__ignoreMap":67},[10,136,137],{},"最后，该组件可以像下面这样使用：",[61,139,142],{"className":140,"code":141,"language":132,"meta":67},[129],"\u003Ctemplate>\n    \u003CNodeTree v-if=\"data\" :content=\"data\" :summary=\"data.summary\" \u002F>\n\u003C\u002Ftemplate>\n",[30,143,141],{"__ignoreMap":67},[10,145,146],{},"就是这样！您拥有完全正常工作的递归节点树组件:)",[46,148,149],{"id":149},"总结",[10,151,152],{},"干得好！您刚刚学习了如何构建递归 Vue 组件。",[10,154,155],{},"保重，下次再见！",[10,157,158],{},"And happy coding as always 🖥️",{"title":67,"searchDepth":160,"depth":160,"links":161},4,[162,164],{"id":48,"depth":163,"text":49},2,{"id":149,"depth":163,"text":149},[166],"译","2025-01-14 13:46:17",false,"md","https:\u002F\u002Fbitmc.uno\u002Fpicgo\u002FBuilding-Recursive-components-in-Vue.jpg",{},true,"\u002Fposts\u002F2025\u002Fbuilding-recursive-components-in-vue",null,{"text":176,"minutes":177,"time":178,"words":179},"4 min read",3.23,193800,646,1,{"title":5,"description":25},{"loc":173},"posts\u002F2025\u002FBuilding-Recursive-components-in-Vue",[185],"Vue","ZqDOTTUqH0wKFM4dyu0cemFTv-eD64GDciqAJwmAVzc",[188,193],{"title":189,"path":190,"stem":191,"date":192,"type":174,"children":-1},"【翻译】在Vue单文件组件中使用两个script块","\u002Fposts\u002F2025\u002Fusing-two-script-blocks-in-vue-single-file-components","posts\u002F2025\u002Fusing-two-script-blocks-in-vue-single-file-components","2025-01-14 11:02:22",{"title":194,"path":195,"stem":196,"date":197,"type":174,"children":-1},"从本地到生产：使用 Docker 部署 Nuxt","\u002Fposts\u002F2025\u002Ffrom-local-to-production-deploy-nuxt-with-docker","posts\u002F2025\u002Ffrom-local-to-production-deploy-nuxt-with-docker","2025-01-15 14:16:16",1789642585483]