Compare commits

...

3 Commits

Author SHA1 Message Date
jhd
049c3353b3 Merge branch '0.8.X' of http://49.232.154.205:10100/DeXun/klp-oa into 0.8.X 2026-05-21 19:09:31 +08:00
jhd
b9d8c17953 多选框组件重写 2026-05-21 19:08:51 +08:00
jhd
ae6878c00b 重复名称可选 2026-05-21 18:33:44 +08:00

View File

@@ -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>