级联组件不好用?实现自定义级联组件,不受UI控件限制

自定义递归实现级联组件,可默认展开全部子节点

前言

级联组件是各个UI组件库中不可或缺的组件,他们常作为下拉选择使用,可以多选或者单选,但是在最近开发中,这个组件并不能实现产品经理无理的需求(需要默认展开所有子节点,需要支持某一项可多选,其他层不需要选择框。。。等等),既然UI框架用不了,只能自己动手造轮子,能够自定义每个选项的插槽内容。源码git地址:gitee.com/fcli/vue-ca...

实现效果图如下:

递归组件

对于未知的层级和不确定的树节点,递归大法是最好的选择。

为什么需要递归组件

级联组件的数据结构,其实就是树结构的平铺展开方式,因此如果需要实现一个级联组件,首先使用递归组件是一个最好的选择,配合插槽的使用,能够动态地改变每一层每一个节点的样式和操作方式。

vue如何实现递归组件

需要在组件中使用自身组件,首先需要定义name属性。如果使用的是vue3的setup语法糖,所以需要单独写一个script标签定义name。

xml 复制代码
<script lang="ts" >
export default {
    name: 'cascadePanel',
}
</script>

这样在模板中就能通过name使用组件,代码如下:

xml 复制代码
<template>
<div class="cascade-panel">
    <cascade-panel>
    </cascade-panel>
</div>
</template>

递归级联组件实现

模板

通过propsIndex判断当前属于哪一层,并通过slot插槽方便上层自定义内容。

ruby 复制代码
<div class="cascade-panel">
    <div class="menu-item" :class="['level_' + propsIndex]">
        <div class="slot-item" v-for="(item, index) in datalist" :key="index" @click="change(index)"
            :class="[Number(index) == childActiveId ? 'active' : '']">
            <slot name="slotScope" :data="item"></slot>
        </div>
    </div>
    <div class="menu-item" :class="['out_level_' + (propsIndex + 1)]"
        v-if="datalist[childActiveId] && datalist[childActiveId].children">
        <cascade-panel ref="cascadeRef" :datalist="datalist[childActiveId].children" :propsIndex="propsIndex + 1"
            v-on:patch="onPatch">
            <template #slotScope="slotProps">
                <slot name="slotScope" :data="slotProps.data"></slot>
            </template>
        </cascade-panel>
    </div>
</div>

注意:在内层的插槽定义时data需要使用slotProps.data,不然如果使用slotScope会导致数据嵌套,会有很多层的data

ruby 复制代码
<template #slotScope="slotProps">
      <slot name="slotScope" :data="slotProps.data"></slot>
</template>

其余主要代码

1、为了让每一层点击的时候都能让下面的几层展开并选中第一个,所以需要在初始化时将childActiveId的值置为0,并循环调用子组件,让所有的子节点都能保持第一个选中状态。

2、通过patch方法,记录当前点击的节点的路径,有需要的小伙伴可自定义修改

需要抛出对应方法,父组件才能调用defineExpose({init})

ini 复制代码
<script lang="ts" >
export default {
    name: 'cascadePanel',
}
</script>
<script lang="ts" setup>
import { ref, defineProps } from 'vue';
const props = defineProps({
    datalist: { default: [], type: Array<any> },
    propsIndex: {
        default: 1,
        type: Number
    }
});
const emit = defineEmits(['patch'])
const childActiveId = ref(0);
const cascadeRef = ref<any>(null);
const init = () => {
    childActiveId.value = 0;
    cascadeRef.value && cascadeRef.value.init();
};
const change = (index: number) => {
    init();
    //点击之后下一个组件显示,activeId初始化0
    childActiveId.value = index
    if (!props.datalist[childActiveId.value].children) {
        emit("patch", props.datalist[childActiveId.value].label);
    }
}
const onPatch = (val: any) => {
    var checkedVal = props.datalist[childActiveId.value].label + "/" + val;
    emit("patch", checkedVal);
}
defineExpose({init})

</script>
<style>
.cascade-panel {
    display: flex;
}
</style>

使用组件

在app.vue 中调用该组件,传入树节点值,并自定义插槽内容。示例中适当实现了一些多选方案。代码如下:

ini 复制代码
<template>
  <div class="content">
    <VueCascadePanel :datalist="dataList">
      <template #slotScope="slotProps">
        <div class="item">{{ slotProps.data.label }}
          <input type="checkbox" @change="(e) => changeSelect(e, slotProps.data)" v-model="slotProps.data.isChecked" :checked="modelNode(slotProps.data)" />
          </div>
      </template>
    </VueCascadePanel>
  </div>
</template>

<script setup lang="ts">
import VueCascadePanel from './plugin/index.vue';
import { ref } from 'vue';
components: {
  VueCascadePanel
}


const changeSelect = (e: any, data: any) => {
  const isChecked = e.target._modelValue;
  getData(isChecked, data)
}
const getData = (isChecked: Boolean, data: any) => {
  //如果选中,递归修改子节点的选中状态
  if (data.children?.length > 0) {
    data.isChecked = isChecked;
    data.children.forEach((item: any) => {
      getData(isChecked, item);
    })
  } else {
    data.isChecked = isChecked;
  }
  console.log(data)
}

const modelNode = (node: any) => {
  checkedNode.value[node.value] = node;
  return false;
}

const checkedNode = ref<any>({});
const dataList = ref<any>([{
  value: 'zhinan',
  label: '指南',
  children: [{
    value: 'shejiyuanze',
    label: '设计原则',
    isChecked: false,
    children: [{
      value: 'yizhi',
      label: '一致',
      isChecked: false,
    }, {
      value: 'fankui',
      label: '反馈',
      isChecked: false,

    }, {
      value: 'xiaolv',
      label: '效率',
      isChecked: false,

    }, {
      value: 'kekong',
      label: '可控',
      isChecked: false,

    }]
  }, {
    value: 'daohang',
    label: '导航',
    isChecked: false,

    children: [{
      value: 'cexiangdaohang',
      label: '侧向导航',
      isChecked: false,

    }, {
      value: 'dingbudaohang',
      label: '顶部导航',
      isChecked: false,

    }]
  }]
}, {
  value: 'zujian',
  label: '组件',
  children: [{
    value: 'basic',
    label: 'Basic',
    isChecked: false,

    children: [{
      value: 'layout',
      label: 'Layout 布局',
      isChecked: false,

    }, {
      value: 'color',
      label: 'Color 色彩',
      isChecked: false,

    }, {
      value: 'typography',
      label: 'Typography 字体',
      isChecked: false,

    }, {
      value: 'icon',
      label: 'Icon 图标',
      isChecked: false,

    }, {
      value: 'button',
      label: 'Button 按钮',
      isChecked: false,

    }]
  }, {
    value: 'form',
    label: 'Form',
    isChecked: false,

    children: [{
      value: 'radio',
      label: 'Radio 单选框',
      isChecked: false,

    }, {
      value: 'checkbox',
      label: 'Checkbox 多选框',
      isChecked: false,

    }, {
      value: 'input',
      label: 'Input 输入框',
      isChecked: false,

    }, {
      value: 'input-number',
      label: 'InputNumber 计数器',
      isChecked: false,

    }, {
      value: 'select',
      label: 'Select 选择器',
      isChecked: false,

    }, {
      value: 'cascader',
      label: 'Cascader 级联选择器',
      isChecked: false,

    }, {
      value: 'switch',
      label: 'Switch 开关',
      isChecked: false,

    }, {
      value: 'slider',
      label: 'Slider 滑块',
      isChecked: false,

    }, {
      value: 'time-picker',
      label: 'TimePicker 时间选择器',
      isChecked: false,

    }, {
      value: 'date-picker',
      label: 'DatePicker 日期选择器',
      isChecked: false,

    }, {
      value: 'datetime-picker',
      label: 'DateTimePicker 日期时间选择器',
      isChecked: false,

    }, {
      value: 'upload',
      label: 'Upload 上传',
      isChecked: false,

    }, {
      value: 'rate',
      label: 'Rate 评分',
      isChecked: false,

    }, {
      value: 'form',
      label: 'Form 表单',
      isChecked: false,

    }]
  }, {
    value: 'data',
    label: 'Data',
    isChecked: false,

    children: [{
      value: 'table',
      label: 'Table 表格',
      isChecked: false,

    }, {
      value: 'tag',
      label: 'Tag 标签',
      isChecked: false,

    }, {
      value: 'progress',
      label: 'Progress 进度条',
      isChecked: false,

    }, {
      value: 'tree',
      label: 'Tree 树形控件',
      isChecked: false,

    }, {
      value: 'pagination',
      label: 'Pagination 分页',
      isChecked: false,

    }, {
      value: 'badge',
      label: 'Badge 标记',
      isChecked: false,

    }]
  }, {
    value: 'notice',
    label: 'Notice',
    isChecked: false,

    children: [{
      value: 'alert',
      label: 'Alert 警告',
      isChecked: false,

    }, {
      value: 'loading',
      label: 'Loading 加载',
      isChecked: false,

    }, {
      value: 'message',
      label: 'Message 消息提示',
      isChecked: false,

    }, {
      value: 'message-box',
      label: 'MessageBox 弹框',
      isChecked: false,

    }, {
      value: 'notification',
      label: 'Notification 通知',
      isChecked: false,

    }]
  }, {
    value: 'navigation',
    label: 'Navigation',
    isChecked: false,

    children: [{
      value: 'menu',
      label: 'NavMenu 导航菜单',
      isChecked: false,

    }, {
      value: 'tabs',
      label: 'Tabs 标签页',
      isChecked: false,

    }, {
      value: 'breadcrumb',
      label: 'Breadcrumb 面包屑',
      isChecked: false,

    }, {
      value: 'dropdown',
      label: 'Dropdown 下拉菜单',
      isChecked: false,

    }, {
      value: 'steps',
      label: 'Steps 步骤条',
      isChecked: false,

    }]
  }, {
    value: 'others',
    label: 'Others',
    isChecked: false,

    children: [{
      value: 'dialog',
      label: 'Dialog 对话框',
      isChecked: false,

    }, {
      value: 'tooltip',
      label: 'Tooltip 文字提示',
      isChecked: false,

    }, {
      value: 'popover',
      label: 'Popover 弹出框',
      isChecked: false,

    }, {
      value: 'card',
      label: 'Card 卡片',
      isChecked: false,

    }, {
      value: 'carousel',
      label: 'Carousel 走马灯',
      isChecked: false,

    }, {
      value: 'collapse',
      label: 'Collapse 折叠面板',
      isChecked: false,

    }]
  }]
}, {
  value: 'ziyuan',
  label: '资源',
  isChecked: false,

  children: [{
    value: 'axure',
    label: 'Axure Components',
    isChecked: false,

  }, {
    value: 'sketch',
    label: 'Sketch Templates',
    isChecked: false,

  }, {
    value: 'jiaohu',
    label: '组件交互文档',
    isChecked: false,

  }]
}]);
</script>

<style>
.cascade-panel {
  background: #f6f9f9;
}

.menu-item .active {
  background: #d9e4fa;
}

.menu-item .item {
  line-height: 32px;
  width: 200px;
  cursor: pointer;
}
</style>

总结

本文主要实现了级联组件的基本思路,保留了更多自定义实现,如果需要实现复杂无理的需求可以参考使用,如果只是简单的选择,推荐使用UI框架里的组件。

相关推荐
mldong6 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排7 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60618 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角9 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!9 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰9 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi10 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen11 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马12 小时前
AI源码分析:拆解模型底层实现逻辑
前端
IT_陈寒12 小时前
React子组件莫名其妙重渲染?你可能漏了这个Hook
前端·人工智能·后端