利用AntDesign中a-tree和checkbox构造组织单位人员树选择组件

发布时间 2023-04-15 17:38:59作者: Felix_Openmind

业务效果图

核心代码

<template>
  <div class="select-container">


    <a-modal v-model:visible="visible" @ok="handleOk" @cancel="handleCancel" width="1500px">
      <template #title>
        <div style="font-weight: 600; font-size: 16px">
          选择任务协作者
        </div>
      </template>
      <div class="tree-container" ref="treeContainerNodeRef">
        <div class="tree-section01">
          <div class="section01-left">
            <a-input-search v-model:value="searchValue" style="margin-bottom: 8px" placeholder="搜索"
                            @search="onSearch"
            />
            <div class="tree-title">选择组织</div>
            <a-tree
                :tree-data="treeData"
                v-model:expandedKeys="expandedKeys"
                v-model:selectedKeys="selectedKeys"
                v-model:checkedKeys="checkedKeys"
                @select="selectOrgTreeNode"

            >
            </a-tree>
          </div>
          <div class="section01-right">
            <a-divider
                type="vertical"
                style="height: 100%;"
            />
          </div>
        </div>
        <div class="tree-section02" ref="treeSection02ScrollRef">
          <div class="section-left">
            <div class="tree-title">选择人员</div>
            <a-checkbox-group
                v-model:value="selectedUserData"
                name="checkboxgroup"
                :options="userDataOptions"
                @change="checkUserData"
            />
          </div>
          <div class="section-right">
            <a-divider
                type="vertical"
                style="height: 100%;"
            />
          </div>
        </div>
        <div class="tree-section03">
          <div class="tag-container">
            <div
                style="font-weight: 600;height: 22px;line-height: 22px; position: relative; top: 3px; margin-bottom: 15px;">
              已选人员
            </div>
            <div class="tag-content">
              <a-tag closable class="tag-item"
                     @close="closeTag(tag)"
                     v-for="(tag) in tags" :key="nanoid()">
                {{ tag.label }}
              </a-tag>
            </div>
          </div>
        </div>
      </div>
    </a-modal>
  </div>
</template>

<script setup>
import {onMounted, ref, defineExpose, defineEmits, onBeforeUnmount, toRaw, watch, nextTick} from 'vue';
import {useStore} from 'vuex'
import {nanoid} from 'nanoid'
import mitt from '@/utils/mitt'
import {getOfficeTree, getUserTree} from "@/api/user";
import lodash from 'lodash'

const emit = defineEmits();
const store = useStore()
const visible = ref(false);
// 组织树数据源
const treeData = ref([]);
// 用户树数据源
const userData = ref([]);
// 所有用户数据
const globalUserDataOptions = ref([]);
const userDataOptions = ref([]);
// checkBoxUserData;
const selectedUserData = ref([]);
// 缓存数据
const cacheUserOptions = ref([]);
const curTreeData = ref([]);
const expandedKeys = ref([]);
// 全局selectedKeys
const globalUserData = ref([]);
const selectedKeys = ref([]);
const checkedKeys = ref([]);
const tags = ref([])
const treeSection02ScrollRef = ref(null);
const treeContainerNodeRef = ref(null);
const executeFlag = ref(false);

onMounted(() => {
  mitt.on('selectedUserDataCode', (data) => {
    selectedUserData.value = data;
    globalUserData.value = data;
    // 从缓存中搜寻 start
    let cacheUserOptionsData = JSON.parse(localStorage.getItem('cacheGlobalUserDataOptions'));
    if (cacheUserOptionsData !== null) {
      nextTick(() => {
        tags.value = cacheUserOptionsData.filter(v => globalUserData.value.some(val => val === v.value))
      })
    }
    // 从缓存中搜索 end
  })
  mitt.on('deselectTag', (deselectTagValue) => {
    tags.value = tags.value.filter(item => item.value !== deselectTagValue);
  })
  queryData();
})
// 滚动到容器顶部
const scrollToEditflag = (domNode, height) => {
  for (let i = 0; i < height + 1000; i++) {
    setTimeout(() => {
      domNode.value.scrollTo(0, i);
    }, 100);
  }
};

// 组织架构树搜索
const searchValue = ref('');
const onSearch = (searchInputValue) => {
  if ('' === searchInputValue) {
    treeData.value = curTreeData.value;
    return;
  }
  treeData.value = mapTreeData(searchInputValue, curTreeData.value)
};

const mapTreeData = (value, arr) => {
  const newarr = [];
  const keys = [];
  arr.forEach((element) => {
    if (element.title.indexOf(value) > -1) {
      newarr.push(element)
      keys.push(element.key)
    } else {
      // 遍历子层
      if (element.children && element.children.length > 0) {
        const redata = mapTreeData(value, element.children);
        if (redata && redata.length > 0) {
          const obj = {
            ...element,
            children: redata
          };
          keys.push(obj.key)
          newarr.push(obj);
        }
      }
    }
  })
  expandedKeys.value = keys;
  return newarr
}


// 点击左侧组织架构树根据组织code获取所属用户
const selectOrgTreeNode = async (selectedKeys) => {
  // 重新得到selectUserData
  executeFlag.value = true;
  let officeCode = selectedKeys[0]
  let res = await getUserTree({officeCode: officeCode});
  if (200 === res.status) {
    userDataOptions.value = [];
    userData.value = res.data.data;
    userData.value.forEach((user) => {
      let option = {
        label: user.userName,
        value: user.userCode,
      }
      userDataOptions.value.push(option)
      globalUserDataOptions.value.push(option);
    })
    // selectUserData 是根据当前userDataOptions过滤globalUserData得到的
    selectedUserData.value = userDataOptions.value.filter(v => globalUserData.value.some(val => val === v.value)).map(item => item.value);
    globalUserDataOptions.value = lodash.unionBy(globalUserDataOptions.value, "value");
    //  start 更新缓存 cacheGlobalUserDataOptions
    let cacheGlobalUserDataOptions = JSON.parse(localStorage.getItem('cacheGlobalUserDataOptions'));
    if (cacheGlobalUserDataOptions !== null) {
      let cacheData = cacheGlobalUserDataOptions;
      // 更新缓存数据并去重
      cacheData = [...cacheData, ...globalUserDataOptions.value];
      cacheData = lodash.unionBy(cacheData, "value");
      localStorage.setItem('cacheGlobalUserDataOptions', JSON.stringify(cacheData));
    } else {
      localStorage.setItem('cacheGlobalUserDataOptions', JSON.stringify(globalUserDataOptions.value));
    }
    //  end 更新缓存
  }
}

// closeTag
const closeTag = (tag) => {
  selectedUserData.value = selectedUserData.value.filter((key) => (key !== tag.value));
}

// 左侧组织架构树
const queryData = async () => {
  let officeUserData = [];
  if (localStorage.getItem('officeUserData') === null) {
    let res = await getOfficeTree();
    if (res.status === 200) {
      officeUserData = res.data.data;
    }
    localStorage.setItem('officeUserData', JSON.stringify(officeUserData));
  } else {
    officeUserData = JSON.parse(localStorage.getItem('officeUserData'));
  }
  recursive(officeUserData);
}

// 根据数据源构建架构树
const recursive = (newList) => {
  newList.map((item => {
    item.title = item.officeName;
    item.key = item.officeCode;
    if (item.children !== undefined) {
      recursive(item.children)
    }
  }))
  treeData.value = newList;
  curTreeData.value = newList;
};

const searchOfficeInfo = (userCode, treeDataParam) => {
  for (let i = 0; i < treeDataParam.length; i++) {
    if (treeDataParam[i].userCode === userCode) {
      return {
        label: treeDataParam[i].userName,
        value: treeDataParam[i].userCode,
      };
    } else {
      let res = searchOfficeInfo(officeCode, treeDataParam[i].userVos);
      if (res != null) {
        return res;
      }
    }
  }
}

const checkUserData = (checkedValue) => {
}

watch(() => selectedUserData.value, (newVal, oldVal) => {
  // 会记录之前的selectedUserData

  let diff = newVal.concat(oldVal)
      .filter(v => !newVal.includes(v) || !oldVal.includes(v));
  let resGlobalUserData = []
  if(newVal.length === 0 && oldVal.length === 0)  {
    return;
  }
  if (newVal.length > oldVal.length) {
    // 新增
    resGlobalUserData = [...new Set([...globalUserData.value, ...diff])];
  }
  if (newVal.length < oldVal.length) {
    if(executeFlag.value) {
      executeFlag.value = false;
      let cacheData = JSON.parse(localStorage.getItem("cacheGlobalUserDataOptions"));
      tags.value = cacheData.filter(v => globalUserData.value.some(val => val === v.value))
      return;
    }
    resGlobalUserData = globalUserData.value.filter(item => item !== diff[0]);
  }
  globalUserData.value = resGlobalUserData;
  // 缓存
  let cacheData = JSON.parse(localStorage.getItem("cacheGlobalUserDataOptions"));
  tags.value = cacheData.filter(v => globalUserData.value.some(val => val === v.value))
}, {deep: true})
// 显示弹窗
const showModal = () => {
  visible.value = true;
};
const handleCancel = (e) => {
  visible.value = false;
}
const handleOk = (e) => {
  visible.value = false;
  emit('tagNodeData', tags.value);
};

onBeforeUnmount(() => {
  mitt.off();
})
defineExpose({
  showModal
});

</script>
<style lang="scss" scoped>

.tree-container {
  display: flex;
  flex-direction: row;
  min-height: 400px;
  max-height: 650px;

  .tree-section01 {
    overflow-y: scroll;
    margin-right: 20px;
    display: flex;
  }

  .tree-section02 {
    display: flex;
    overflow-y: scroll;
    margin-right: 20px;
    padding-top: 35px;
    flex: 2;

    & :deep(.ant-checkbox-group) {
      display: flex !important;
      flex-direction: column !important;
    }
  }

  .tree-section03 {
    padding-top: 30px;
    flex: 3;
  }

  .section-left {
    width: 100%;
  }

  .tree-title {
    font-size: 14px;
    height: 22px;
    line-height: 22px;
    font-weight: 600;
    color: rgba(21, 22, 24, 0.92);
  }

  .tag-container {
    .tag-content {
      width: 100%;
      display: flex;
      flex-wrap: wrap;

      .tag-title {
        font-size: 14px;
        height: 22px;
        line-height: 22px;
        font-weight: 600;
        color: red;
      }

      .tag-item {
        margin: 2px 2px;
      }
    }
  }
}


</style>