Skip to content

树形结构功能

思路,采用递归组件, 子组件emit 给父组件改变半选,权限状态

本页面启用了 reactivityTransform: true

父组件

Demo.vue

vue
<script lang="ts" setup>
import { ref } from 'vue'
import Row from './Row.vue'
const isSelectAll = ref(false)
const isIndeterminate = ref(false)
const nodeTree = ref([])

function handleAllStatus() {
  isSelectAll.value = nodeTree.value.length && nodeTree.value.every(e => e.active)
  isIndeterminate.value
    = !(isSelectAll.value || nodeTree.value.every(e => !e.active))
    || nodeTree.value.some(e => e.indeterminate) // 子类有半选设置父级半选;
}
</script>

<template>
  <Row :items="nodeTree" @change="handleAllStatus" />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import Row from './Row.vue'
const isSelectAll = ref(false)
const isIndeterminate = ref(false)
const nodeTree = ref([])

function handleAllStatus() {
  isSelectAll.value = nodeTree.value.length && nodeTree.value.every(e => e.active)
  isIndeterminate.value
    = !(isSelectAll.value || nodeTree.value.every(e => !e.active))
    || nodeTree.value.some(e => e.indeterminate) // 子类有半选设置父级半选;
}
</script>

<template>
  <Row :items="nodeTree" @change="handleAllStatus" />
</template>

递归组件

Row.vue 文件

vue
<script setup lang="ts">
const { items = [] } = defineProps<{
  items: Record<string, string>[]
}>()

const emit = defineEmits<{
  (e: 'change', val: [boolean, boolean]): void
}>()
/**
 * 递归改变子类状态
 */
function recursion(nodeItem, nodeActive) {
  if (nodeItem.length === 0)
    return

  nodeItem.forEach((item) => {
    item.active = nodeActive
    item.indeterminate = false
    if (item.child && item.child.length)
      recursion(item.child, item.active)
  })
}

function handleChange(val, node) {
  const [active, isAllChild] = Array.isArray(val) ? val : [val]

  if (node) {
    handleSetStatus(node)

    if (!isAllChild && node.child && node.child.length) {
      node.active = active
      node.indeterminate = false
      recursion(node.child, active)
      emit('change', [active, true])
      return
    }
  }
  emit('change', [active, true])
}
/**
 * 修改状态
 */
function handleSetStatus(node) {
  node.active = node.child.every(e => e.active)
  node.indeterminate
    = !(node.active || node.child.every(e => !e.active))
    || node.child.some(e => e.indeterminate) // 子类有半选设置父级半选
  // vue2 使用
  // this.$forceUpdate()
}
</script>

<template>
  <div class="node-wrap">
    <template v-for="nodeItem in items">
      <div
        v-if="nodeItem.child && nodeItem.child.length"
        :key="nodeItem.node_id"
        class="node-row"
      >
        <div class="node-main">
          <el-checkbox
            v-model="nodeItem.active"
            :indeterminate="nodeItem.indeterminate"
            @change="handleChange($event, nodeItem)"
          >
            {{ nodeItem.node_name }}
          </el-checkbox>
        </div>
        <row
          class="node-child"
          :items="nodeItem.child"
          @change="handleChange($event, nodeItem)"
        />
      </div>
      <div v-else :key="nodeItem.node_id" class="node-item">
        <el-checkbox
          v-model="nodeItem.active"
          @change="handleChange($event)"
        >
          {{ nodeItem.node_name }}
        </el-checkbox>
      </div>
    </template>
  </div>
</template>

<style lang="less" scoped>
.node-wrap :not(.node-row).node-child {
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  height: 100%;
}
.node-row {
  width: 100%;
  display: flex;
  align-items: center;
  border-top: 1px solid #ebeef5;
  .node-row:first-of-type {
    border-top: none;
  }
}
.node-main {
  flex: 1;
  min-width: 130px;
  height: 100%;
  display: flex;
  justify-content: left;
  padding: 6px 20px 6px 20px;

  line-height: 50px;

  & + .node-child {
    display: flex;
    flex: 9;
    flex-direction: column;
    border-left: 1px solid #ebeef5;
  }
}
.node-item {
  display: inline-block;
  padding: 6px 20px 6px 20px;
  line-height: 50px;
}
</style>
<script setup lang="ts">
const { items = [] } = defineProps<{
  items: Record<string, string>[]
}>()

const emit = defineEmits<{
  (e: 'change', val: [boolean, boolean]): void
}>()
/**
 * 递归改变子类状态
 */
function recursion(nodeItem, nodeActive) {
  if (nodeItem.length === 0)
    return

  nodeItem.forEach((item) => {
    item.active = nodeActive
    item.indeterminate = false
    if (item.child && item.child.length)
      recursion(item.child, item.active)
  })
}

function handleChange(val, node) {
  const [active, isAllChild] = Array.isArray(val) ? val : [val]

  if (node) {
    handleSetStatus(node)

    if (!isAllChild && node.child && node.child.length) {
      node.active = active
      node.indeterminate = false
      recursion(node.child, active)
      emit('change', [active, true])
      return
    }
  }
  emit('change', [active, true])
}
/**
 * 修改状态
 */
function handleSetStatus(node) {
  node.active = node.child.every(e => e.active)
  node.indeterminate
    = !(node.active || node.child.every(e => !e.active))
    || node.child.some(e => e.indeterminate) // 子类有半选设置父级半选
  // vue2 使用
  // this.$forceUpdate()
}
</script>

<template>
  <div class="node-wrap">
    <template v-for="nodeItem in items">
      <div
        v-if="nodeItem.child && nodeItem.child.length"
        :key="nodeItem.node_id"
        class="node-row"
      >
        <div class="node-main">
          <el-checkbox
            v-model="nodeItem.active"
            :indeterminate="nodeItem.indeterminate"
            @change="handleChange($event, nodeItem)"
          >
            {{ nodeItem.node_name }}
          </el-checkbox>
        </div>
        <row
          class="node-child"
          :items="nodeItem.child"
          @change="handleChange($event, nodeItem)"
        />
      </div>
      <div v-else :key="nodeItem.node_id" class="node-item">
        <el-checkbox
          v-model="nodeItem.active"
          @change="handleChange($event)"
        >
          {{ nodeItem.node_name }}
        </el-checkbox>
      </div>
    </template>
  </div>
</template>

<style lang="less" scoped>
.node-wrap :not(.node-row).node-child {
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  height: 100%;
}
.node-row {
  width: 100%;
  display: flex;
  align-items: center;
  border-top: 1px solid #ebeef5;
  .node-row:first-of-type {
    border-top: none;
  }
}
.node-main {
  flex: 1;
  min-width: 130px;
  height: 100%;
  display: flex;
  justify-content: left;
  padding: 6px 20px 6px 20px;

  line-height: 50px;

  & + .node-child {
    display: flex;
    flex: 9;
    flex-direction: column;
    border-left: 1px solid #ebeef5;
  }
}
.node-item {
  display: inline-block;
  padding: 6px 20px 6px 20px;
  line-height: 50px;
}
</style>