效果图
多选
单选
父组件使用
<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>