最近写h5要做那种稍微复杂一点的树,没找到现成的UI组件库可用,vant的树只有两级不满足,只能自己写
ps. 选框的选择/反选/半选对父子选项的影响还有bug,留到脑子好的时候再优化
效果
代码
框架是Vue3+Vite+Vant4。复选框用的vant的checkbox,应该也可以换别的或者原生。
模板
<template>
<div class="top-content">
<div class="top-tree">
<div class="top-tree-panel" v-for="(active, level) in treeLevelIndex" :key="level">
<ul>
<li :class="['top-tree-item', index === active ? 'active' : '', item.checked ? 'checked' : '', item.indeterminate ? 'indeterminate' : '']"
v-for="(item,index) in treePanel[level]" :key="index"
@click="($event) => expandChildren(item, index, level, $event)">
<span>{{ item.dataName }}</span>
<van-checkbox v-model="item.checked" shape="square" @change="(checked) => itemCheckChange(checked, item, index, level)"></van-checkbox>
</li>
</ul>
</div>
</div>
<div class="top-selected">
<div class="top-selected-total">已选({{ checkedItems.length }})</div>
<div class="top-selected-list">
<div class="top-selected-item" v-for="(item,index) in checkedItems" :key="index">
<span>{{ item.dataName }}</span>
<van-icon name="cross" @click="removeItem(item)" />
</div>
</div>
</div>
<div class="top-button">
<div class="btn btn-reset" @click="reset">重置</div>
<div class="btn btn-submit" @click="submit">查看</div>
</div>
</div>
</template>
script(setup)
<script setup>
// props.data 是示例数据
const props = defineProps({
data: {
type: Array,
required: true,
default: function(){
return [
{
id: "1",
dataName: "数据1",
children: [
{
id: "1-1",
dataName: "二级1",
parentId: "1",
root: {
id: "1",
dataName: "数据1",
}
},
{
id: "1-2",
dataName: "二级2",
parentId: "1",
root: {
id: "1",
dataName: "数据1",
}
},
]
},
{
id: "2",
dataName: "数据2",
},
{
id: "3",
dataName: "数据3",
children: [
{
id: "3-1",
dataName: "数据3二级1",
parentId: "3",
root: {
id: "3",
dataName: "数据3",
},
children: [
{
id: "3-1-1",
dataName: "数据3三级1",
parentId: "3-1",
root: {
id: "3",
dataName: "数据3",
},
},
{
id: "3-1-2",
dataName: "数据3三级2",
parentId: "3-1",
root: {
id: "3",
dataName: "数据3",
},
},
{
id: "3-1-3",
dataName: "数据3三级3",
parentId: "3-1",
root: {
id: "3",
dataName: "数据3",
},
},
{
id: "3-1-4",
dataName: "数据3三级4",
parentId: "3-1",
root: {
id: "3",
dataName: "数据3",
},
},
]
},
{
id: "3-2",
dataName: "数据3二级2",
parentId: "3",
root: {
id: "3",
dataName: "数据3",
},
},
]
}
];
}
}
})
// 选中的叶子数据项
const checkedItems = ref([]);
const treePanel = ref([]); // 当前显示的选项列表面板
const treeLevelIndex = ref([]); // 记录当前显示的列表面板在树中的索引
const handleTreePanel = () => {
treePanel.value = [];
if(props.data.length > 0) {
let tree = JSON.parse(JSON.stringify(props.data));
treePanel.value.push(tree);
// 1级以下递归初始化treePanel和treeLevelIndex
pushTreeIndex(tree);
}
}
const pushTreeIndex = (tree) => {
let nextTree = tree[0].children;
if(nextTree && nextTree.length > 0) {
treePanel.value.push(nextTree);
treeLevelIndex.value.push(0);
pushTreeIndex(nextTree);
}else{
treeLevelIndex.value.push(-1);
}
}
// 如果是固定的树数据,mounted执行一次即可
onMounted(()=>{
handleTreePanel();
})
// 如果是异步延迟获取的数据,可能需要用watch监听获取
watch(
() => {
return {...props.data};
},
(newVal, oldVal) => {
handleTreePanel();
},
{deep: true}
)
// 点击该项(而非复选框)展开子列表
const expandChildren = (item, index, level, event) => {
// 点击到图标(checkbox)阻止事件
let tar = event.target;
if(tar.tagName === "I" || tar.className.indexOf("van-icon") > -1) {
return;
}
let children = item.children || [];
treeLevelIndex.value[level] = index; // 当前level下当前点击的item变为active
if(treePanel.value[level + 1] !== undefined) { // 下一级已经有了
if(children.length > 0){ // 从有到有
treePanel.value[level + 1] = children;
treeLevelIndex.value[level + 1] = -1;
} else { // 从有到无,删到当前级别
treeLevelIndex.value.length = level + 1;
treePanel.value.length = level + 1;
}
}else { // 当前没有下一级
if(children.length > 0){ // 从无到有
treePanel.value.push(children);
treeLevelIndex.value.push(-1);
}else { // 从无到无
treePanel.value.length = level + 1;
treeLevelIndex.value.length = level + 1;
}
}
}
// 复选框点击事件
const itemCheckChange = (checked, item, index, level, auto = true) => {
if(checked === undefined) {
return; // 禁止子级面板切换时触发
}
if(item.children) {
// 不在treePanel里显示的子项,不会自动继续触发itemCheckChange事件 ;_;
const childrenVisible = treeLevelIndex.value[level] === index;
item.children.forEach((c, j) => {
c.checked = checked;
// 所以在这里做处理,如果当前没有显示,需要手动触发子项的itemCheckChange事件
if(!childrenVisible){
itemCheckChange(checked, c, j, level + 1, false)
}
})
}else{
onClickItem(item, checked); // 有可能真正要做业务逻辑的地方其实只在这里
// 向checkedItems中增加或删除当前项
let i = checkedItems.value.findIndex(child => child.id === item.id);
if(checked){
if(i === -1){
checkedItems.value.push(item);
}
}else {
if(i > -1){
checkedItems.value.splice(i,1);
}
}
}
// 子项全选/全不选时修改父项,这里发现有bug
if(level > 0 && auto){
let parentItem = treePanel.value[level - 1][treeLevelIndex.value[level - 1]];
let parentChildren = parentItem.children;
if(parentChildren.every(i => i.checked)) {
parentItem.checked = true;
parentItem.indeterminate = false;
} else if (parentChildren.every(i => !i.checked)) {
parentItem.checked = false;
parentItem.indeterminate = false;
} else if (parentChildren.some(i => !i.checked)) {
parentItem.indeterminate = true; // 半选
}
}
}
const onClickItem = (nodeData, checked) => {
// ...具体业务逻辑
}
// 已选列表中单个取消选中
const removeItem = (item) => {
item.checked = false;
let i = checkedItems.value.findIndex(child => child.id === item.id);
checkedItems.value.splice(i,1);
}
// 重置按钮,全部取消选中
const reset = () => {
checkedItems.value.forEach(item => {
item.checked = false;
})
checkedItems.value.length = 0;
}
</script>
样式(参考)
<style lang="scss" scoped>
.top-content {
.top-tree {
display: flex;
flex-direction: row;
width: 100%;
.top-tree-panel {
flex: 1;
padding: 30px 0;
border-right: 1px solid #314051;
&:last-child {
border-right: none;
}
.top-tree-item {
display: flex;
justify-content: space-between;
height: 80px;
line-height: 80px;
padding: 0 30px;
color: #fff;
font-size: 28px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
// 点击当前行
&.active {
background: #3A4B60;
}
// 选中
&.checked {
color: #37B1FF;
}
// 父级半选
&.indeterminate {
:deep(.van-badge__wrapper) {
color: var(--van-white);
background-color: var(--van-checkbox-checked-icon-color);
border-color: var(--van-checkbox-checked-icon-color);
&::before{
content: "\2013" !important; // 短横线
}
}
}
// 以下两项,解决选框从半选变为不选时,由于样式设置而短暂出现对勾的问题。(不确定使用其他checkbox组件是否会出现此问题)
:deep(.van-badge__wrapper)::before {
content: "";
}
&.checked {
:deep(.van-badge__wrapper)::before {
content: "\e728"; // 对勾
}
}
}
}
}
.top-selected {
display: flex;
font-size: 28px;
font-weight: 400;
padding: 30px;
align-items: center;
.top-selected-total {
flex-shrink: 0;
}
.top-selected-list {
display: flex;
flex-flow: row wrap;
.top-selected-item {
border-radius: 38px;
padding: 10px 20px;
margin-left: 10px;
margin-bottom: 10px;
}
}
}
}
</style>
标签:checked,item,level,value,H5,dataName,Vue3,id,Vite
From: https://www.cnblogs.com/cosmicbison/p/17318120.html