Compare commits
3 Commits
a3d80b09cb
...
049c3353b3
| Author | SHA1 | Date | |
|---|---|---|---|
| 049c3353b3 | |||
| b9d8c17953 | |||
| ae6878c00b |
@@ -16,23 +16,93 @@
|
||||
<el-button slot="append" icon="el-icon-search" @click="handleSearch" />
|
||||
</el-input>
|
||||
|
||||
<!-- 多选模式:穿梭框 -->
|
||||
<el-transfer
|
||||
v-loading="loading"
|
||||
v-if="multiple"
|
||||
v-model="selectedIds"
|
||||
:data="transferData"
|
||||
:titles="['可选员工', '已选员工']"
|
||||
:button-texts="['移除', '添加']"
|
||||
:props="{
|
||||
key: keyField,
|
||||
label: 'label',
|
||||
disabled: 'disabled'
|
||||
}"
|
||||
filterable
|
||||
:filter-method="transferFilterMethod"
|
||||
filter-placeholder="根据员工部门搜索"
|
||||
/>
|
||||
<!-- 多选模式:自定义穿梭框 -->
|
||||
<div v-if="multiple" class="custom-transfer" v-loading="loading">
|
||||
<!-- 左侧:可选列表 -->
|
||||
<div class="transfer-panel">
|
||||
<div class="panel-header">可选员工</div>
|
||||
<div class="panel-search">
|
||||
<input
|
||||
type="text"
|
||||
v-model="leftSearchQuery"
|
||||
placeholder="搜索姓名/部门"
|
||||
class="search-input"
|
||||
/>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div
|
||||
v-for="item in filteredAvailableList"
|
||||
:key="`left-${item.infoId}`"
|
||||
:class="['transfer-item', { disabled: item.disabled, selected: isLeftSelected(item), 'already-selected': item.isSelected }]"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="item.isSelected"
|
||||
:disabled="item.disabled"
|
||||
@click.stop="toggleItem(item)"
|
||||
class="custom-checkbox"
|
||||
/>
|
||||
<span v-if="item.isDuplicate" class="duplicate-icon" title="重名">
|
||||
<i class="el-icon-user" />
|
||||
</span>
|
||||
<span class="item-label">{{ item.dept }} - {{ item.name }} ({{ item.jobType }})</span>
|
||||
<span v-if="item.isSelected" class="selected-tag">已选</span>
|
||||
</div>
|
||||
<div v-if="filteredAvailableList.length === 0" class="empty-tip">暂无数据</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 中间按钮组 -->
|
||||
<div class="transfer-buttons">
|
||||
<el-button
|
||||
type="primary"
|
||||
icon="el-icon-arrow-right"
|
||||
@click="addSelected"
|
||||
:disabled="leftSelectedKeys.length === 0"
|
||||
>
|
||||
添加
|
||||
</el-button>
|
||||
<el-button
|
||||
icon="el-icon-arrow-left"
|
||||
@click="removeSelected"
|
||||
:disabled="rightSelectedKeys.length === 0"
|
||||
>
|
||||
移除
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:已选列表 -->
|
||||
<div class="transfer-panel">
|
||||
<div class="panel-header">已选员工</div>
|
||||
<div class="panel-search">
|
||||
<input
|
||||
type="text"
|
||||
v-model="rightSearchQuery"
|
||||
placeholder="搜索姓名/部门"
|
||||
class="search-input"
|
||||
/>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div
|
||||
v-for="item in filteredSelectedList"
|
||||
:key="`right-${item.infoId}`"
|
||||
:class="['transfer-item', 'selected-item', { selected: isRightSelected(item) }]"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="isRightSelected(item)"
|
||||
@click.stop="toggleSelectedItem(item)"
|
||||
class="custom-checkbox"
|
||||
/>
|
||||
<span v-if="item.isDuplicate" class="duplicate-icon" title="重名">
|
||||
<i class="el-icon-user" />
|
||||
</span>
|
||||
<span class="item-label">{{ item.dept }} - {{ item.name }} ({{ item.jobType }})</span>
|
||||
</div>
|
||||
<div v-if="filteredSelectedList.length === 0" class="empty-tip">暂无数据</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 单选模式:表格 -->
|
||||
<div v-if="!multiple" class="table-container">
|
||||
@@ -101,9 +171,14 @@ export default {
|
||||
open: false,
|
||||
loading: false,
|
||||
searchQuery: '',
|
||||
leftSearchQuery: '',
|
||||
rightSearchQuery: '',
|
||||
selectedIds: [],
|
||||
selectedId: '',
|
||||
selectedEmployee: {}
|
||||
selectedEmployee: {},
|
||||
// 用于记录选中状态(解决重名问题)
|
||||
leftSelectedKeys: [],
|
||||
rightSelectedKeys: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -132,12 +207,73 @@ export default {
|
||||
disabled: this.disabledIdList.includes(employee[this.keyField].toString())
|
||||
}))
|
||||
},
|
||||
transferData() {
|
||||
return this.employeeList.map(employee => ({
|
||||
[this.keyField]: employee[this.keyField],
|
||||
label: `${employee.dept} - ${employee.name} (${employee.jobType})`,
|
||||
disabled: employee.disabled
|
||||
}))
|
||||
// 获取重名的姓名列表
|
||||
duplicateNames() {
|
||||
const nameCount = {}
|
||||
this.rawEmployeeList.forEach(employee => {
|
||||
const name = employee.name || ''
|
||||
nameCount[name] = (nameCount[name] || 0) + 1
|
||||
})
|
||||
return Object.keys(nameCount).filter(name => nameCount[name] > 1)
|
||||
},
|
||||
// 可选列表(显示所有员工,包括已选的)
|
||||
availableList() {
|
||||
const duplicates = this.duplicateNames
|
||||
return this.rawEmployeeList.map(employee => {
|
||||
// 直接使用 infoId 作为唯一标识
|
||||
const employeeId = employee.infoId
|
||||
const idStr = String(employeeId)
|
||||
return {
|
||||
...employee,
|
||||
disabled: this.disabledIdList.includes(idStr),
|
||||
isDuplicate: duplicates.includes(employee.name || ''),
|
||||
isSelected: this.selectedIds.some(id => String(id) === idStr)
|
||||
}
|
||||
})
|
||||
},
|
||||
// 已选列表
|
||||
selectedList() {
|
||||
const duplicates = this.duplicateNames
|
||||
// 使用 infoId 作为唯一标识,支持同名不同ID的员工
|
||||
return this.selectedIds
|
||||
.map(id => {
|
||||
const idStr = String(id)
|
||||
const matched = this.rawEmployeeList.filter(item => String(item.infoId) === idStr)
|
||||
const employee = matched.length > 0 ? matched[0] : null
|
||||
if (employee) {
|
||||
// 创建新对象,避免修改原始数据
|
||||
return {
|
||||
...employee,
|
||||
isDuplicate: duplicates.includes(employee.name || '')
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
.filter(item => item !== null)
|
||||
},
|
||||
// 左侧过滤后的列表
|
||||
filteredAvailableList() {
|
||||
if (!this.leftSearchQuery) {
|
||||
return this.availableList
|
||||
}
|
||||
const query = this.leftSearchQuery.toLowerCase()
|
||||
return this.availableList.filter(item => {
|
||||
const name = (item.name || '').toLowerCase()
|
||||
const dept = (item.dept || '').toLowerCase()
|
||||
return name.includes(query) || dept.includes(query)
|
||||
})
|
||||
},
|
||||
// 右侧过滤后的列表
|
||||
filteredSelectedList() {
|
||||
if (!this.rightSearchQuery) {
|
||||
return this.selectedList
|
||||
}
|
||||
const query = this.rightSearchQuery.toLowerCase()
|
||||
return this.selectedList.filter(item => {
|
||||
const name = (item.name || '').toLowerCase()
|
||||
const dept = (item.dept || '').toLowerCase()
|
||||
return name.includes(query) || dept.includes(query)
|
||||
})
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -145,7 +281,13 @@ export default {
|
||||
handler(newVal) {
|
||||
if (this.multiple) {
|
||||
if (newVal) {
|
||||
this.selectedIds = newVal.split(',').map(id => id.trim()).filter(id => id).map(id => isNaN(Number(id)) ? id : Number(id))
|
||||
// 处理重名问题:确保ID是唯一标识,转换为正确类型
|
||||
this.selectedIds = newVal.split(',').map(id => {
|
||||
const trimmedId = id.trim()
|
||||
if (!trimmedId) return null
|
||||
const numId = Number(trimmedId)
|
||||
return isNaN(numId) ? trimmedId : numId
|
||||
}).filter(id => id !== null)
|
||||
} else {
|
||||
this.selectedIds = []
|
||||
}
|
||||
@@ -164,6 +306,9 @@ export default {
|
||||
methods: {
|
||||
toggleDialog() {
|
||||
if (!this.open) {
|
||||
// 打开时清空选中状态
|
||||
this.leftSelectedKeys = []
|
||||
this.rightSelectedKeys = []
|
||||
this.getEmployeeList()
|
||||
}
|
||||
this.open = !this.open
|
||||
@@ -177,7 +322,11 @@ export default {
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
listEmployeeInfo(params).then(response => {
|
||||
this.rawEmployeeList = response.rows;
|
||||
// 处理重名情况:确保每个员工有唯一标识
|
||||
// 过滤掉已离职的员工
|
||||
this.rawEmployeeList = (response.rows || []).filter(employee => {
|
||||
return !this.isEmployeeResigned(employee)
|
||||
})
|
||||
this.loading = false
|
||||
resolve()
|
||||
}).catch(() => {
|
||||
@@ -186,6 +335,11 @@ export default {
|
||||
})
|
||||
})
|
||||
},
|
||||
// 判断员工是否已离职
|
||||
isEmployeeResigned(employee) {
|
||||
// 支持多种可能的离职状态字段
|
||||
return employee.status === 1 || employee.isLeave === true || employee.resignStatus === 1 || employee.leaveStatus === 1
|
||||
},
|
||||
|
||||
handleSearch() {
|
||||
},
|
||||
@@ -208,6 +362,12 @@ export default {
|
||||
return isDisabled
|
||||
},
|
||||
|
||||
// 判断员工是否已离职
|
||||
isEmployeeResigned(employee) {
|
||||
// isLeave: 1 表示已离职,0 表示在职
|
||||
return employee.isLeave === 1 || employee.isLeave === '1'
|
||||
},
|
||||
|
||||
rowClassName({ row }) {
|
||||
if (this.isSelected(row)) {
|
||||
return 'selected-row'
|
||||
@@ -235,14 +395,114 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
transferFilterMethod(query, item) {
|
||||
return item.label.toLowerCase().includes(query.toLowerCase())
|
||||
// ========== 多选模式方法 ==========
|
||||
|
||||
// 判断左侧是否选中
|
||||
isLeftSelected(item) {
|
||||
return this.leftSelectedKeys.includes(item.infoId)
|
||||
},
|
||||
|
||||
// 判断右侧是否选中
|
||||
isRightSelected(item) {
|
||||
return this.rightSelectedKeys.includes(item.infoId)
|
||||
},
|
||||
|
||||
// 切换左侧选中状态
|
||||
toggleItem(item) {
|
||||
if (item.disabled) return
|
||||
const key = item.infoId
|
||||
const index = this.leftSelectedKeys.indexOf(key)
|
||||
if (index > -1) {
|
||||
this.leftSelectedKeys.splice(index, 1)
|
||||
} else {
|
||||
this.leftSelectedKeys.push(key)
|
||||
}
|
||||
},
|
||||
|
||||
// 切换右侧选中状态
|
||||
toggleSelectedItem(item) {
|
||||
const key = item.infoId
|
||||
const index = this.rightSelectedKeys.indexOf(key)
|
||||
if (index > -1) {
|
||||
this.rightSelectedKeys.splice(index, 1)
|
||||
} else {
|
||||
this.rightSelectedKeys.push(key)
|
||||
}
|
||||
},
|
||||
|
||||
// 添加单个到已选
|
||||
addToSelected(item) {
|
||||
if (item.disabled) return
|
||||
// 直接使用 infoId 作为唯一标识
|
||||
const key = item.infoId
|
||||
const keyStr = String(key)
|
||||
// 检查是否已存在(字符串比较)
|
||||
const exists = this.selectedIds.some(id => String(id) === keyStr)
|
||||
if (!exists) {
|
||||
this.selectedIds.push(key)
|
||||
console.log('添加员工:', item.name, 'ID:', key, 'selectedIds:', this.selectedIds)
|
||||
} else {
|
||||
console.log('员工已存在:', item.name, 'ID:', key)
|
||||
}
|
||||
},
|
||||
|
||||
// 从已选移除单个
|
||||
removeFromSelected(item) {
|
||||
// 直接使用 infoId 作为唯一标识
|
||||
const key = item.infoId
|
||||
const keyStr = String(key)
|
||||
const index = this.selectedIds.findIndex(id => String(id) === keyStr)
|
||||
if (index > -1) {
|
||||
this.selectedIds.splice(index, 1)
|
||||
}
|
||||
},
|
||||
|
||||
// 添加左侧勾选的员工
|
||||
addSelected() {
|
||||
this.leftSelectedKeys.forEach(key => {
|
||||
const keyStr = String(key)
|
||||
if (!this.selectedIds.some(id => String(id) === keyStr)) {
|
||||
this.selectedIds.push(key)
|
||||
}
|
||||
})
|
||||
this.leftSelectedKeys = []
|
||||
},
|
||||
|
||||
// 移除右侧勾选的员工
|
||||
removeSelected() {
|
||||
this.rightSelectedKeys.forEach(key => {
|
||||
const keyStr = String(key)
|
||||
const index = this.selectedIds.findIndex(id => String(id) === keyStr)
|
||||
if (index > -1) {
|
||||
this.selectedIds.splice(index, 1)
|
||||
}
|
||||
})
|
||||
this.rightSelectedKeys = []
|
||||
},
|
||||
|
||||
// 添加所有可选员工(未禁用且未选中的)
|
||||
addAll() {
|
||||
this.availableList.forEach(item => {
|
||||
if (!item.disabled && !item.isSelected) {
|
||||
const keyStr = String(item.infoId)
|
||||
if (!this.selectedIds.some(id => String(id) === keyStr)) {
|
||||
this.selectedIds.push(item.infoId)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 移除所有已选员工
|
||||
removeAll() {
|
||||
this.selectedIds = []
|
||||
this.rightSelectedKeys = []
|
||||
},
|
||||
|
||||
confirmSelection() {
|
||||
if (this.multiple) {
|
||||
// 使用ID作为唯一标识,避免重名问题
|
||||
this.$emit('input', this.selectedIds.join(','))
|
||||
const selectedEmployees = this.rawEmployeeList.filter(item => this.selectedIds.includes(item[this.keyField]))
|
||||
const selectedEmployees = this.selectedList
|
||||
this.$emit('change', selectedEmployees)
|
||||
}
|
||||
this.open = false
|
||||
@@ -251,10 +511,17 @@ export default {
|
||||
cancelSelection() {
|
||||
if (this.multiple) {
|
||||
if (this.value) {
|
||||
this.selectedIds = this.value.split(',').map(id => id.trim()).filter(id => id).map(id => isNaN(Number(id)) ? id : Number(id))
|
||||
this.selectedIds = this.value.split(',').map(id => {
|
||||
const trimmedId = id.trim()
|
||||
if (!trimmedId) return null
|
||||
const numId = Number(trimmedId)
|
||||
return isNaN(numId) ? trimmedId : numId
|
||||
}).filter(id => id !== null)
|
||||
} else {
|
||||
this.selectedIds = []
|
||||
}
|
||||
this.leftSelectedKeys = []
|
||||
this.rightSelectedKeys = []
|
||||
} else {
|
||||
if (this.value) {
|
||||
this.findSelectedEmployee(this.value)
|
||||
@@ -295,29 +562,161 @@ export default {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
::v-deep .el-transfer {
|
||||
/* 自定义穿梭框样式 */
|
||||
.custom-transfer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
gap: 20px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.transfer-panel {
|
||||
width: 350px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 12px 15px;
|
||||
font-weight: 600;
|
||||
border-bottom: 1px solid #dcdfe6;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.panel-search {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #f2f6fc;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
outline: none;
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.transfer-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 15px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #f2f6fc;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
::v-deep .el-transfer-panel {
|
||||
width: 380px;
|
||||
.transfer-item:hover:not(.disabled) {
|
||||
background-color: #ecf5ff;
|
||||
}
|
||||
|
||||
::v-deep .el-transfer-panel__body {
|
||||
height: 450px;
|
||||
.transfer-item.disabled {
|
||||
color: #c0c4cc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
::v-deep .el-transfer__buttons {
|
||||
padding: 0 15px;
|
||||
.transfer-item.selected {
|
||||
background-color: #e8f4fd;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
flex: 1;
|
||||
margin-left: 4px;
|
||||
font-size: 13px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.duplicate-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background-color: #ffb800;
|
||||
color: #fff;
|
||||
border-radius: 50%;
|
||||
font-size: 12px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.duplicate-icon i {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.already-selected {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.selected-tag {
|
||||
font-size: 11px;
|
||||
color: #67c23a;
|
||||
background-color: #f0f9eb;
|
||||
padding: 1px 4px;
|
||||
border-radius: 2px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.resigned-tag {
|
||||
font-size: 11px;
|
||||
color: #f56c6c;
|
||||
background-color: #fef0f0;
|
||||
padding: 1px 4px;
|
||||
border-radius: 2px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.id-badge {
|
||||
font-size: 10px;
|
||||
color: #909399;
|
||||
background-color: #f5f7fa;
|
||||
padding: 1px 3px;
|
||||
border-radius: 2px;
|
||||
margin-left: 4px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/* 自定义 checkbox 样式 */
|
||||
.custom-checkbox {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
accent-color: #409eff;
|
||||
}
|
||||
|
||||
.custom-checkbox:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.transfer-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
::v-deep .el-transfer__button {
|
||||
margin: 10px 0;
|
||||
.empty-tip {
|
||||
text-align: center;
|
||||
color: #c0c4cc;
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
::v-deep .el-table .selected-row {
|
||||
@@ -327,8 +726,4 @@ export default {
|
||||
::v-deep .el-table .selected-row:hover {
|
||||
background-color: #ecf5ff !important;
|
||||
}
|
||||
|
||||
::v-deep .el-transfer-panel__list.is-filterable {
|
||||
height: calc(100% - 60px)
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user