树形结构的勾选、取消勾选、删除、清空已选、回显、禁用

树形结构的勾选、取消勾选、删除、清空已选、回显、禁用

基本页面:

分为上传文件和编辑的页面

代码实现要点:

  • 上传文件页面:

    1. 点开选择范围弹窗,三个radio单选框都为可选状态,默认显示的是第一个单选框(按机构项目组)对应的内容

    2. 可以对树形数据进行勾选,勾选的数据展示在右边已选框内

    3. 可以对树形数据进行全选,取消全选

    4. 右边已选框内数据有对应的删除按钮,可以点击删除按钮或对已选数据进行删除

    5. 可以点击清空已选按钮对已选数据进行一次性全部删除

    6. 在左边树形数据取消勾选,右边已选框的数据也会对应发生变化

    7. 点击确定按钮但未确认上传时,再次打开弹框,页面中应把上一次选择的数据展示出来,且不能禁用,并同步展示在右边已选框内,同样可以对数据进行以上操作

    8. 点击确定并在上一层点击确认上传按钮,已经勾选的数据就会传给后端,同步新增到了页面中,后续可以对数据进行编辑操作来修改数据

  • 编辑页面:

    1. 点开选择范围弹窗,只有从后端返回的对应的单选框类型的单选框才能可选,其他的单选框都为禁用状态

    2. 树形数据显示的也是可选单选框对应的树形数据

    3. 从后端得到的上一次确认上传的数据,对数据进行禁用,即只能继续添加不能删除原有的

    4. 从后端得到的上一次确认上传的数据,对数据同步展示在右边已选框

    5. 从后端得到的上一次确认上传的数据,在右边已选框内不能存在删除按钮

    6. 可以继续勾选想要勾选的数据,会同步展示在右边已选框,有删除按钮,可以删除

    7. 点击全选,全选数据,点击反选,把除了从后端得到的上一次确认上传的数据都取消勾选,同时右边已选框也要同步展示

    8. 点击清空已选,把除了从后端得到的上一次确认上传的数据在右边已选框内删除,同时左边的树形结构也要同步取消勾选数据

    9. 点击确定按钮但未确认上传时,再次打开弹框,显示的勾选数据为上次点击确定前勾选的数据,禁用的数据也仅为从后端得到的上一次确认上传的数据,右边已选框也展示的是上次点击确定前勾选的数据,没有删除按钮的也仅为从后端得到的上一次确认上传的数据

    10. 点击确定并在上一层点击确认上传按钮,已经勾选的数据就会传给后端,同步更新到页面中,后续也可以对数据进行编辑操作来修改数据

代码设计:

单选框用到的是el-radio组件

<div class="person_top_nav">
    <span>选择签署范围</span>
    <el-radio-group style="margin-left: 12px" v-model="searchRange" @change="handleSearchChange">
        <el-radio style="max-width: 90px" :disabled="!(SelectedType == 'clientOrganization') && disabledArr.length > 0" :label="1">
            按机构项目组
        </el-radio>
        <el-radio style="max-width: 60px" :disabled="!(SelectedType == 'organization') && disabledArr.length > 0" :label="2">
            按事业部
        </el-radio>
        <el-radio style="max-width: 90px" :disabled="!(SelectedType == 'branchCompany') && disabledArr.length > 0" :label="3">
            按合约归属公司
        </el-radio>
    </el-radio-group>
</div>
  • 绑定的是searchRange,点击事件调用的函数是handleSearchChange()函数

  • :disabled="!(SelectedType ==

<template> <el-dialog :append-to-body="true" title="配置人员" v-if="hasLoaded" :close-on-click-modal="false" v-model="dialogVisible" width="800px" center class="dialog" @close="handleDialogVisible" > <div class="line"></div> <el-form ref="form" :model="form" label-width="140px" size="default" :rules="rules"> <!-- 有效期择 --> <el-form-item label="有效期" prop="signatureTime"> <el-date-picker v-model="form.signatureTime" clearable type="daterange" value-format="YYYY-MM-DD" format="YYYY-MM-DD" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" align="right" class="time-picker" :disabled-date="disabledPastDate" /> </el-form-item> <!-- 树形择区域(优化核心:用el-tree替代复框组) --> <div class="merchant-select-area"> <!-- 左侧:树形择区(可择人员) --> <div class="selectablebox"> <div class="select-header"> <h3>择人员(树形结构)</h3> <el-input placeholder="搜索:名称/ID/层级路径" v-model="userInfoSearch" prefix-icon="el-icon-search" clearable @input="handleSearchDebounced" /> </div> <div class="tree-container"> <el-tree ref="userTree" :data="flatUserList" :props="treeProps" node-key="id" show-checkbox :check-strictly="false" :checked-keys="selectedUserIds" @check-change="handleTreeCheck" :filter-node-method="filterTreeNode" /> </div> </div> <!-- 中间:转移按钮 --> <div class="arrow-column"> <el-button class="btn" @click="moveToSelected" :disabled="!selectedUserToAdd.length" icon="el-icon-d-arrow-right" > 添加到已 </el-button> <el-button class="btn" @click="moveToSelectable" :disabled="!selectedUserToRemove.length" icon="el-icon-d-arrow-left" > 从已移除 </el-button> </div> <!-- 右侧:已中列表(支持搜索) --> <div class="selectablebox"> <div class="select-header"> <h3>已中人员({{ selectedUserInfos.length }}人)</h3> <el-input placeholder="搜索已人员" v-model="selectedUserSearch" prefix-icon="el-icon-search" clearable /> </div> <div class="selected-container"> <el-checkbox-group v-model="selectedUserToRemove"> <el-checkbox v-for="user in filteredSelectedUser" :key="user.id" :label="user" class="merchant-checkbox" > <!-- 显示层级路径,便于区分同名人员 --> {{ user.path || user.name }} <small class="user-id">(ID: {{ user.id }})</small> </el-checkbox> </el-checkbox-group> <!-- 空状态提示 --> <div v-if="!filteredSelectedUser.length" class="empty-tip"> <el-empty description="暂无已人员" /> </div> </div> </div> </div> <!-- 底部按钮 --> <div class="dialog-footer"> <el-button @click="dialogVisible = false">取消</el-button> <el-button type="primary" @click="handleSave">保存配置</el-button> </div> </el-form> </el-dialog> </template> <script> import _ from 'lodash'; import { ElMessageBox, ElMessage, ElEmpty } from 'element-plus'; import { Validator } from '/@/config/utils'; export default { components: { ElEmpty }, // 注册空状态组件 props: { id: { type: String, default: '', required: true, // 强制要求传入rateId validator: (val) => val.trim() !== '', // 基础校验 }, }, data() { return { form: { signatureTime: [], // 有效期范围 }, dialogVisible: true, hasLoaded: false, allUserList: [], // 原始树形人员数据(接口返回) flatUserList: [], // 扁平化后的树形数据(用于渲染) mappingUserList: [], // 已配置人员列表(接口返回) selectedUserInfos: [], // 最终已中人员(右侧展示) // 搜索相关 userInfoSearch: '', // 左侧树形搜索关键词 selectedUserSearch: '', // 右侧已列表搜索关键词 searchTimer: null, // 搜索防抖定时器 // 树形配置 treeProps: { label: 'name', children: 'children', disabled: 'isDisabled', // 用于禁用中人员 }, }; }, computed: { // 已中人员的ID集合(用于树形禁用回显) selectedUserIds() { return this.selectedUserInfos.map(user => user.id); }, // 左侧树形中已勾选但未加入已列表的人员 selectedUserToAdd() { const tree = this.$refs.userTree; if (!tree) return []; // 获取树形当前勾选的节点(排除已在右侧的人员) return tree.getCheckedNodes().filter(node => !this.selectedUserIds.includes(node.id) ); }, // 右侧已中列表中被勾选的人员(用于移除) selectedUserToRemove: { get() { return []; // 初始为空,由复框组双向绑定 }, set(val) { // 确保绑定的是完整对象而非仅ID this._selectedUserToRemove = val.map(item => typeof item === 'string' ? this.selectedUserInfos.find(u => u.id === item) : item ).filter(Boolean); } }, // 右侧已列表的过滤结果(根据搜索关键词) filteredSelectedUser() { if (!this.selectedUserSearch) return this.selectedUserInfos; const keyword = this.selectedUserSearch.toLowerCase(); return this.selectedUserInfos.filter(user => user.name.toLowerCase().includes(keyword) || user.id.toLowerCase().includes(keyword) || (user.path && user.path.toLowerCase().includes(keyword)) ); }, }, watch: { // 监听原始树形数据,初始化时扁平化 allUserList: { immediate: true, handler(newVal) { if (newVal.length) { this.flatUserList = this.flattenTree(newVal); // 扁平化树形数据 } }, }, }, created() { this.getDetail(); // 初始化获取数据 }, methods: { /** * 1. 获取详情数据(整合接口逻辑:一次请求获取所有必要数据) */ async getDetail() { try { console.log('获取人员配置详情,rateId:', this.id); const { data } = await this.$http.get( this.$api.settlement.userConfigDetail, { params: { rateId: this.id } } ); // 解构接口返回数据(与提供的JSON结构对齐) const { rateId, allUserList, mappingUserList, validStartTime, validEndTime } = data; // 填充基础数据 this.allUserList = allUserList || []; // 原始树形人员数据 this.mappingUserList = mappingUserList || []; // 已配置人员 this.form.signatureTime = [validStartTime, validEndTime].filter(Boolean); // 有效期 // 初始化已中列表(处理mappingUserList格式) this.selectedUserInfos = this.mappingUserList.map(user => ({ id: user.id, name: user.name, userId: user.userId, path: this.getNodePath(user.id), // 生成层级路径(如:金币>陆逊>lusu) })); this.hasLoaded = true; } catch (error) { console.error('获取人员配置详情失败:', error); ElMessage.error('数据获取失败,请刷新重试'); this.hasLoaded = true; // 失败后仍显示弹窗,避免阻塞 } }, /** * 2. 树形数据扁平化(保留层级路径) * @param {Array} tree - 原始树形数据 * @param {String} parentPath - 父节点路径(递归用) * @returns {Array} 扁平化后的数组 */ flattenTree(tree, parentPath = '') { let result = []; tree.forEach(node => { // 生成当前节点的层级路径(如:父节点路径>当前节点名称) const currentPath = parentPath ? `${parentPath}>${node.name}` : node.name; // 复制节点并添加路径属性 const flatNode = { ...node, path: currentPath }; // 递归处理子节点 if (node.children && node.children.length) { flatNode.children = this.flattenTree(node.children, currentPath); } // 禁用中的节点(避免重复择) flatNode.isDisabled = this.selectedUserIds.includes(node.id); result.push(flatNode); }); return result; }, /** * 3. 根据节点ID获取层级路径 * @param {String} nodeId - 节点ID * @returns {String} 层级路径(如:金币>陆逊) */ getNodePath(nodeId) { const findPath = (tree, id, path = []) => { for (const node of tree) { const newPath = [...path, node.name]; if (node.id === id) return newPath.join('>'); if (node.children && node.children.length) { const childPath = findPath(node.children, id, newPath); if (childPath) return childPath; } } return ''; }; return findPath(this.allUserList, nodeId); }, /** * 4. 树形节点过滤(搜索逻辑) * @param {String} value - 搜索关键词 * @param {Object} data - 节点数据 * @returns {Boolean} 是否匹配 */ filterTreeNode(value, data) { if (!value) return true; const keyword = value.toLowerCase(); // 匹配名称、ID、层级路径 return data.name.toLowerCase().includes(keyword) || data.id.toLowerCase().includes(keyword) || (data.path && data.path.toLowerCase().includes(keyword)); }, /** * 5. 搜索防抖处理(避免频繁过滤) */ handleSearchDebounced() { clearTimeout(this.searchTimer); this.searchTimer = setTimeout(() => { this.$refs.userTree?.filter(this.userInfoSearch); }, 300); // 300ms防抖延迟 }, /** * 6. 树形节点勾选事件(同步禁用状态) */ handleTreeCheck() { // 重新设置禁用状态(避免已人员被再次勾选) this.flatUserList = this.flattenTree(this.allUserList); }, /** * 7. 转移到已列表 */ moveToSelected() { if (!this.selectedUserToAdd.length) return; // 处理重复择(按ID去重) const newUsers = this.selectedUserToAdd.map(user => ({ id: user.id, name: user.name, userId: user.userId, path: user.path, })); const existingIds = this.selectedUserIds; const uniqueUsers = newUsers.filter(user => !existingIds.includes(user.id)); // 更新已列表 this.selectedUserInfos = [...this.selectedUserInfos, ...uniqueUsers]; ElMessage.success(`成功添加 ${uniqueUsers.length} 人`); // 清空树形勾选状态 this.$refs.userTree?.setCheckedKeys([]); }, /** * 8. 从已列表移除 */ moveToSelectable() { if (!this._selectedUserToRemove.length) return; // 获取要移除的人员ID const removeIds = this._selectedUserToRemove.map(user => user.id); // 过滤已列表 this.selectedUserInfos = this.selectedUserInfos.filter( user => !removeIds.includes(user.id) ); ElMessage.success(`成功移除 ${removeIds.length} 人`); // 清空右侧勾选状态 this._selectedUserToRemove = []; // 重新启用树形中已移除的节点 this.flatUserList = this.flattenTree(this.allUserList); }, /** * 9. 禁用过去的日期(有效期只能今天及以后) */ disabledPastDate(date) { return date < new Date(new Date().setHours(0, 0, 0, 0)); }, /** * 10. 保存配置 */ async handleSave() { // 基础校验 const { signatureTime } = this.form; if (!Array.isArray(signatureTime) || signatureTime.length !== 2) { return ElMessage.error('请择完整的有效期范围'); } if (this.selectedUserInfos.length === 0) { return ElMessage.error('请至少择一名人员'); } // 构造提交数据 const submitData = { rateId: this.id, validStartTime: signatureTime[0], validEndTime: signatureTime[1], userIds: this.selectedUserInfos.map(user => user.id), // 仅传ID数组(符合接口规范) }; try { const { data } = await this.$http.post( this.$api.settlement.saveUserRateConfig, submitData ); if (data.code === 0) { ElMessage.success('人员配置保存成功'); this.$emit('onSuccess', submitData); // 通知父组件成功 this.dialogVisible = false; } else { ElMessage.error(`保存失败:${data.msg || '未知错误'}`); } } catch (error) { console.error('保存人员配置失败:', error); ElMessage.error('网络异常,请重试'); this.$emit('onClose'); } }, /** * 11. 关闭弹窗(通知父组件) */ handleDialogVisible() { this.$emit('onClose'); // 清空状态(避免下次打开残留) this.userInfoSearch = ''; this.selectedUserSearch = ''; this._selectedUserToRemove = []; this.$refs.userTree?.setCheckedKeys([]); }, }, // 组件销毁时清理定时器 beforeUnmount() { clearTimeout(this.searchTimer); }, }; </script> <style lang="scss" scoped> // 基础样式优化 .dialog { ::v-deep .el-dialog__body { padding: 15px 30px !important; } } // 有效期择器样式 .time-picker { width: 100% !important; ::v-deep .el-date-editor { min-width: 100% !important; } } // 分割线样式 .line { margin: -10px 0 20px; border-bottom: 1px solid #eee; } // 标题样式 ::v-deep .el-dialog__title { text-align: center; font-size: 18px; font-weight: 500; } // 择区域主容器 .merchant-select-area { display: flex; justify-content: space-between; align-items: flex-start; gap: 20px; margin: 20px 0; } // 左右择框通用样式 .selectablebox { flex: 1; display: flex; flex-direction: column; gap: 10px; } // 择框头部(标题+搜索) .select-header { display: flex; flex-direction: column; gap: 8px; } .select-header h3 { font-size: 16px; font-weight: 500; color: #333; margin: 0; } .select-header ::v-deep .el-input { width: 100% !important; } // 树形容器样式 .tree-container { max-height: 400px; overflow-y: auto; border: 1px solid #eee; border-radius: 4px; padding: 10px; } // 已列表容器样式 .selected-container { max-height: 400px; overflow-y: auto; border: 1px solid #eee; border-radius: 4px; padding: 10px; } // 已人员复框样式 .merchant-checkbox { display: block; margin-bottom: 8px; padding: 4px 8px; border-radius: 4px; transition: background-color 0.2s; &:hover { background-color: #f5f7fa; } .user-id { margin-left: 8px; color: #666; font-size: 12px; } } // 空状态提示样式 .empty-tip { padding: 40px 0; text-align: center; } // 中间按钮列样式 .arrow-column { display: flex; flex-direction: column; gap: 15px; padding-top: 20px; } .btn { min-width: 120px; color: var(--el-color-primary); border-color: var(--el-color-primary); &:disabled { color: var(--el-color-disabled); border-color: var(--el-color-disabled-border); } } // 底部按钮样式 .dialog-footer { display: flex; justify-content: center; margin-top: 20px; gap: 15px; } // 滚动条样式优化 .tree-container::-webkit-scrollbar, .selected-container::-webkit-scrollbar { width: 6px; height: 6px; } .tree-container::-webkit-scrollbar-thumb, .selected-container::-webkit-scrollbar-thumb { background-color: #ddd; border-radius: 3px; } .tree-container::-webkit-scrollbar-track, .selected-container::-webkit-scrollbar-track { background-color: #f5f5f5; } </style>树形结构无法中什么原因
10-15
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值