This commit is contained in:
parent
400a11fc7e
commit
3bc10cb2de
|
|
@ -26,79 +26,66 @@
|
|||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-bar">
|
||||
<el-input
|
||||
v-if="activeTab === 'person'"
|
||||
v-model.trim="searchKeyword"
|
||||
placeholder="输入关键字"
|
||||
clearable
|
||||
prefix-icon="Search"
|
||||
@input="handleSearch"
|
||||
/>
|
||||
<el-input
|
||||
v-else
|
||||
v-model.trim="groupSearchKeyword"
|
||||
placeholder="输入关键字"
|
||||
clearable
|
||||
prefix-icon="Search"
|
||||
@input="handleGroupSearch"
|
||||
/>
|
||||
<!-- 人员tab内容 -->
|
||||
<div v-show="activeTab === 'person'" class="tab-content">
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-bar">
|
||||
<el-input
|
||||
v-model.trim="searchKeyword"
|
||||
placeholder="输入关键字"
|
||||
clearable
|
||||
prefix-icon="Search"
|
||||
@input="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 人员表格 - 使用虚拟表格 -->
|
||||
<div class="table-container">
|
||||
<el-auto-resizer>
|
||||
<template #default="{ height, width }">
|
||||
<el-table-v2
|
||||
:columns="personTableColumns"
|
||||
:data="filteredPersonList"
|
||||
:width="width"
|
||||
:height="height"
|
||||
row-key="id"
|
||||
fixed
|
||||
/>
|
||||
</template>
|
||||
</el-auto-resizer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 人员表格 -->
|
||||
<el-table
|
||||
v-if="activeTab === 'person'"
|
||||
border
|
||||
ref="personTableRef"
|
||||
:data="filteredPersonList"
|
||||
height="400"
|
||||
@selection-change="onPersonSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="50" align="center" />
|
||||
<el-table-column
|
||||
show-overflow-tooltip
|
||||
prop="orgName"
|
||||
label="人员所属"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
show-overflow-tooltip
|
||||
prop="workerName"
|
||||
label="姓名"
|
||||
width="120"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column show-overflow-tooltip prop="sex" label="性别" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.sex == 1 ? '男' : '女' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
show-overflow-tooltip
|
||||
prop="phone"
|
||||
label="电话"
|
||||
align="center"
|
||||
/>
|
||||
</el-table>
|
||||
<!-- 分组tab内容 -->
|
||||
<div v-show="activeTab === 'group'" class="tab-content">
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-bar">
|
||||
<el-input
|
||||
v-model.trim="groupSearchKeyword"
|
||||
placeholder="输入关键字"
|
||||
clearable
|
||||
prefix-icon="Search"
|
||||
@input="handleGroupSearch"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 分组表格 -->
|
||||
<el-table
|
||||
v-else
|
||||
border
|
||||
ref="groupTableRef"
|
||||
:data="filteredGroupList"
|
||||
height="400"
|
||||
@selection-change="onGroupSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="50" align="center" />
|
||||
<el-table-column
|
||||
show-overflow-tooltip
|
||||
prop="groupName"
|
||||
label="分组名称"
|
||||
align="center"
|
||||
/>
|
||||
</el-table>
|
||||
<!-- 分组表格 -->
|
||||
<el-table
|
||||
border
|
||||
ref="groupTableRef"
|
||||
:data="filteredGroupList"
|
||||
height="400"
|
||||
@selection-change="onGroupSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="50" align="center" />
|
||||
<el-table-column
|
||||
show-overflow-tooltip
|
||||
prop="groupName"
|
||||
label="分组名称"
|
||||
align="center"
|
||||
/>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-col>
|
||||
|
||||
<!-- 右侧已选内容 -->
|
||||
|
|
@ -157,10 +144,12 @@
|
|||
</template>
|
||||
|
||||
<script setup name="PersonPicker">
|
||||
import { ref, computed, watch, nextTick, onMounted } from 'vue'
|
||||
import { ref, computed, watch, nextTick, onMounted, h } from 'vue'
|
||||
import { Close } from '@element-plus/icons-vue'
|
||||
import { ElCheckbox } from 'element-plus'
|
||||
import { getAllPersonAPI } from '@/api/personManage/person.js'
|
||||
import { getAllGroupAPI } from '@/api/basicManage/groupManage.js'
|
||||
import { debounce } from '@/utils'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
|
|
@ -178,7 +167,6 @@ const emit = defineEmits(['update:modelValue', 'update:groupList', 'confirm'])
|
|||
const activeTab = ref('person')
|
||||
const searchKeyword = ref('')
|
||||
const groupSearchKeyword = ref('')
|
||||
const personTableRef = ref(null)
|
||||
const groupTableRef = ref(null)
|
||||
|
||||
// 数据列表
|
||||
|
|
@ -197,19 +185,106 @@ const isSyncing = ref(false)
|
|||
// 标志位:是否已经初始化过(用于区分是用户操作还是外部数据变化)
|
||||
const isInitialized = ref(false)
|
||||
|
||||
// 过滤后的人员列表
|
||||
const filteredPersonList = computed(() => {
|
||||
if (!searchKeyword.value) {
|
||||
return personList.value
|
||||
}
|
||||
const keyword = searchKeyword.value.toLowerCase()
|
||||
return personList.value.filter((item) => {
|
||||
const orgName = (item.orgName || '').toLowerCase()
|
||||
const workerName = (item.workerName || '').toLowerCase()
|
||||
return orgName.includes(keyword) || workerName.includes(keyword)
|
||||
})
|
||||
// 已选中行的keys(用于虚拟化表格)
|
||||
const selectedRowKeys = ref([])
|
||||
|
||||
// 过滤后的人员列表(使用ref避免频繁计算)
|
||||
const filteredPersonList = ref([])
|
||||
|
||||
// 防抖后的过滤函数
|
||||
const debouncedPersonFilter = ref(null)
|
||||
|
||||
// 虚拟化表格列配置
|
||||
const personTableColumns = computed(() => {
|
||||
return [
|
||||
{
|
||||
key: 'selection',
|
||||
width: 55,
|
||||
cellRenderer: ({ rowData }) => {
|
||||
const isSelected = selectedRowKeys.value.includes(rowData.id)
|
||||
return h(ElCheckbox, {
|
||||
modelValue: isSelected,
|
||||
'onUpdate:modelValue': (val) => {
|
||||
handlePersonRowSelect({ row: rowData, checked: val })
|
||||
},
|
||||
})
|
||||
},
|
||||
headerCellRenderer: () => {
|
||||
const allSelected =
|
||||
filteredPersonList.value.length > 0 &&
|
||||
filteredPersonList.value.every((row) => selectedRowKeys.value.includes(row.id))
|
||||
const someSelected = filteredPersonList.value.some((row) =>
|
||||
selectedRowKeys.value.includes(row.id),
|
||||
)
|
||||
|
||||
return h(ElCheckbox, {
|
||||
modelValue: allSelected,
|
||||
indeterminate: someSelected && !allSelected,
|
||||
'onUpdate:modelValue': (val) => {
|
||||
handlePersonSelectAll({ checked: val })
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'orgName',
|
||||
dataKey: 'orgName',
|
||||
title: '人员所属',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
key: 'workerName',
|
||||
dataKey: 'workerName',
|
||||
title: '姓名',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
key: 'sex',
|
||||
dataKey: 'sex',
|
||||
title: '性别',
|
||||
width: 80,
|
||||
cellRenderer: ({ rowData }) => {
|
||||
return rowData.sex == 1 ? '男' : '女'
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'phone',
|
||||
dataKey: 'phone',
|
||||
title: '电话',
|
||||
width: 200,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
// 执行人员过滤操作(优化后的版本)
|
||||
const performPersonFilter = () => {
|
||||
if (personList.value.length === 0) {
|
||||
filteredPersonList.value = []
|
||||
return
|
||||
}
|
||||
|
||||
if (!searchKeyword.value) {
|
||||
filteredPersonList.value = personList.value
|
||||
} else {
|
||||
const keyword = searchKeyword.value.toLowerCase()
|
||||
filteredPersonList.value = personList.value.filter((item) => {
|
||||
const orgName = (item.orgName || '').toLowerCase()
|
||||
const workerName = (item.workerName || '').toLowerCase()
|
||||
return orgName.includes(keyword) || workerName.includes(keyword)
|
||||
})
|
||||
}
|
||||
|
||||
// 过滤后同步选中状态
|
||||
nextTick(() => {
|
||||
syncPersonTableSelection()
|
||||
})
|
||||
}
|
||||
|
||||
// 创建防抖过滤函数
|
||||
const createDebouncedPersonFilter = () => {
|
||||
debouncedPersonFilter.value = debounce(performPersonFilter, 300)
|
||||
}
|
||||
|
||||
// 过滤后的分组列表
|
||||
const filteredGroupList = computed(() => {
|
||||
if (!groupSearchKeyword.value) {
|
||||
|
|
@ -286,11 +361,13 @@ const handleTabChange = (tabName) => {
|
|||
})
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
// 搜索处理(使用防抖)
|
||||
const handleSearch = () => {
|
||||
nextTick(() => {
|
||||
syncPersonTableSelection()
|
||||
})
|
||||
if (debouncedPersonFilter.value) {
|
||||
debouncedPersonFilter.value()
|
||||
} else {
|
||||
performPersonFilter()
|
||||
}
|
||||
}
|
||||
|
||||
const handleGroupSearch = () => {
|
||||
|
|
@ -299,50 +376,133 @@ const handleGroupSearch = () => {
|
|||
})
|
||||
}
|
||||
|
||||
// 人员选择变化
|
||||
const onPersonSelectionChange = (selection) => {
|
||||
// 虚拟化表格:单行选择变化
|
||||
const handlePersonRowSelect = ({ row, checked }) => {
|
||||
if (isSyncing.value) {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前过滤后可见的人员ID集合
|
||||
const visiblePersonIds = new Set(filteredPersonList.value.map((item) => item.id))
|
||||
isSyncing.value = true
|
||||
|
||||
// 获取当前选中的可见人员ID集合
|
||||
const selectedVisibleIds = new Set(selection.map((item) => item.id))
|
||||
const rowId = Number(row.id)
|
||||
const allSelectedIds = new Set(directSelectedPersonIds.value.map((id) => Number(id)))
|
||||
|
||||
// 保留不在当前过滤列表中的已选人员(这些人员被搜索过滤掉了,但应该保留)
|
||||
const preservedIds = directSelectedPersonIds.value.filter((id) => !visiblePersonIds.has(id))
|
||||
if (checked) {
|
||||
allSelectedIds.add(rowId)
|
||||
if (!selectedRowKeys.value.includes(row.id)) {
|
||||
selectedRowKeys.value.push(row.id)
|
||||
}
|
||||
} else {
|
||||
allSelectedIds.delete(rowId)
|
||||
const index = selectedRowKeys.value.indexOf(row.id)
|
||||
if (index > -1) {
|
||||
selectedRowKeys.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 合并保留的ID和当前选中的可见ID
|
||||
directSelectedPersonIds.value = [...preservedIds, ...selectedVisibleIds]
|
||||
directSelectedPersonIds.value = Array.from(allSelectedIds)
|
||||
|
||||
// 触发更新,合并所有选中的人员
|
||||
// 触发更新
|
||||
updateSelectedPersons()
|
||||
|
||||
nextTick(() => {
|
||||
isSyncing.value = false
|
||||
})
|
||||
}
|
||||
|
||||
// 更新选中的内容
|
||||
// 虚拟化表格:全选/取消全选(优化:批量处理,避免逐个操作,使用异步更新提升性能)
|
||||
const handlePersonSelectAll = ({ checked }) => {
|
||||
if (isSyncing.value) {
|
||||
return
|
||||
}
|
||||
|
||||
isSyncing.value = true
|
||||
|
||||
// 先更新UI状态,提升响应速度
|
||||
if (checked) {
|
||||
// 全选:先快速更新UI显示的选中keys
|
||||
const filteredLength = filteredPersonList.value.length
|
||||
if (filteredLength > 0) {
|
||||
// 使用预分配数组,性能更好
|
||||
const newSelectedKeys = new Array(filteredLength)
|
||||
for (let i = 0; i < filteredLength; i++) {
|
||||
newSelectedKeys[i] = filteredPersonList.value[i].id
|
||||
}
|
||||
selectedRowKeys.value = newSelectedKeys
|
||||
}
|
||||
} else {
|
||||
// 取消全选:清空当前可见的选中keys
|
||||
selectedRowKeys.value = []
|
||||
}
|
||||
|
||||
// 使用 requestAnimationFrame 延迟更新数据状态,避免阻塞UI渲染
|
||||
requestAnimationFrame(() => {
|
||||
const allSelectedIds = new Set(directSelectedPersonIds.value.map((id) => Number(id)))
|
||||
const filteredList = filteredPersonList.value
|
||||
const filteredLength = filteredList.length
|
||||
|
||||
if (checked) {
|
||||
// 全选:批量添加当前过滤列表中的所有项
|
||||
// 直接遍历 filteredList,避免创建中间数组
|
||||
for (let i = 0; i < filteredLength; i++) {
|
||||
allSelectedIds.add(Number(filteredList[i].id))
|
||||
}
|
||||
} else {
|
||||
// 取消全选:批量移除当前过滤列表中的项
|
||||
for (let i = 0; i < filteredLength; i++) {
|
||||
allSelectedIds.delete(Number(filteredList[i].id))
|
||||
}
|
||||
}
|
||||
|
||||
// 更新选中的人员ID列表(使用预分配数组,性能更好)
|
||||
const selectedIdsArray = new Array(allSelectedIds.size)
|
||||
let index = 0
|
||||
for (const id of allSelectedIds) {
|
||||
selectedIdsArray[index++] = id
|
||||
}
|
||||
directSelectedPersonIds.value = selectedIdsArray
|
||||
|
||||
// 延迟触发更新,避免阻塞UI
|
||||
requestAnimationFrame(() => {
|
||||
updateSelectedPersons()
|
||||
isSyncing.value = false
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 更新选中的内容(优化:使用Map缓存,避免重复查找)
|
||||
const updateSelectedPersons = () => {
|
||||
// 标记为已初始化,避免watch反向同步
|
||||
isInitialized.value = true
|
||||
|
||||
// 更新人员列表
|
||||
const persons = directSelectedPersonIds.value
|
||||
.map((personId) => {
|
||||
const person = personList.value.find((p) => p.id === personId)
|
||||
if (person) {
|
||||
return {
|
||||
id: person.id,
|
||||
workerName: person.workerName,
|
||||
orgName: person.orgName,
|
||||
sex: person.sex,
|
||||
phone: person.phone,
|
||||
}
|
||||
// 创建人员ID到人员对象的Map,提升查找性能
|
||||
const personMap = new Map()
|
||||
personList.value.forEach((person) => {
|
||||
personMap.set(Number(person.id), person)
|
||||
})
|
||||
|
||||
// 更新人员列表(使用Map查找,性能更好)
|
||||
const selectedIds = directSelectedPersonIds.value
|
||||
const persons = new Array(selectedIds.length)
|
||||
let validCount = 0
|
||||
|
||||
for (let i = 0; i < selectedIds.length; i++) {
|
||||
const personId = Number(selectedIds[i])
|
||||
const person = personMap.get(personId)
|
||||
if (person) {
|
||||
persons[validCount++] = {
|
||||
id: person.id,
|
||||
workerName: person.workerName,
|
||||
orgName: person.orgName,
|
||||
sex: person.sex,
|
||||
phone: person.phone,
|
||||
}
|
||||
return null
|
||||
})
|
||||
.filter(Boolean)
|
||||
emit('update:modelValue', persons)
|
||||
}
|
||||
}
|
||||
|
||||
// 只保留有效的人员
|
||||
const validPersons = validCount === persons.length ? persons : persons.slice(0, validCount)
|
||||
emit('update:modelValue', validPersons)
|
||||
|
||||
// 更新分组列表
|
||||
const groups = selectedGroupIds.value
|
||||
|
|
@ -376,33 +536,38 @@ const onGroupSelectionChange = (selection) => {
|
|||
updateSelectedPersons()
|
||||
}
|
||||
|
||||
// 同步人员表格选中状态
|
||||
// 同步人员表格选中状态(优化:使用Set提高查找性能,批量操作)
|
||||
const syncPersonTableSelection = () => {
|
||||
if (!personTableRef.value || directSelectedPersonIds.value.length === 0) {
|
||||
if (personTableRef.value) {
|
||||
isSyncing.value = true
|
||||
personTableRef.value.clearSelection()
|
||||
nextTick(() => {
|
||||
isSyncing.value = false
|
||||
})
|
||||
}
|
||||
// 如果正在同步中,跳过,避免重复同步
|
||||
if (isSyncing.value) {
|
||||
return
|
||||
}
|
||||
|
||||
// 设置同步标志,防止触发 watch
|
||||
isSyncing.value = true
|
||||
const selectedIds = new Set(directSelectedPersonIds.value.map((id) => Number(id)))
|
||||
|
||||
nextTick(() => {
|
||||
personTableRef.value.clearSelection()
|
||||
filteredPersonList.value.forEach((row) => {
|
||||
const rowId = Number(row.id)
|
||||
if (selectedIds.has(rowId)) {
|
||||
personTableRef.value.toggleRowSelection(row, true)
|
||||
}
|
||||
})
|
||||
if (directSelectedPersonIds.value.length === 0) {
|
||||
// 如果没有选中项,清空所有选中状态
|
||||
selectedRowKeys.value = []
|
||||
nextTick(() => {
|
||||
isSyncing.value = false
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 使用Set提高查找性能
|
||||
const selectedIds = new Set(directSelectedPersonIds.value.map((id) => Number(id)))
|
||||
|
||||
// 只同步当前过滤列表中可见的选中项
|
||||
const visibleSelectedKeys = filteredPersonList.value
|
||||
.filter((row) => selectedIds.has(Number(row.id)))
|
||||
.map((row) => row.id)
|
||||
|
||||
selectedRowKeys.value = visibleSelectedKeys
|
||||
|
||||
// 同步完成后,延迟重置标志位
|
||||
nextTick(() => {
|
||||
isSyncing.value = false
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -439,6 +604,11 @@ const syncGroupTableSelection = () => {
|
|||
// 移除单个人员(仅用于人员tab)
|
||||
const handleRemovePerson = (personId) => {
|
||||
directSelectedPersonIds.value = directSelectedPersonIds.value.filter((id) => id !== personId)
|
||||
// 同时更新虚拟表格的选中keys
|
||||
const index = selectedRowKeys.value.indexOf(personId)
|
||||
if (index > -1) {
|
||||
selectedRowKeys.value.splice(index, 1)
|
||||
}
|
||||
updateSelectedPersons()
|
||||
// 同步人员表格
|
||||
nextTick(() => {
|
||||
|
|
@ -451,9 +621,7 @@ const handleClearAll = () => {
|
|||
isSyncing.value = true
|
||||
if (activeTab.value === 'person') {
|
||||
directSelectedPersonIds.value = []
|
||||
if (personTableRef.value) {
|
||||
personTableRef.value.clearSelection()
|
||||
}
|
||||
selectedRowKeys.value = []
|
||||
} else {
|
||||
selectedGroupIds.value = []
|
||||
if (groupTableRef.value) {
|
||||
|
|
@ -472,6 +640,11 @@ const getPersonList = async () => {
|
|||
const result = await getAllPersonAPI()
|
||||
if (result.code === 200 && result.rows) {
|
||||
personList.value = result.rows || []
|
||||
// 初始化过滤列表
|
||||
performPersonFilter()
|
||||
// 创建防抖过滤函数
|
||||
createDebouncedPersonFilter()
|
||||
// 人员列表加载完成后,同步选中状态
|
||||
nextTick(() => {
|
||||
syncPersonTableSelection()
|
||||
})
|
||||
|
|
@ -600,10 +773,20 @@ onMounted(() => {
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.person-picker {
|
||||
.tab-content {
|
||||
min-height: 450px;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
height: 400px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.selected-persons {
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
|
|
|
|||
Loading…
Reference in New Issue