移动端树形选择组件 -- 基于vant4+vue3 支持多选、单选、节点搜索

2023-11-16

效果图

多选

单选

父组件使用

<treeSelect ref="treeSelectRef" v-model="value" :listData="listData"
                    placeholder="请选择"></treeSelect>

子组件代码  

index.vue

<template>
  <van-field v-model="data.names" is-link readonly v-bind="$attrs" @click="sendWordOpen" />
  <van-popup v-model:show="sendWordShow" round position="bottom">
    <div class="tree-box">
      <van-search label-align="left" v-model="data.treeParam" show-action placeholder="请输入搜索关键词">
        <template #action>
          <div @click="searchTreeByParam">搜索</div>
        </template>
      </van-search>
      <div class="tree-container">
        <div class="tree-btns">

          <van-checkbox style="margin-right:24px;" v-if="props.multiple" shape="square" @click.stop="allSelectChange"
            v-model="data.allSelect">全选</van-checkbox>

          <van-radio-group v-model="data.state" direction="horizontal" @change="handleState">
            <van-radio name="0">在职</van-radio>
            <van-radio name="1">离职</van-radio>
          </van-radio-group>
        </div>
        <div class="tree-data">
          <TreeSelect ref="treeSelectRef" @change="checkChange" :labelKey="props.labelKey" :idKey="props.idKey"
            :pidKey="props.pidKey" :isLink="props.isLink" :list="data.list" :listObj="data.listObj"
            :treeParamAlready="data.treeParamAlready" :multiple="props.multiple" @confirm="onConfirm"
            :defaultId="props.modelValue[0]"></TreeSelect>
        </div>


      </div>

    </div>

    <div class="tree-confirm">
      <van-button v-if="multiple" type="primary" block @click="handleConfirm">确定</van-button>
    </div>
  </van-popup>
</template>

<script setup>
import { reactive, watch, ref, nextTick, onMounted } from 'vue'
import TreeSelect from "./tree.vue";
import request from "@/utils/request"
import { showLoadingToast, closeToast } from 'vant';

const emits = defineEmits(["update:modelValue", "change", 'confirm'])
const props = defineProps({
  // 绑定值
  modelValue: {
    type: Array,
    default: [],
  },
  listData: {
    type: Array,
    default() {
      return [];
    },
  },
  // label key
  labelKey: {
    type: String,
    default() {
      return "name";
    },
  },
  // id key
  idKey: {
    type: String,
    default() {
      return "id";
    },
  },
  // pid key
  pidKey: {
    type: String,
    default() {
      return "pid";
    },
  },
  // 是否联动勾选
  isLink: {
    type: Boolean,
    default() {
      return true;
    },
  },
  // 是否多选
  multiple: {
    type: Boolean,
    default: true
  }
})

const data = reactive({
  treeParam: "",
  treeParamAlready: "", // 已经进行了搜索的参数
  list: props.listData, // 树数组
  listObj: {}, // 数组对象
  selectList: [], // 选中的数据
  searchSomeDataList: [], // 搜索相同对象数组
  canCheckList: [], // 能够选择的数据集合
  canCheckListFixed: [], // 固定的能够选择的数据集合
  allSelect: false, // 是否全选
  state: '0', // "" -- 全部   0 -- 在职   1 -- 离职
  names: ''
})

const treeSelectRef = ref(null)

const init = (type) => {
  if (type) {
    data.names = "";
  }

  data.treeParam = "";
  data.treeParamAlready = "";
  data.canCheckList = [];
  data.canCheckListFixed = [];
}
const initData = (options) => {
  if (options && options.length) {
    data.list = options
    init();
    data.listObj = setListObj(options);
  }
}
// 查询在职离职业务员
const handleState = (type) => {
  showLoadingToast({
    message: '加载中...',
    forbidClick: true,
  });
  request({ url: `请求地址`, method: "GET" }).then(res => {
    initData(res.data.data)
    closeToast()
  }).catch(() => {
    closeToast()
  })
}
// 将树形数据转为扁平对象
const setListObj = (list, pid) => {
  let listObj = {};
  list.forEach((itm) => {
    if (pid) {
      itm[props.pidKey] = pid;
    }
    data.canCheckList.push(itm);
    data.canCheckListFixed.push(itm);
    listObj[itm[props.idKey]] = itm;
    if (itm.children && itm.children.length) {
      listObj = {
        ...listObj,
        ...setListObj(itm.children, itm[props.idKey]),
      };
    }
  });
  return listObj;
}

// 确认
const handleConfirm = () => {
  const showSelectList = filterData(data.selectList)
  // data.names = showSelectList.map((itm) => itm[props.labelKey]).join(',')
  emits("update:modelValue", showSelectList.map((itm) => itm[props.idKey]))
  emits('confirm', showSelectList)
  sendWordShow.value = false
}
const onConfirm = (e) => {
  // data.names = e.map((itm) => itm[props.labelKey]).join(',')
  emits("update:modelValue", e.map((itm) => itm[props.idKey]))
  emits('confirm', e)
  sendWordShow.value = false
}

// 过滤数据
const filterData = (selectList) => {
  if (
    data.canCheckList.length ===
    selectList.filter(
      (itm) => itm[props.labelKey].indexOf(data.treeParamAlready) !== -1
    ).length
  ) {
    data.allSelect = true;
  } else {
    data.allSelect = false;
  }
  // 过滤出展示中,且打勾的数据
  const showSelectList = selectList.filter((itm) => {
    return !itm.isHide && !itm.isShowChildren
  });
  return showSelectList
}
// 该方法在 树形数据变化 和 全选变化 时会执行
const checkChange = (selectList) => {
  data.selectList = selectList;
  const showSelectList = filterData(selectList)
  emits("change", showSelectList);
}
// 根据参数搜索
const searchTreeByParam = () => {
  const someDataList = []; // 搜索数据
  const someDataCanCheckList = []; // 搜索且能够check的数据
  data.treeParamAlready = data.treeParam;
  treeSelectRef.value?.outDataBuffer();
  for (const id in data.listObj) {
    if (data.treeParam) {
      data.listObj[id].isHide = true
      data.listObj[id].isShowChildren = false
      if (data.listObj[id][props.labelKey].indexOf(data.treeParam) !== -1 || data.listObj[id].checked) {
        data.listObj[id].isHide = false;
        someDataList.push(data.listObj[id]);
        someDataCanCheckList.push(data.listObj[id]);
      }
    } else {
      data.listObj[id].isHide = false
    }
  }

  data.searchSomeDataList = someDataList;
  data.canCheckList = someDataCanCheckList.length
    ? someDataCanCheckList
    : deepClone(data.canCheckListFixed);
  setShowData(someDataList);
  checkChange(data.selectList);
}
// 设置展示和展开数据
const setShowData = (datas, bool) => {
  const d = [];
  datas.forEach((itm) => {
    if (itm[props.pidKey]) {
      if (!d.find(item => item[props.idKey] === data.listObj[itm[props.pidKey]][props.idKey])) {
        d.push(data.listObj[itm[props.pidKey]]);
      }
      if (bool === false || bool === true) {
        data.listObj[itm[props.pidKey]].checked = bool;
      }
      data.listObj[itm[props.pidKey]].isHide = false;
      data.listObj[itm[props.pidKey]].isShowChildren = true;
    }
  });
  if (d.length) {
    setShowData(d, bool);
  }

}
// 获取全部可选择数据,进行全选/取消
const toggleAllSelectData = (bool) => {
  let selectData = [];
  for (const id in data.listObj) {
    data.listObj[id].isShowChildren = false
    if (
      data.listObj[id][props.labelKey].indexOf(data.treeParam) !== -1
    ) {
      data.listObj[id].checked = bool
      data.listObj[id].isHide = false
      selectData.push(data.listObj[id]);
    }
  }
  setShowData(selectData, bool);
}
// 全选状态改变
const allSelectChange = () => {
  toggleAllSelectData(data.allSelect);
}
const deepClone = (obj) => {
  const type = Object.prototype.toString.call(obj); // 通过原型对象获取对象类型
  let newObj;
  if (type === "[object Array]") {
    // 数组
    newObj = [];
    if (obj.length > 0) {
      for (let i = 0; i < obj.length; i++) {
        newObj.push(deepClone(obj[i]));
      }
    }
  } else if (type === "[object Object]") {
    // 对象
    newObj = {};
    for (const i in obj) {
      newObj[i] = deepClone(obj[i]);
    }
  } else {
    // 基本类型和方法可以直接赋值
    newObj = obj;
  }
  return newObj;
}

// 设置默认值
const setDefault = () => {
  const someDataList = []; // 默认数据
  const someDataCanCheckList = []; // 搜索且能够check的数据
  treeSelectRef.value?.outDataBuffer();
  for (const id in data.listObj) {
    data.listObj[id].checked = false
    data.listObj[id].isShowChildren = false
    props.modelValue.forEach((mid) => {
      if (data.listObj[id][props.idKey] === mid) {
        data.listObj[id].checked = true;
        someDataList.push(data.listObj[id]);
        someDataCanCheckList.push(data.listObj[id]);
      }
    })

  }
  data.names = someDataList.map(item => item.name).join(',')

  data.searchSomeDataList = someDataList;
  data.canCheckList = someDataCanCheckList.length
    ? someDataCanCheckList
    : deepClone(data.canCheckListFixed);
  setShowData(someDataList);
  // filterData();
  // checkChange(data.selectList);
}


watch(() => props.listData, () => {
  initData(props.listData)
}, { deep: true, immediate: true })

watch(() => props.modelValue, () => {
  setDefault();
}, { deep: true, immediate: true })

onMounted(() => {
  nextTick(() => {
    setDefaultTime()
  })
})

const setDefaultTime = () => {
  if (props.listData.length === 0) {
    setTimeout(() => {
      setDefaultTime()
    }, 100);
  } else {
    setDefault()
  }

}



const sendWordShow = ref(false)
const sendWordOpen = () => {
  sendWordShow.value = true
  if(props.multiple){
    setDefault();
  }
  // searchTreeByParam();
}

defineExpose({
  init,
  setListObj,
  checkChange,
  searchTreeByParam,
  setShowData,
  toggleAllSelectData,
  allSelectChange,
  deepClone,
  setDefault
})
</script>

<style lang="scss" scoped>
.tree-box {
  --van-search-content-background-color: #eeeeee;
  --van-search-content-background: #eeeeee;
}

.tree-container {
  width: 100%;
  padding: 32px 32px 0;
}

.tree-data {
  height: 60vh;
  overflow-y: auto;
}

.tree-btns {
  width: 100%;
  margin-bottom: 24px;
  display: flex;
  align-items: center;
}

.tree-confirm {
  width: 100%;
  padding: 12px 32px;
}
</style>

tree.vue

<template>
  <div class="list">
    <div class="item" v-for="item in props.list" :key="item[idKey]" v-show="!item.isHide">
      <div class="title">
        <div class="checkbox-box">
          <van-checkbox v-if="props.multiple" icon-size="16px" shape="square" @click.stop="checkChange(item)" v-model="item.checked"><span style="font-size: 15px;">{{ item[labelKey] }}</span></van-checkbox>
          <p v-else :style="{fontSize: '15px',color : defaultId === item[idKey] ? 'var(--van-primary-color)' : ''}" @click.stop="checkChange(item)">{{ item[labelKey] }}</p>
        </div>
        <div @click.stop="itemClick(item)" class="arrow">
            <van-icon v-if="item.children && item.children.length" :name="item.isShowChildren ? 'arrow-up' : 'arrow-down'" />
        </div>
        
      </div>
      <div class="tree" v-show="item.isShowChildren">
        <tree :labelKey="props.labelKey" :idKey="props.idKey" :pidKey="props.pidKey" :isLink="props.isLink"
          v-if="item.children && item.children.length" :list="item.children" :listObj="props.listObj"
          :isFirstFloor="false" :multiple="props.multiple" @confirm="onConfirm" :defaultId="defaultId">
        </tree>
      </div>
    </div>
  </div>
</template>
<script setup>
import { reactive, watch } from 'vue'
import tree from './tree.vue'
const emits = defineEmits(["change","confirm"])
const props = defineProps({
  // label key
  labelKey: {
    type: String,
    default() {
      return "name";
    },
  },
  // id key
  idKey: {
    type: String,
    default() {
      return "id";
    },
  },
  // pid key
  pidKey: {
    type: String,
    default() {
      return "pid";
    },
  },
  // 是否联动勾选
  isLink: {
    type: Boolean,
    default() {
      return true;
    },
  },
  // 是否是第一层
  isFirstFloor: {
    type: Boolean,
    default() {
      return true;
    },
  },
  // 树形结构
  list: {
    type: Array,
    default() {
      return [];
    },
  },
  // 树形扁平化数据
  listObj: {
    type: Object,
    default() {
      return {};
    },
  },
  // 树形搜索参数
  treeParamAlready: {
    type: String,
    default() {
      return "";
    },
  },
  // 是否全选
  allSelect: {
    type: Boolean,
    default() {
      return false;
    },
  },
  // 是否多选
  multiple : {
    type: Boolean,
    default : true
  },
  // 单选默认值
  defaultId : String
})

const data = reactive({
  firstLoad: true,
  checkboxValue1: [],
  showList: [],
  isOutData: true, // 需要将数据抛出
})

watch(() => props.list, () => {
  if (data.firstLoad) {
    outDataBuffer();
    data.firstLoad = false;
  }
  // 判断 是第一层树 且 不是进行显示隐藏操作时,进行数据的抛出
  if (props.isFirstFloor && data.isOutData) {
    if(props.multiple){
      outCheckedData();
    }
  }
}, { deep: true })

// 展开
const itemClick = (item) => {
  outDataBuffer();
  item.isShowChildren = !item.isShowChildren
  
}
// 数据抛出缓冲(在list数据变化时,不想抛出选择的数据时,调用该方法)
const outDataBuffer = () => {
  data.isOutData = false;
  setTimeout(() => {
    data.isOutData = true;
  }, 500);
}
// 获取选中对象
const getCheckData = (list) => {
  let deptList = [];
  list.forEach((itm) => {
    // && itm.label.indexOf(this.treeParamAlready) !== -1
    if (itm.checked) {
      deptList.push(itm);
    }
    if (itm.children && itm.children.length) {
      deptList = deptList.concat(getCheckData(itm.children));
    }
  });
  return deptList;
}
// 单项checked改变
const checkChange = (item) => {
  // 多选
  if (props.multiple) {
    // item.checked = !item.checked
    if (props.isLink) {
      // 展开所有可以展开的节点
      if (item.checked) {
        expandAll(item);
      }
      // 判断父级是否需要勾选
      checkParent(item);
      // 勾选子级
      if (item.children && item.children.length) {
        checkChidren(item.children, item.checked);
      }
    }
    return
  }

  // 单选
  if(item.children && item.children.length) return
  toggleAllSelectData(props.list)
  item.checked = true
  outCheckedData();
}

// 获取全部可选择数据,进行全选/取消
const toggleAllSelectData = (list) => {
  list.forEach((itm) => {
    itm.checked = false
    if (itm.children && itm.children.length) {
      toggleAllSelectData(itm.children)
    }
  });
}

// 展开所有可以展开的节点
const expandAll = (item) => {
  if (item.children?.length) {
    item.isShowChildren = true
    item.children.forEach(itm => {
      expandAll(itm);
    })
  }
}
// 判断父级是否需要勾选
const checkParent = (item) => {
  // 父级不存在不再往下走
  if (!props.listObj[item[props.pidKey]]) {
    return;
  }
  let someDataCount = 0; // 同级的相同父级数据量
  let checkedDataCount = 0; // 同级已勾选的数据量
  for (const id in props.listObj) {
    const itm = props.listObj[id];
    if (itm[props.pidKey] === item[props.pidKey] && !itm.isHide) {
      someDataCount++;
      if (itm.checked) {
        checkedDataCount++;
      }
    }
  }
  const isEqual = someDataCount === checkedDataCount;
  if (props.listObj[item[props.pidKey]].checked != isEqual) {
    props.listObj[item[props.pidKey]].checked = isEqual
    checkParent(props.listObj[item[props.pidKey]]);
  }
}
// 根据父级统一取消勾选或勾选
const checkChidren = (list, isChecked) => {
  list.forEach((itm) => {
    itm.checked = isChecked
    if (itm.children && itm.children.length) {
      checkChidren(itm.children, isChecked);
    }
  });
}
// 抛出选中的数据
const outCheckedData = () => {
  const checkedList = getCheckData(props.list);
  emits("change", checkedList);
  onConfirm(checkedList)
}

const onConfirm = (e) => {
  if(props.multiple) return
  if(e[0].children && e[0].children.length) return
  emits("confirm", e);
}

defineExpose({
  itemClick,
  outDataBuffer,
  getCheckData,
  checkChange,
  expandAll,
  checkParent,
  checkChidren,
  outCheckedData,
})
</script>

<style lang="scss" scoped>

.list {
  .item {
    margin-bottom: 10px;

    .title {
      display: flex;
      align-items: center;
      justify-content: space-between;
      margin-bottom: 10px;

      .checkbox-box {
        display: flex;
        align-items: center;
        cursor: pointer;
        padding: 10px 0;
      }

      .arrow{
        width: 80px;
        display: flex;
        justify-content: flex-end;
      }
    }

    .tree {
      margin-left: 50px;
    }
  }
}
</style>

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

移动端树形选择组件 -- 基于vant4+vue3 支持多选、单选、节点搜索 的相关文章

随机推荐

  • idea忽略.iml文件

    1 点击file文件下的设置中 2 点下file types 文件类型 进入到file types窗口 如图 然后点击忽略文件那添加需要忽略的类型
  • 自用HTML+CSS学习笔记

    HTML CSS学习笔记 1 Web标准 Web标准也称为网页标准 由一系列的标准组成 大部分由W3C World Wide Web Consortium 万维网联盟 负责制定 由三个组成部分 HTML 负责网页的结构 页面元素和内容 CS
  • IT的教育

    IT的教育 李颜芯 CSDN的网友大家好 欢迎大家收看这一起的CSDN视频访谈节目 今天我们请到了两位嘉宾 一位是 金旭亮 老师 一位是 金戈 老师 两位老师作一下自我介绍怎么样 金旭亮 我先介绍一下吧 我叫金旭亮是北京理工大学的讲师 我在
  • 怎样把pdf转换成word-多语言ocr支持

    http jingyan baidu com article 86fae34699bb4e3c49121a23 html PDF格式良好的视觉阅读性和通用性使得PDF文件的使用越来越广泛了 网络上的PDF资料也越来越多 但是我们往往想要提出
  • 【大屏】 amap + echarts 踩坑以及避免办法

    amap echarts 踩坑以及避免办法 大屏 踩坑 代码 大屏 html body container margin 0 padding 0 width 5376px height 1944px background color 000
  • softmax用于分类问题/逻辑回归

    参考 d2l 线性回归问题最后输出一个参数用于预测 多分类问题最后输出多个维度的数据 多少个output channels就有多少个类别 softmax是一种激活函数 它常见于分类问题的最后一层激活函数 目的是让输出属于一个概率密度函数 我
  • AI「领悟」有理论解释了!谷歌:两种脑回路内部竞争,训练久了突然不再死记硬背...

    梦晨 发自 凹非寺量子位 公众号 QbitAI 谷歌PAIR团队不久前撰文介绍了AI的 领悟 Grokking 现象 训练久了突然不再死记硬背 而是学会举一反三 有了泛化能力 不出一个月 另一只团队 主要成员来自DeepMind 表示 已经
  • 说实话,其实Spring Security并没有看起来那么复杂(附源码)

    权限管理是每个项目必备的功能 只是各自要求的复杂程度不同 简单的项目可能一个 Filter 或 Interceptor 就解决了 复杂一点的就可能会引入安全框架 如 Shiro Spring Security 等 其中 Spring Sec
  • Android Studio使用技巧:添加Module依赖

    今天在学习使用Volley的时候 下载好了Volley的Module文件 成功import到了Android Studio 但是却不知道怎么在自己的项目 Module 里使用 经朋友指点才知道原来还有给自己的项目添加Module Depen
  • JVM反射的实现

    实现方式 有两种不同的实现方式 一种是本地实现 一种是动态实现 JVM开始运行之后 方法的代码和入口地址都能获取到 想要通过反射调用方法 无非就是找到方法地址 然后将参数传递进去执行 本地实现就是使用native方法直接调用方法 但是这种方
  • PS调节图片:拉伸、变形

    一 对图片进行变形处理 打开PS软件 选中需要处理的图片 ctrl J复制一层图层 点击编辑选项 在下拉菜单里找到变换 变形选项 即可对图片进行变形操作 注意变形的图片下边还有一层图形 那即是我们复制图层的效果 复制图层相当于在图片上面加了
  • 计算机网络基础知识总结及思维导图(六)应用层

    文章目录 六 应用层 6 1 域名系统DNS 6 1 1 基础概念 6 1 2 域名服务器 6 2 文件传送协议 6 2 1 介绍 6 2 2 FTP协议 6 2 3 简单文件传送协议TFTP 6 3 远程终端协议TELNET 6 4 万维
  • 哪个网站云服务器最便宜,哪家云服务器比较便宜

    哪家 云服务器 是一种简单高效 安全可靠 处理能力可弹性伸缩的计算服务 其管理方式比物理服务器更简单高效 用户无需提前购买硬件 即可迅速创建或释放任意多台云服务器 我们在选择云服务器时 多从CPU 内存和磁盘特性等方面来对比 在这些因素相差
  • webdriver在浏览器中显示版本不对的解决方法

    相信看到这的小伙伴已经安装好了selenium包了 pip3 install selenium 可能是运行的时候出现这样的错误 SessionNotCreatedException Message session not created T
  • k8s系列——部署k8s集群

    1 环境准备 1 1 安装操作系统 此处选用centos 7 操作系统进行安装操作 1 2 关闭防火墙 systemctl stop firewalld systemctl disable firewalld 1 3 关闭selinux s
  • java使用okhttp3实现gofastdfs上传

    1 maven
  • [数值计算-7]:一元n次非线性方程求解-单点盲探-牛顿迭代法&Python法代码示例

    作者主页 文火冰糖的硅基工坊 https blog csdn net HiWangWenBing 本文网址 https blog csdn net HiWangWenBing article details 119813740 目录 1 一
  • 【OPENCV_系列电子PDF图书连载】计算机视觉从入门到精通完整学习路线专栏

    OPENCV PDF图书连载之 图像的几何变换 一 图像几何变换 1 3 a 图像坐标仿射 仿射自定义代码展示 warpAffine pointsAffine 自定义包 from img pakage ocv import warpAffi
  • Raft算法的Java实现

    自己这几天在看Redis的Sentinel高可用解决方案 Sentinel选主使用的是著名的Raft一致性算法 本文对Raft的选主作了介绍 具体的算法内容 请参考 Raft 论文 Raft的整体结构 Raft 通过选举一个高贵的领导人 然
  • 移动端树形选择组件 -- 基于vant4+vue3 支持多选、单选、节点搜索

    效果图 多选 单选 父组件使用