当前位置: 首页 > news >正文

树形结构三级分类列表

使用vue3加antd-design实现树形控件(三级分类)

主要内容包括:

  1. 使用a-tree组件构建树形结构,支持三级分类展示
  2. 实现分类的增删改查功能:
    • 通过右键菜单提供创建、编辑、删除操作
    • 使用模态框进行表单交互
  3. 添加搜索功能,可过滤分类并自动展开匹配节点
  4. 响应式设计:
    • 动态计算分类层级
    • 实时更新树形数据
    • 自动展开/收起节点
<template> <div class="cbox"> <div style="display:flex;align-items:center;justify-content: space-between"> <span style="font-size: 15px;">创建分类</span> <PlusOutlined @click="openCreateModal(null, 1)" style="cursor: pointer" /> </div> <div> <a-input-search v-model:value="searchValue" allowClear style="width: 100%; margin: 10px 0px" placeholder="搜索分类" @input="onSearch" /> </div> <!-- 使用 Ant Design Tree 组件 --> <a-tree :tree-data="filteredTreeData" :expanded-keys="expandedKeys" :selected-keys="selectedKeys" @expand="onExpand" @select="onSelect" block-node > <template #title="{ node }"> <div class="tree-node-title"> <span>{{ node.title }}</span> <a-dropdown :trigger="['click']" placement="bottomRight"> <MoreOutlined class="node-actions-icon" @click.stop /> <template #overlay> <a-menu @click="({ key }) => handleMenuAction(key, node)"> <a-menu-item key="create" v-if="node.level < 3">创建{{ getSubLevelName(node.level) }}分类</a-menu-item> <a-menu-item key="edit">编辑</a-menu-item> <a-menu-item key="delete">删除</a-menu-item> </a-menu> </template> </a-dropdown> </div> </template> </a-tree> <!-- 创建/编辑弹窗 --> <a-modal v-model:visible="modalVisible" :title="modalTitle" @ok="handleModalOk" @cancel="handleModalCancel" > <a-form layout="vertical"> <a-form-item label="分类名称" required> <a-input v-model:value="formData.name" placeholder="请输入分类名称" /> </a-form-item> </a-form> </a-modal> </div> </template> <script lang="ts" setup> import { ref, computed } from 'vue'; import { PlusOutlined, MoreOutlined } from '@ant-design/icons-vue'; import { message, TreeProps } from 'ant-design-vue'; interface TreeNode { key: string; title: string; level: number; parentKey?: string; children?: TreeNode[]; scopedSlots?: { title: string }; } const treeData = ref<TreeNode[]>([ { key: '1', title: '全部分类 (13)', level: 1, scopedSlots: { title: 'title' }, children: [ { key: '1-1', title: '未分组 (13)', level: 2, parentKey: '1', scopedSlots: { title: 'title' }, }, { key: '1-2', title: '分类1 (0)', level: 2, parentKey: '1', scopedSlots: { title: 'title' }, children: [ { key: '1-2-1', title: '子分类1 (0)', level: 3, parentKey: '1-2', scopedSlots: { title: 'title' }, } ] } ] } ]); const searchValue = ref(''); const expandedKeys = ref<string[]>(['1']); const selectedKeys = ref<string[]>([]); const modalVisible = ref(false); const modalTitle = ref(''); const formData = ref({ name: '' }); const currentEditNode = ref<TreeNode | null>(null); const currentParentNode = ref<TreeNode | null>(null); const createLevel = ref(1); const getSubLevelName = (level: number) => { if (level === 1) return '二级'; if (level === 2) return '三级'; return ''; }; const handleMenuAction = (action: string, node: TreeNode) => { if (action === 'create') { openCreateModal(node, node.level + 1); } else if (action === 'edit') { openEditModal(node); } else if (action === 'delete') { handleDelete(node); } }; const openCreateModal = (parentNode: TreeNode | null, level: number) => { if (level > 3) { message.warning('最多支持三级分类'); return; } currentParentNode.value = parentNode; currentEditNode.value = null; createLevel.value = level; modalTitle.value = `创建${level === 2 ? '二级' : level === 3 ? '三级' : ''}分类`; formData.value.name = ''; modalVisible.value = true; }; const openEditModal = (node: TreeNode) => { currentEditNode.value = node; modalTitle.value = '编辑分类'; formData.value.name = node.title.replace(/\(\d+\)$/, '').trim(); modalVisible.value = true; }; const handleDelete = (node: TreeNode) => { const deleteNode = (nodes: TreeNode[], key: string): boolean => { for (let i = 0; i < nodes.length; i++) { if (nodes[i].key === key) { nodes.splice(i, 1); return true; } if (nodes[i].children && deleteNode(nodes[i].children, key)) { return true; } } return false; }; const newData = [...treeData.value]; deleteNode(newData, node.key); treeData.value = newData; message.success('删除成功'); }; const handleModalOk = () => { if (!formData.value.name.trim()) { message.warning('请输入分类名称'); return; } if (currentEditNode.value) { // 编辑逻辑 const updateNode = (nodes: TreeNode[]): boolean => { for (let i = 0; i < nodes.length; i++) { if (nodes[i].key === currentEditNode.value!.key) { const countMatch = nodes[i].title.match(/\((\d+)\)$/); nodes[i].title = countMatch ? `${formData.value.name} (${countMatch[1]})` : formData.value.name; return true; } if (nodes[i].children && updateNode(nodes[i].children)) { return true; } } return false; }; const newData = [...treeData.value]; updateNode(newData); treeData.value = newData; message.success('编辑成功'); } else { // 创建逻辑 const newNode: TreeNode = { key: `key_${Date.now()}_${Math.random()}`, title: `${formData.value.name} (0)`, level: createLevel.value, scopedSlots: { title: 'title' }, children: createLevel.value === 3 ? undefined : [], }; if (!currentParentNode.value) { // 根节点创建 treeData.value = [...treeData.value, newNode]; } else { // 子节点创建 const addToParent = (nodes: TreeNode[]): boolean => { for (let i = 0; i < nodes.length; i++) { if (nodes[i].key === currentParentNode.value!.key) { if (!nodes[i].children) nodes[i].children = []; nodes[i].children!.push(newNode); // 自动展开父节点 if (!expandedKeys.value.includes(nodes[i].key)) { expandedKeys.value.push(nodes[i].key); } return true; } if (nodes[i].children && addToParent(nodes[i].children)) { return true; } } return false; }; const newData = [...treeData.value]; addToParent(newData); treeData.value = newData; } message.success('创建成功'); } modalVisible.value = false; }; const handleModalCancel = () => { modalVisible.value = false; }; const onExpand = (keys: string[]) => { expandedKeys.value = keys; }; const onSelect = (keys: string[]) => { selectedKeys.value = keys; }; // 搜索过滤 const onSearch = () => { if (!searchValue.value) { expandedKeys.value = ['1']; return; } // 展开所有匹配的节点路径 const getAllParentKeys = (nodes: TreeNode[], keyword: string): string[] => { let keys: string[] = []; for (const node of nodes) { if (node.title.toLowerCase().includes(keyword.toLowerCase())) { keys.push(node.key); let parent = findParentNode(treeData.value, node.key); while (parent) { keys.push(parent.key); parent = findParentNode(treeData.value, parent.key); } } if (node.children) { keys = [...keys, ...getAllParentKeys(node.children, keyword)]; } } return [...new Set(keys)]; }; const findParentNode = (nodes: TreeNode[], childKey: string): TreeNode | null => { for (const node of nodes) { if (node.children?.some(child => child.key === childKey)) { return node; } if (node.children) { const found = findParentNode(node.children, childKey); if (found) return found; } } return null; }; if (searchValue.value) { expandedKeys.value = getAllParentKeys(treeData.value, searchValue.value); } }; const filteredTreeData = computed(() => { if (!searchValue.value) return treeData.value; const filterNodes = (nodes: TreeNode[]): TreeNode[] => { return nodes.reduce<TreeNode[]>((acc, node) => { const matches = node.title.toLowerCase().includes(searchValue.value.toLowerCase()); let filteredChildren: TreeNode[] = []; if (node.children) { filteredChildren = filterNodes(node.children); } if (matches || filteredChildren.length > 0) { acc.push({ ...node, children: filteredChildren.length > 0 ? filteredChildren : node.children, }); } return acc; }, []); }; return filterNodes(treeData.value); }); </script> <style scoped lang="scss"> .cbox { padding: 16px; background: #fff; border-radius: 8px; } .tree-node-title { display: flex; align-items: center; justify-content: space-between; width: 100%; padding-right: 8px; .node-actions-icon { opacity: 0; transition: opacity 0.2s; cursor: pointer; font-size: 14px; color: #999; &:hover { color: #1890ff; } } &:hover .node-actions-icon { opacity: 1; } } :deep(.ant-tree-node-content-wrapper) { width: 100%; } </style>
http://www.jsqmd.com/news/691309/

相关文章:

  • 从EdgeX到CVAT:我是如何用Docker Compose搭建一个安全的本地AI数据标注工作流的
  • 告别驱动烦恼:手把手教你为RTL8188GU芯片网卡在Linux下编译安装rtl8xxxu驱动
  • SCons构建MDK工程翻车实录:从‘No module named building’到完美运行的踩坑全指南
  • 2025-2026知识管理平台排行榜发布:泛微·采知连为何成为企业首选?
  • 【实战解析】STM32驱动BLDC无感控制:从反电动势过零检测到稳定换向
  • Windows下ESP32开发环境搭建:Clion 2024.x + ESP-IDF v5.x 最新版配置指南
  • MACKO-SpMV:低稀疏度下的GPU加速与存储优化
  • Word论文排版小技巧:如何一键实现连续文献引用[1-3]格式(附详细操作截图)
  • 【独家泄露】车规级MCU嵌入式大模型安全合规报告(ISO/SAE 21434 ASPICE Level 3交叉映射表)
  • 不止于转动:用STM32F103的PWM精细控制MG996舵机角度,实现平滑运动与多点定位
  • Qwen3.5-9B-GGUF部署案例:边缘设备Jetson Orin Nano轻量化部署实践
  • 2026年4月河南考研机构推荐:五家口碑服务评测对比领先二战生择校迷茫 - 品牌推荐
  • 国产高速复合开关标杆|四方杰芯 FSW6860:5 路高速 + 2 路低速,一站式搞定 USB Type‑C 全接口设计
  • Qianfan-OCR参数详解:4096 token上限下百页PDF摘要生成实测与截断策略
  • 别再对着指针发懵了!用CodeBlocks的Watch窗口一步步调试,把内存地址和引用关系看得明明白白
  • Phi-3.5-mini-instruct生成技术文档与API手册实战
  • Phi-mini-MoE-instruct的“思维过程”可视化:注意力机制与专家路由分析
  • Linux Mint 21.3 新机到手必做的5个设置,让你的桌面更顺手(附软件源更换保姆级教程)
  • IMDb电影评论情感分析数据预处理实战指南
  • 用免费Grok作自动素材池
  • 2025-2026年国内河南考研机构推荐:五大口碑服务对比评测领先在职考生时间碎片化规划 - 品牌推荐
  • Docker 27跨平台镜像兼容性测试实战手册:从manifest list校验、goos/goarch比对到符号表ABI一致性扫描,一文覆盖全部19个关键检查点
  • 潮玩抽赏小程序一番赏玩法实操解析:运营避坑,快速跑通变现
  • 【5G Modem】从协议栈到天线阵列:揭秘5G Modem的完整架构与协同设计
  • 效率翻倍!一款超好用的投简历Edge插件“塔塔网申”体验分享
  • RWKV-7 (1.5B World)轻量化方案:FlashAttention-2集成与显存再压缩
  • 从Segmentation Fault到零P0事故:某头部自动驾驶公司落地2026 C内存规范的7步迁移路径(含静态分析规则集v3.2)
  • 去哪个嵌入式培训机构学习比较好
  • 别再只会移动物体了!用Godot4的Tween系统实现5种酷炫游戏动画(附完整代码)
  • NVIDIA开发者课程:GPU加速AI与数据科学实战指南