el-tree+手写穿梭框

51 阅读3分钟

image.png

<script setup>
import {ref, reactive, getCurrentInstance, watch} from 'vue'
import {ElLoading, ElMessage} from 'element-plus'
import {addOrgUser, listDeptTree, listOrgUser} from "../../api/gljg";

// 初始化响应式数据
const visible = ref(false)
const title = ref('')
const formRef = ref(null)
const leftTreeRef = ref(null)
const rightTreeRef = ref(null)

// 树形数据
const leftTreeData = ref([])
const rightTreeData = ref([])
const allUsers = ref([]) // 存储所有用户数据

// 初始化表单数据
const form = ref({
  orgId: null,
  userList: [],
})

// 搜索文本
const leftFilterText = ref('')
const rightFilterText = ref('')

// 表单校验规则
const rules = reactive({})

// 字典相关
const {proxy} = getCurrentInstance();

// 提交后刷新主表
const emit = defineEmits(['success'])


// 修改左侧树的props配置,添加图标支持
const treeProps = {
  label: 'label',
  children: 'children',
  disabled:'disabled'
}


// 处理树形数据,添加图标
function processTreeData(data) {
  return data.map(item => {
    const processedItem = {
      realId: item.realId,
      label: item.label,
      treeType: item.treeType,
      children: item.children&&item.children.length>0 ? processTreeData(item.children) : undefined
    }

    // 为treeType为'1'的节点添加文件夹图标
    if (item.treeType === '1') {
      processedItem.label = `📁 ${item.label}`
    }

    return processedItem
  })
}


// 树节点过滤
function filterNode(value, data) {
  if (!value) return true
  return data.label.toLowerCase().includes(value.toLowerCase())
}

// 监听搜索框变化
watch(leftFilterText, (val) => {
  leftTreeRef.value?.filter(val)
})

watch(rightFilterText, (val) => {
  rightTreeRef.value?.filter(val)
})

// 处理树形结构和图标
async function getList() {
  try {
    leftTreeData.value = []
    allUsers.value = []
    const res = await listDeptTree()
    if (res.code === 200) {
      // 处理树形结构数据并添加图标
      allUsers.value = processTreeData(res.data)
      //获取已选用户
      listHasUser()
    }
  } catch (error) {
    proxy.$modal.msgError("获取列表失败:" + error.message)
  }
}


// 获取已选用户
async function listHasUser() {
  try {
    form.value.userList = []
    let query = {
      orgId: form.value.orgId
    }
    const res = await listOrgUser(query)
    if (res.code === 200) {
      form.value.userList = res.data.map(item => item.realId)
      // 更新左右侧树数据
      updateLeftTreeData()
      updateRightTreeData()
    }
  } catch (error) {
    proxy.$modal.msgError("获取列表失败:" + error.message)
  }
}



// 修改从左侧添加到右侧的方法
const addSelectedUsers = () => {
  const checkedNodes = leftTreeRef.value.getCheckedNodes(false, false) // 获取所有选中节点

  const allCheckedKeys = []

  // 处理完全选中的节点
  checkedNodes.forEach(node => {
    if (node.treeType !== '1') {
      // 用户节点直接添加
      allCheckedKeys.push(node.realId)
    } else {
      // 文件夹节点需要添加其下所有用户
      collectUserIds(node, allCheckedKeys)
    }
  })

  if (allCheckedKeys.length === 0) {
    ElMessage.warning('请先选择要分配的人员')
    return
  }

  // 合并并去重
  const newUserList = [...new Set([...form.value.userList, ...allCheckedKeys])]
  form.value.userList = newUserList

  // 清除左侧选中状态
  leftTreeRef.value.setCheckedKeys([])

  // 更新左右侧树数据
  updateLeftTreeData()
  updateRightTreeData()
}

// 收集节点下的所有用户ID
function collectUserIds(node, realIds) {
  if (node.treeType !== '1') {
    realIds.push(node.realId)
  }
  if (node.children) {
    node.children.forEach(child => collectUserIds(child, realIds))
  }
}

// 修改更新左侧树数据的方法(未分配用户)
function updateLeftTreeData() {
  // 保持完整树结构,只禁用已分配的节点
  leftTreeData.value = filterTreeData(JSON.parse(JSON.stringify(allUsers.value)))
}

// 递归过滤树数据,禁用已分配节点而不是移除
function filterTreeData(nodes) {
  return nodes.map(node => {
    // 如果是用户节点且已分配,则禁用
    if (node.treeType !== '1' && form.value.userList.includes(node.realId)) {
      node.disabled = true
    }
    // 递归处理子节点
    if (node.children) {
      node.children = filterTreeData(node.children)
    }
    return node
  })
}

// 更新右侧树数据(已分配用户)
function updateRightTreeData() {
  // 构建右侧树数据,只包含已分配的用户
  rightTreeData.value = buildAssignedTree(JSON.parse(JSON.stringify(allUsers.value)))
}

// 构建已分配用户的树结构
function buildAssignedTree(nodes) {
  const result = []
  nodes.forEach(node => {
    // 如果是已分配的用户节点
    if (node.treeType !== '1' && form.value.userList.includes(node.realId)) {
      result.push({...node})
    }
    // 如果有子节点,递归处理
    else if (node.children) {
      const assignedChildren = buildAssignedTree(node.children)
      // 如果子节点中有已分配的用户
      if (assignedChildren.length > 0) {
        const newNode = {...node,children: assignedChildren}
        result.push(newNode)
      }
    }
  })
  return result
}

// 从右侧移除到左侧
const removeSelectedUsers = () => {
  const checkedNodes = rightTreeRef.value.getCheckedNodes(false, false) // 获取所有选中节点

  const checkedKeys = []

  // 收集要移除的用户ID
  checkedNodes.forEach(node => {
    if (node.treeType !== '1') {
      // 用户节点直接移除
      checkedKeys.push(node.realId)
    } else {
      // 文件夹节点需要移除其下所有用户
      collectUserIds(node, checkedKeys)
    }
  })

  if (checkedKeys.length === 0) {
    ElMessage.warning('请先选择要移除的人员')
    return
  }

  // 从userList中移除选中的用户
  form.value.userList = form.value.userList.filter(realId => !checkedKeys.includes(realId))

  // 清除右侧选中状态
  rightTreeRef.value.setCheckedKeys([])

  // 更新左右侧树数据
  updateLeftTreeData()
  updateRightTreeData()
}

// 移除所有用户
const removeAllUsers = () => {
  if (form.value.userList.length === 0) {
    ElMessage.warning('暂无已分配人员')
    return
  }

  form.value.userList = []

  // 更新左右侧树数据
  updateLeftTreeData()
  updateRightTreeData()
}


// 修改显示对话框方法
const show = (orgId) => {
  reset()
  visible.value = true
  title.value = "修改人员权限"
  form.value.orgId = orgId

  try {
    //获取列表所有数据
    getList()
  } catch (error) {
    proxy.$modal.msgError("获取详情失败:" + error.message)
  }
}

// 关闭对话框
const cancel = () => {
  visible.value = false
  reset()
}

// 重置表单
const reset = () => {
  form.value = {
    orgId: null,
    userList: [],
  }
  leftFilterText.value = ''
  rightFilterText.value = ''
  leftTreeData.value = []
  rightTreeData.value = []
  allUsers.value = []
  // Reset form validation
  formRef.value?.resetFields()
}

// 提交表单
const submitForm = async () => {
  if (!formRef.value) return
  const loading = ElLoading.service({
    lock: true,
    text: '提交中...',
    background: 'rgba(0, 0, 0, 0.7)',
  })
  try {
    let validRes = await formRef.value.validate()
    if (validRes) {
      if (form.value.orgId != null) {
        let res = await addOrgUser(form.value)
        if (res.code === 200) {
          ElMessage({
            message: res.msg,
            type: 'success'
          })
          emit('getList')
          visible.value = false
        } else {
          ElMessage({
            message: res.msg,
            type: 'error'
          })
        }
      }
    }
    loading.close()
  } catch (error) {
    console.error('提交表单失败:', error)
    loading.close()
  }
}

// 暴露方法给父组件使用
defineExpose({
  show
})
</script>

<template>
  <div>
    <el-dialog
        :title="title"
        v-model="visible"
        width="1200px"
        append-to-body
        custom-class="industry-code-dialog"
    >
      <el-form ref="formRef" :model="form" :rules="rules">
        <div class="transfer-container">
          <!-- 左侧树形选择面板 -->
          <div class="left-panel">
            <div class="panel-header">
              <span>系统人员</span>
              <el-input
                  v-model="leftFilterText"
                  placeholder="搜索人员"
                  clearable
                  size="small"
                  style="width: 200px; margin-left: 10px;"
              />
            </div>
            <!-- 左侧树 -->
            <el-tree ref="leftTreeRef" :data="leftTreeData" node-key="realId" show-checkbox :default-checked-keys="form.userList" :default-expanded-keys="form.userList"
                     :props="treeProps" :filter-node-method="filterNode" class="tree-container"/>
          </div>

          <!-- 中间操作按钮 -->
          <div class="center-buttons">
            <el-button type="primary" icon="el-icon-arrow-right" @click="addSelectedUsers">分配 ></el-button>
            <el-button type="primary" icon="el-icon-arrow-left" @click="removeSelectedUsers">< 移除</el-button>
            <el-button type="danger" icon="el-icon-delete" @click="removeAllUsers">全部移除</el-button>
          </div>

          <!-- 右侧已选面板 -->
          <div class="right-panel">
            <div class="panel-header">
              <span>已选人员 ({{ form.userList.length }})</span>
              <el-input
                  v-model="rightFilterText"
                  placeholder="搜索人员"
                  clearable
                  size="small"
                  style="width: 200px; margin-left: 10px;"
              />
            </div>
            <!-- 右侧树 -->
            <el-tree ref="rightTreeRef" :data="rightTreeData" node-key="realId" show-checkbox :default-expanded-keys="form.userList"
                     :props="treeProps" :filter-node-method="filterNode" class="tree-container"/>
          </div>
        </div>
      </el-form>

      <template #footer>
        <div class="dialog-footer">
          <el-button @click="cancel">取 消</el-button>
          <el-button type="primary" @click="submitForm">确 定</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>

<style scoped>
.industry-code-dialog :deep(.el-dialog__body) {
  box-sizing: border-box;
  padding: 20px 30px;
  height: 100%;
  overflow-y: auto;
}

.transfer-container {
  display: flex;
  height: 500px;
  border: 1px solid #ebeef5;
  border-radius: 4px;
  overflow: hidden;
}

.left-panel, .right-panel {
  flex: 1;
  display: flex;
  flex-direction: column;
  border-right: 1px solid #ebeef5;
}

.right-panel {
  border-right: none;
}

.panel-header {
  padding: 12px 15px;
  background-color: #f5f7fa;
  border-bottom: 1px solid #ebeef5;
  font-weight: bold;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.tree-container {
  flex: 1;
  padding: 10px;
  overflow: auto;
}

.center-buttons {
  width: 100px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background-color: #fafafa;
  padding: 0 10px;
}

.center-buttons .el-button {
  margin: 10px 0;
  width: 100%;
}

.selected-container {
  flex: 1;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.empty-tip {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #999;
  font-size: 14px;
}

.scroll-container {
  flex: 1;
  padding: 10px;
  height: 0; /* 使滚动区域自适应 */
}

.selected-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px 12px;
  margin-bottom: 8px;
  background-color: #f5f7fa;
  border-radius: 4px;
  transition: all 0.3s;
}

.selected-item:hover {
  background-color: #ecf5ff;
  transform: translateY(-1px);
}

.item-label {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.remove-btn {
  padding: 0;
  color: #f56c6c;
  opacity: 0.7;
}

.remove-btn:hover {
  opacity: 1;
}

.dialog-footer {
  text-align: right;
  margin-top: 20px;
}

.dialog-footer .el-button {
  min-width: 100px;
  padding: 10px 20px;
}

.dialog-footer .el-button + .el-button {
  margin-left: 12px;
}

.dialog-footer .el-button:hover {
  opacity: 0.9;
  transform: translateY(-1px);
  transition: all 0.3s ease;
}
</style>