<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>树形结构无法选中什么原因