依据传入的树形结构渲染 并选择节点

根据产品经理提出的原型图做了一个根据传入的数据渲染选择节点image

思路

1.根据数据结构
{ id: '2', name: '店铺', children: [ { id: '2-1', name: '店铺管理', children: [...] } ] }
来进行渲染列表
3.父子关系管理

  • 使用 Map 存储节点间的父子关系
  • buildParentMap 方法初始化时建立映射
  • getParentIds 和 getChildrenIds 方法用于获取相关节点

3.选中逻辑
   3-1选中某个节点时:

- 获取所有子节点 ID
- 获取所有父节点 ID
- 将这些 ID 添加到选中列表

   3-2取消选中时:

移除当前节点及其子节点

4.组件通信

- 使用 v-model 进行数据双向绑定
- 通过自定义事件 parent-check 处理选中状态变化

5.样式设计

使用 flex 布局实现层级结构

- 不同层级使用不同的样式
- 最后一层使用横向排列

实现代码

父组件 PermissionSelect.vue

<template>
  <div class="permission-select">
    <div class="permission-header">
      <div>
        <span class="required">*</span>
        选择权限(仅适用于本店铺)
      </div>
      <div class="operation-btns">
        <el-button type="text" @click="handleCheckAll">全选</el-button>
        <el-button type="text" @click="handleClear">清空</el-button>
      </div>
    </div>

    <el-checkbox-group v-model="checkedPermissions" @change="handleCheckedChange">
      <div v-for="(item, index) in permissionData" :key="index" class="permission-group">
        <permission-level
          :item="item"
          :depth="1"
          v-model="checkedPermissions"
          @parent-check="handleParentCheck"
        />
      </div>
    </el-checkbox-group>
  </div>
</template>

<script>
import PermissionLevel from './PermissionLevel.vue'
export default {
  name: 'PermissionSelect',
    components: {
    PermissionLevel
  },
  data() {
    return {
      checkedPermissions: [],
      permissionData: [
        {
          id: '1',
          name: '概况'
        },
        {
          id: '2',
          name: '店铺',
          children: [
            {
              id: '2-1',
              name: '店铺管理',
              children: [
                {
                  id: '2-1-1',
                  name: '移动门店',
                  children: [
                    { id: '2-1-1-1', name: '查看' },
                    { id: '2-1-1-2', name: '编辑' },
                    { id: '2-1-1-3', name: '禁用' },
                    { id: '2-1-1-4', name: '启用' },
                    { id: '2-1-1-5', name: '推广' }
                  ]
                },
                {
                  id: '2-1-2',
                  name: '门店管理',
                  children: [
                    { id: '2-1-2-1', name: '查看' },
                    { id: '2-1-2-2', name: '新增' },
                    { id: '2-1-2-3', name: '编辑' },
                    { id: '2-1-2-4', name: '下架' },
                    { id: '2-1-2-5', name: '推广' },
                    { id: '2-1-2-6', name: '商品管理' },
                    { id: '2-1-2-7', name: '关联仓库' }
                  ]
                }
              ]
            },
            {
              id: '2-2',
              name: '店铺装修',
              children: [
                { id: '2-2-1', name: '自主装修' },
                { id: '2-2-2', name: '店铺主页' },
                { id: '2-2-3', name: '营养窗' }
              ]
            }
          ]
        },
        {
          id: '3',
          name: '商品',
          children: [
            {
              id: '3-1',
              name: '商品管理',
              children: [
                {
                  id: '3-1-1',
                  name: '商品管理',
                  children: [
                    { id: '3-1-1-1', name: '查看' },
                    { id: '3-1-1-2', name: '新增' },
                    { id: '3-1-1-3', name: '编辑' },
                    { id: '3-1-1-4', name: '上架' },
                    { id: '3-1-1-5', name: '下架' },
                    { id: '3-1-1-6', name: '删除' },
                    { id: '3-1-1-7', name: '改分类' }
                  ]
                }
              ]
            }
          ]
        }
      ],
      // 添加一个用于存储父子关系的映射
      parentMap: new Map()
    }
  },
  created() {
    // 初始化时建立父子关系映射
    this.buildParentMap(this.permissionData)
  },
  methods: {
    // 建立父子关系映射
    buildParentMap(data, parent = null) {
      data.forEach(item => {
        if (parent) {
          this.parentMap.set(item.id, parent)
        }
        if (item.children) {
          this.buildParentMap(item.children, item)
        }
      })
    },
    // 获取所有父节点ID
    getParentIds(id) {
      const parentIds = []
      let currentParent = this.parentMap.get(id)
      while (currentParent) {
        parentIds.push(currentParent.id)
        currentParent = this.parentMap.get(currentParent.id)
      }
      return parentIds
    },
    // 获取所有子节点ID
    getChildrenIds(item) {
      let ids = []
      if (item.children) {
        item.children.forEach(child => {
          ids.push(child.id)
          ids = ids.concat(this.getChildrenIds(child))
        })
      }
      return ids
    },
    // 处理节点选中
    handleParentCheck(checked, item) {
      const updateIds = new Set()
      
      // 获取所有子节点ID
      const childrenIds = this.getChildrenIds(item)
      childrenIds.forEach(id => updateIds.add(id))
      
      // 获取所有父节点ID
      const parentIds = this.getParentIds(item.id)
      parentIds.forEach(id => updateIds.add(id))
      
      // 添加当前节点ID
      updateIds.add(item.id)

      if (checked) {
        // 选中时,将所有相关节点添加到选中列表
        this.checkedPermissions = [...new Set([...this.checkedPermissions, ...updateIds])]
      } else {
        // 取消选中时,移除所有子节点
        this.checkedPermissions = this.checkedPermissions.filter(id => !childrenIds.includes(id) && id !== item.id)
      }
    },
    // 处理选中变化
    handleCheckedChange(value) {
      this.$emit('change', value)
    },
    // 获取所有权限ID
    getAllPermissionIds(data = this.permissionData) {
      let ids = []
      data.forEach(item => {
        ids.push(item.id)
        if (item.children) {
          ids = ids.concat(this.getAllPermissionIds(item.children))
        }
      })
      return ids
    },
    // 全选
    handleCheckAll() {
      this.checkedPermissions = this.getAllPermissionIds()
    },
    // 清空
    handleClear() {
      this.checkedPermissions = []
    }
  }
}
</script>

<style lang="scss" scoped>
.permission-select {
  .permission-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 10px;
    
    .required {
      color: #F56C6C;
      margin-right: 4px;
    }
    
    .operation-btns {
      .el-button {
        padding: 0;
        margin-left: 15px;
        color: #409EFF;
        font-size: 12px;
      }
    }
  }

  .el-checkbox-group {
    border: 1px solid #EBEEF5;
    border-radius: 2px;
  }

  .permission-group {
    display: flex;
    border: 1px solid #EBEEF5;
    margin-bottom: 10px;
    
    // 第一级复选框容器
    > .el-checkbox {
      width: 80px;
      flex-shrink: 0;
      padding: 10px;
      border-right: 1px solid #EBEEF5;
    }

    .permission-sub-group {
      flex: 1;
      
      .sub-item {
        display: flex;
        border-bottom: 1px solid #EBEEF5;
        
        &:last-child {
          border-bottom: none;
        }
        
        // 第二级复选框容器
        > .el-checkbox {
          width: 100px;
          flex-shrink: 0;
          padding: 10px;
          border-right: 1px solid #EBEEF5;
          background-color: #FAFAFA;
        }

        .permission-actions {
          flex: 1;
          display: flex;
          flex-wrap: wrap;
          gap: 10px;
          padding: 10px;
          background-color: #FAFAFA;
          
          // 第三级复选框
          .el-checkbox {
            margin-right: 15px;
          }
        }
      }
    }
  }

  ::v-deep .el-checkbox {
    margin-right: 0;
    
    .el-checkbox__input {
      margin-right: 6px;
      
      .el-checkbox__inner {
        border-radius: 2px;
      }
    }

    .el-checkbox__label {
      font-weight: normal;
      font-size: 13px;
      color: #606266;
    }

    &.is-checked {
      .el-checkbox__input {
        .el-checkbox__inner {
          background-color: #409EFF;
          border-color: #409EFF;
        }
      }
    }
  }

  // 最后一个组去掉底部边框
  .permission-group:last-child {
    margin-bottom: 0;
  }

  // 每个权限组之间的间隔
  .permission-group + .permission-group {
    margin-top: 10px;
  }
}
</style>

子组件 PermissionLevel.vue

<template>
  <div class="permission-level" :class="'level-' + depth">
    <el-checkbox 
      :label="item.id" 
      @change="(val) => handleParentCheck(val, item)"
    >
      {{ item.name }}
    </el-checkbox>
    <div v-if="item.children" class="children-group">
      <permission-level
        v-for="(child, index) in item.children"
        :key="index"
        :item="child"
        :depth="depth + 1"
        v-model="checkedPermissions"
        @parent-check="handleParentCheck"
      />
    </div>
  </div>
</template>

<script>
export default {
  name: 'PermissionLevel',
  props: {
    item: {
      type: Object,
      required: true
    },
    depth: {
      type: Number,
      default: 1
    },
    modelValue: {
      type: Array,
      default: () => []
    }
  },
  emits: ['update:modelValue', 'parent-check'],
  computed: {
    checkedPermissions: {
      get() {
        return this.modelValue
      },
      set(val) {
        this.$emit('update:modelValue', val)
      }
    }
  },
  methods: {
    handleParentCheck(checked, item) {
      this.$emit('parent-check', checked, item)
    }
  }
}
</script>

<style lang="scss" scoped>
.permission-level {
  display: flex;
  
  &.level-1 {
    > .el-checkbox {
      width: 80px;
      flex-shrink: 0;
      padding: 10px;
      border-right: 1px solid #EBEEF5;
    }
  }
  
  &.level-2, &.level-3{
    border-bottom: 1px solid #EBEEF5;
    
    &:last-child {
      border-bottom: none;
    }
    
    > .el-checkbox {
      width: 100px;
      flex-shrink: 0;
      padding: 10px;
      border-right: 1px solid #EBEEF5;
      background-color: #FAFAFA;
    }
  }
  
  .children-group {
    flex: 1;
    display: flex;
    flex-direction: column;
    
    // 最后一层的样式
    .permission-level:not(.level-1):not(.level-2) {
      .children-group {
        display: flex;
        flex-wrap: wrap;
        flex-direction: row;
        gap: 10px;
        padding: 10px;
        background-color: #FAFAFA;
        
        .el-checkbox {
          margin-right: 15px;
        }
      }
    }
  }
}
</style>
posted @ 2025-05-21 09:44  網友攃  阅读(31)  评论(0)    收藏  举报