首页 > 其他分享 >Vue3 Vite H5 手写一个横向展开的多级树列表

Vue3 Vite H5 手写一个横向展开的多级树列表

时间:2023-04-14 15:11:23浏览次数:50  
标签:checked item level value H5 dataName Vue3 id Vite

最近写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

相关文章

  • vue3微信公众号商城项目实战系列(3)项目初始文件及文件夹简介
    首先我们来看下项目的文件结构图,如下: 各个文件及文件夹作用如下:文件或文件夹名称作用.vscodeVisualStudioCode开发工具的配置信息存放目录,从这个目录可以看出vue3确实是推荐使用vscode作为开发工具的。node_modules项目中用到的包存放目录,当我们用"npminstall......
  • vue3 + ts + electron项目搭建过程
    1、输入指令npmcreateelectron-vite2、工程创建好后进入工程目录 执行npmi加载依赖项,加载过程中可能会出现加载失败的问题,是因为github资源的问题,非项目本身问题,多执行几次即可3、打包输入npmrunbuild,打包过程也会出现打包失败的问题,原因和上面一样,也是github资......
  • 前端H5使用html5QrCode实现扫一扫识别二维码
          Vue版本:npminstallhtml5-qrcode<template><divclass="index"><!--扫一扫--><divclass="scan"v-if="isScanning"><divclass="scan-box"><divid=&qu......
  • vue3微信公众号商城项目实战系列(1)开发环境准备
    项目忙完,这次上新,写一个前端系列,采用vue3来开发一个微信公众号商城。前言:1.微信公众号商城本质也是一个网站,由一个个网页组成,只不过这些网页运行在手机端,能响应手指的点击、长按、拖拽等操作。2.既然是网页,当然可以用3件套(js+html+css)来写,但象vue这样的前端框架比3件套更高效......
  • taro3.6.4 在H5 端使用 tabBar 出现bug --- taro Cannot read properties of undefi
    taro3.6.4在H5端使用tabBar出现bug解决办法就是换版本3.6.2好像没有这个问题已经有人反馈了,https://github.com/NervJS/taro/issues/13581taroCannotreadpropertiesofundefined(reading'list')......
  • 在Django+Vue3+GraphQL的Blog例子代码中引入Element-Plus UI Framework
    Vue3的UIFramework中有Element-Plus、BalmUI、Quasar、PrimeVue、AntDesignVue等UIFramework.Element-Plus是Element-UI的Vue3版,Element-UI的使用人数的基数较大,Github上的Star数也较多,就选择了Element-Plus作为这个Blog项目的UIFramework.UIFramework的好处就是提供了......
  • 用quasar+vue3+组合式api VueRouter实现路由嵌套(二级路由)
    前言:本项目使用的是quasar创建,vue3的组合式api语法。部分语法不同,但不影响理解,修改语法后可以在vue2/选项式api项目中运行。效果图:文件目录结构和代码如下:   文中用到的标题栏数据如下:consttitles=ref([{name:"首页",path:"home",children:[]},{......
  • h5使用高德获取用户当前位置信息
    在index.html文件中引入高德js文件:key需要从高德获取获取key<scripttype="text/javascript"src="https://webapi.amap.com/maps?v=1.4.15&key=f77da011880c2d55aeccba6446b85c78"></script>js文件我将方法写入了单独的js文件:locationService.jsimportAMapfr......
  • taro 3.0 官方模板运行报错 插件依赖 "@tarojs/plugin-platform-h5" 加载失败
    taroError:插件依赖"@tarojs/plugin-platform-h5"加载失败,请检查插件配置报错如下,原因:node版本的问题,使用nvm切换node版本就可以了......
  • 基于vue和vite的cesium、cesiumNavigation使用--(1)项目构建及引入
    ​系统信息windows10LTSC21H2vscode:1.77.1nodejs:18.15.0npm:9.5.0版本信息vue:3.2.47vite:4.1.4cesium:1.104.0vue-router:4.1.6默认在以上的js相关版本下构建环境,并包括创建vue项目,创建vue-router文件并构建路由,创建vue视图等等,以上内容不再赘述。如环境和版本不同,则项......