Vue基础教程(136)组件和组合API之使用组件创建树状项目分类:Vue组件树实战——用组合API打造会“繁殖”的项目分类神器

一、为什么你的项目分类总是像一锅乱炖?

还记得上次接手那个“简单”的项目分类需求吗?产品经理微笑着要求:“就做个能无限层级的树状分类,支持展开收起、选中高亮,最好还能动态加载数据哦~”

结果你写了300行代码,发现:

  • 展开收起逻辑和点击事件纠缠不清
  • 新增一个字段要修改5个地方
  • 深层嵌套的数据让你头晕目眩

别慌,这不是你的问题!传统开发思维确实难以应对树状结构的复杂性。但Vue的组件化开发,特别是组合式API,就是为这种场景而生的!

二、Vue组件基础:先理解什么是“会繁殖”的组件

2.1 组件到底是什么鬼?

简单说,组件就是能自己复制自己的代码块。想象一下乐高积木:你设计了一个基础模块,然后可以用它搭建出整个宇宙。

<!-- 最简单的组件示例 -->
<template>
  <div class="category-item">
    <h3>{{ title }}</h3>
    <p>{{ description }}</p>
  </div>
</template>
<script setup>
defineProps({
  title: String,
  description: String
})
</script>

但这只是个“死”组件,真正厉害的是...

2.2 组件的终极奥义:递归!

树状分类的核心就是组件自己调用自己,就像俄罗斯套娃一样:

<!-- CategoryNode.vue -->
<template>
  <div class="node">
    <div class="node-content" @click="toggle">
      {{ node.name }}
    </div>
    
    <!-- 关键在这里:组件调用自身! -->
    <div v-if="isExpanded" class="children">
      <CategoryNode
        v-for="child in node.children"
        :key="child.id"
        :node="child"
      />
    </div>
  </div>
</template>

看到没?CategoryNode组件内部又使用了CategoryNode,这就是递归组件的魔力!

三、组合式API:给你的组件装上涡轮增压

3.1 为什么选择组合API?

选项式API(Options API)就像在IKEA买东西:所有零件都给你分类放好,但你想改个设计得跑遍整个商场。

组合式API(Composition API)更像是乐高工作室:所有积木块任你挑选,想怎么组合就怎么组合。

3.2 为树状组件量身定制的组合函数
// useTree.js
import { ref, computed } from 'vue'

export function useTreeNode(node) {
  const isExpanded = ref(false)
  const isSelected = ref(false)
  
  const toggle = () => {
    isExpanded.value = !isExpanded.value
  }
  
  const select = () => {
    isSelected.value = true
  }
  
  // 计算是否有子节点
  const hasChildren = computed(() => {
    return node.children && node.children.length > 0
  })
  
  return {
    isExpanded,
    isSelected,
    toggle,
    select,
    hasChildren
  }
}

这样就把树节点的状态逻辑完全抽离,可以在任何组件中复用!

四、实战:从零搭建会“生孩子”的树状分类

4.1 数据结构设计:打好地基

好的数据结构是成功的一半:

// 树节点数据结构
const treeData = [
  {
    id: 1,
    name: '前端开发',
    children: [
      {
        id: 2, 
        name: '框架',
        children: [
          { id: 3, name: 'Vue' },
          { id: 4, name: 'React' }
        ]
      },
      {
        id: 5,
        name: '构建工具',
        children: [
          { id: 6, name: 'Webpack' },
          { id: 7, name: 'Vite' }
        ]
      }
    ]
  }
]
4.2 核心组件实现:让树“长”起来
<!-- CategoryTree.vue -->
<template>
  <div class="category-tree">
    <CategoryNode
      v-for="node in treeData"
      :key="node.id"
      :node="node"
      :level="0"
    />
  </div>
</template>
<script setup>
import CategoryNode from './CategoryNode.vue'

defineProps({
  treeData: {
    type: Array,
    required: true
  }
})
</script>
<!-- CategoryNode.vue -->
<template>
  <div class="tree-node" :style="{ marginLeft: `${level * 20}px` }">
    <!-- 节点内容 -->
    <div 
      class="node-content"
      :class="{ 
        'is-selected': isSelected,
        'has-children': hasChildren
      }"
      @click="handleClick"
    >
      <span 
        class="expand-icon"
        v-if="hasChildren"
      >
        {{ isExpanded ? '📂' : '📁' }}
      </span>
      <span class="node-name">{{ node.name }}</span>
    </div>
    
    <!-- 递归渲染子节点 -->
    <div v-if="isExpanded && hasChildren" class="children">
      <CategoryNode
        v-for="child in node.children"
        :key="child.id"
        :node="child"
        :level="level + 1"
      />
    </div>
  </div>
</template>
<script setup>
import { useTreeNode } from '../composables/useTree'

const props = defineProps({
  node: {
    type: Object,
    required: true
  },
  level: {
    type: Number,
    default: 0
  }
})

const { 
  isExpanded, 
  isSelected, 
  toggle, 
  select, 
  hasChildren 
} = useTreeNode(props.node)

const handleClick = () => {
  if (hasChildren.value) {
    toggle()
  }
  select()
}
</script>
<style scoped>
.tree-node {
  text-align: left;
  cursor: pointer;
}

.node-content {
  padding: 8px 12px;
  border-radius: 4px;
  transition: all 0.2s;
}

.node-content:hover {
  background-color: #f5f5f5;
}

.is-selected {
  background-color: #e3f2fd;
  color: #1976d2;
}

.expand-icon {
  margin-right: 8px;
  transition: transform 0.2s;
}

.children {
  transition: all 0.3s ease;
}
</style>
4.3 添加高级功能:让树更智能

动态加载数据:

// 在useTree.js中添加
async function loadChildren(node) {
  if (!node.children || node.children.length === 0) {
    const newChildren = await fetchChildren(node.id)
    node.children = newChildren
  }
}

搜索过滤:

// 搜索功能
const searchQuery = ref('')

const filteredTree = computed(() => {
  if (!searchQuery.value) return treeData
  
  function filterNodes(nodes) {
    return nodes.filter(node => {
      const isMatch = node.name.includes(searchQuery.value)
      if (node.children) {
        node.children = filterNodes(node.children)
        return isMatch || node.children.length > 0
      }
      return isMatch
    })
  }
  
  return filterNodes([...treeData])
})

五、避坑指南:我踩过的坑你们就别踩了

5.1 递归组件的无限循环问题

错误示范:

// 这会导致栈溢出!
const node = {
  ...props.node,
  children: props.node.children || []
}

正确做法:

// 确保有终止条件!
const hasChildren = computed(() => {
  return props.node.children && props.node.children.length > 0
})
5.2 响应式数据更新陷阱
// 错误:直接修改props
props.node.children = newChildren

// 正确:使用emit或者重新赋值
const emit = defineEmits(['update:node'])
function updateNode(newNode) {
  emit('update:node', newNode)
}
5.3 性能优化:成百上千节点也不卡
// 虚拟滚动:只渲染可见节点
const visibleNodes = computed(() => {
  // 根据滚动位置计算可见节点
})

// 防抖搜索
const debouncedSearch = useDebounce(searchQuery, 300)

六、完整示例:复制粘贴就能用的树状菜单

下面是一个完整的、可以直接使用的示例:

<template>
  <div class="tree-demo">
    <div class="search-box">
      <input 
        v-model="searchQuery"
        placeholder="搜索分类..."
        class="search-input"
      />
    </div>
    
    <CategoryTree :tree-data="filteredTree" />
  </div>
</template>
<script setup>
import { ref, computed } from 'vue'
import CategoryTree from './components/CategoryTree.vue'

// 模拟数据
const treeData = ref([
  {
    id: 1,
    name: '技术栈',
    children: [
      {
        id: 2,
        name: '前端',
        children: [
          { id: 3, name: 'Vue.js' },
          { id: 4, name: 'React' },
          { id: 5, name: 'Angular' }
        ]
      },
      {
        id: 6,
        name: '后端',
        children: [
          { id: 7, name: 'Node.js' },
          { id: 8, name: 'Java' },
          { id: 9, name: 'Python' }
        ]
      }
    ]
  },
  {
    id: 10,
    name: '产品设计',
    children: [
      { id: 11, name: 'UI设计' },
      { id: 12, name: 'UX设计' },
      { id: 13, name: '交互设计' }
    ]
  }
])

const searchQuery = ref('')

const filteredTree = computed(() => {
  // 实现搜索过滤逻辑
  return treeData.value
})
</script>
<style>
.tree-demo {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
}

.search-input {
  width: 100%;
  padding: 8px 12px;
  border: 1px solid #ddd;
  border-radius: 4px;
  margin-bottom: 20px;
}
</style>

七、总结:从此告别“面条代码”

通过Vue组件和组合式API,我们实现了:

高度可复用:一个组件搞定无限层级
逻辑清晰:状态管理和UI渲染分离
易于维护:新增功能只需修改少量代码
性能优秀:虚拟滚动支持大数据量

记住,好的组件设计就像培养皿里的细胞——给它合适的养分(数据),它就能自己生长出完整的结构。现在就去试试这个会“繁殖”的树状组件吧,保证让你的项目分类从此井然有序!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

值引力

持续创作,多谢支持!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值