当前位置: 首页 > news >正文

基于若依框架编写的选人组件(vue3 + ts 版本)

先上效果图

多选未选中状态:

多选已选中状态:

单选未选中状态:

单选已选中状态:

废话不多说直接贴代码!

组件代码(template部分):

<template><div class="select-people-container"><!-- 单选模式的输入框显示 --><div v-if="!multiple" class="people-input" @click="handleOpen"><el-input :model-value="singleSelectedName" readonly :placeholder="placeholder" :disabled="disabled"><template #suffix><el-icon class="cursor-pointer text-primary"><User /></el-icon></template></el-input></div><!-- 多选模式的标签显示 --><div v-else class="people-tags" @click="handleOpen"><div class="tags-container" :class="{ 'disabled': disabled, 'has-tags': selectedPeople.length > 0 }"><el-tag v-for="person in selectedPeople" :key="person.userId" size="default" closable type="primary" effect="light"@close="handleRemovePerson(person)" @click.stop><el-icon class="mr-1"><User /></el-icon>{{ person.nickName }}</el-tag><span v-if="selectedPeople.length === 0" class="placeholder"><el-icon class="mr-1"><Plus /></el-icon>{{ placeholder }}</span><el-icon class="suffix-icon"><ArrowDown /></el-icon></div></div><!-- 选人对话框 --><el-dialog v-model="dialogVisible" :title="dialogTitle" width="80%" :before-close="handleCancel" append-to-bodydestroy-on-close class="select-people-dialog-wrapper"><div class="select-people-dialog"><el-row :gutter="24"><!-- 左侧部门树 --><el-col :span="6" class="dept-tree-col"><div class="section-card"><div class="section-header"><el-icon class="header-icon"><OfficeBuilding /></el-icon><span class="header-title">组织架构</span></div><div class="search-wrapper"><el-input v-model="deptSearchText" placeholder="搜索部门" clearable size="default"@input="handleDeptFilter"><template #prefix><el-icon class="search-icon"><Search /></el-icon></template></el-input></div><div class="tree-wrapper"><el-tree ref="deptTreeRef" v-loading="deptLoading" :data="deptOptions":props="{ label: 'label', children: 'children' }" node-key="id" :filter-node-method="filterDeptNode":expand-on-click-node="false" highlight-current default-expand-all @node-click="handleDeptClick"><template #default="{ node, data }"><span class="tree-node"><el-icon class="node-icon"><OfficeBuilding /></el-icon><span class="node-label">{{ node.label }}</span></span></template></el-tree></div></div></el-col><!-- 中间人员列表 --><el-col :span="12" class="people-list-col"><div class="section-card"><div class="section-header"><div class="header-left"><el-icon class="header-icon"><User /></el-icon><span class="header-title">人员列表</span><el-badge :value="peopleTotal" class="ml-2" type="info" /></div><div class="header-right"><el-input v-model="peopleSearchText" placeholder="搜索姓名或手机号" clearable size="default"class="search-input" @input="handlePeopleSearch"><template #prefix><el-icon class="search-icon"><Search /></el-icon></template></el-input></div></div><div class="table-wrapper"><el-table ref="peopleTableRef" v-loading="peopleLoading" :data="peopleList"size="default" :highlight-current-row="!multiple" row-key="userId"@current-change="handleSingleSelect" @selection-change="handleMultipleSelect"class="people-table" :height="null"><el-table-column v-if="multiple" type="selection" width="55" :reserve-selection="true" /><el-table-column type="index" width="60" label="序号" /><el-table-column prop="nickName" label="姓名" min-width="80"><template #default="{ row }"><div class="user-info"><el-avatar :size="32" class="mr-2"><el-icon><User /></el-icon></el-avatar><span class="user-name">{{ row.nickName }}</span></div></template></el-table-column><el-table-column prop="phonenumber" label="手机号" min-width="110"><template #default="{ row }"><div class="phone-info"><el-icon class="mr-1"><Phone /></el-icon>{{ row.phonenumber || '-' }}</div></template></el-table-column><el-table-column prop="deptName" label="部门" min-width="100" show-overflow-tooltip><template #default="{ row }"><el-tag size="small" type="info" effect="plain">{{ row.deptName || '-' }}</el-tag></template></el-table-column></el-table></div><!-- 分页 --><div class="pagination-wrapper"><el-pagination v-show="peopleTotal > 0" v-model:current-page="peopleQuery.pageNum"v-model:page-size="peopleQuery.pageSize" :total="peopleTotal"layout="total, sizes, prev, pager, next, jumper" :page-sizes="[10, 20, 50, 100]"@size-change="handlePeopleSizeChange" @current-change="handlePeopleCurrentChange"background size="small" /></div></div></el-col><!-- 右侧已选人员 --><el-col :span="6" class="selected-people-col"><div class="section-card"><div class="section-header"><div class="header-left"><el-icon class="header-icon"><Check /></el-icon><span class="header-title">已选人员</span><el-badge :value="tempSelectedPeople.length" class="ml-2" type="primary" /></div><el-button link size="small" @click="handleClearAll" class="clear-btn"><el-icon class="mr-1"><Delete /></el-icon>清空</el-button></div><div class="selected-list"><el-scrollbar><div v-for="person in tempSelectedPeople" :key="person.userId" class="selected-item"><div class="person-info"><el-avatar :size="28" class="person-avatar"><el-icon><User /></el-icon></el-avatar><div class="person-details"><div class="person-name">{{ person.nickName }}</div><div class="person-dept">{{ person.deptName }}</div></div></div><el-button link size="small" @click="handleRemoveFromSelected(person)" class="remove-btn"><el-icon><Close /></el-icon></el-button></div><div v-if="tempSelectedPeople.length === 0" class="empty-selected"><el-icon class="empty-icon"><User /></el-icon><div class="empty-text">暂无选中人员</div><div class="empty-desc">请从左侧选择人员</div></div></el-scrollbar></div></div></el-col></el-row></div><template #footer><div class="dialog-footer"><div class="footer-info"><el-icon class="info-icon"><InfoFilled /></el-icon><span>已选择 {{ tempSelectedPeople.length }} 人</span></div><div class="footer-actions"><el-button @click="handleCancel" size="default"><el-icon class="mr-1"><Close /></el-icon>取消</el-button><el-button type="primary" @click="handleConfirm" size="default"><el-icon class="mr-1"><Check /></el-icon>确定</el-button></div></div></template></el-dialog></div>
</template>

组件代码(script部分):

<script setup lang="ts" name="SelectPeople">
import { ref, reactive, computed, watch, nextTick, getCurrentInstance } from 'vue'
import { ElMessage } from 'element-plus'
import {User, Search, OfficeBuilding, Delete, Plus, ArrowDown,Phone, Check, Close, InfoFilled
} from '@element-plus/icons-vue'
import { listUser, deptTreeSelect } from '@/api/system/user'
import { UserVO, UserQuery } from '@/api/system/user/types'
import { DeptVO } from '@/api/system/dept/types'// Props定义
interface Props {modelValue?: UserVO | UserVO[] | string | string[]  // 支持多种数据格式multiple?: boolean                    // 是否多选placeholder?: string                  // 占位符disabled?: boolean                   // 是否禁用title?: string                       // 对话框标题limit?: number                       // 多选时的最大选择数量deptId?: string | number            // 指定部门ID,只显示该部门下的人员excludeUserIds?: (string | number)[] // 排除的用户ID列表
}const props = withDefaults(defineProps<Props>(), {multiple: false,placeholder: '请选择人员',disabled: false,title: '',limit: 0,deptId: '',excludeUserIds: () => []
})// Emits定义
const emit = defineEmits<{'update:modelValue': [value: UserVO | UserVO[] | string | string[] | undefined]'change': [value: UserVO | UserVO[], people: UserVO[]]
}>()const { proxy } = getCurrentInstance()!// 响应式数据
const dialogVisible = ref(false)
const deptLoading = ref(false)
const peopleLoading = ref(false)
const deptSearchText = ref('')
const peopleSearchText = ref('')// 部门相关
const deptTreeRef = ref()
const deptOptions = ref<DeptVO[]>([])
const currentDeptId = ref<string | number>('')// 人员相关
const peopleTableRef = ref()
const peopleList = ref<UserVO[]>([])
const peopleTotal = ref(0)
const peopleQuery = reactive<UserQuery>({pageNum: 1,pageSize: 20,nickName: '',phonenumber: '',deptId: ''
})// 选中的人员
const selectedPeople = ref<UserVO[]>([])          // 当前实际选中的人员
const tempSelectedPeople = ref<UserVO[]>([])      // 对话框中临时选中的人员// 方法定义(在监听器之前定义)
const initSelectedPeople = (value: any) => {if (!value) {selectedPeople.value = []return}if (props.multiple) {if (Array.isArray(value)) {// 如果是用户对象数组if (value.length > 0 && typeof value[0] === 'object') {selectedPeople.value = value as UserVO[]} else {// 如果是用户ID数组,需要根据ID获取用户信息selectedPeople.value = []// 这里可以根据需要调用API获取用户详细信息}}} else {if (typeof value === 'object') {selectedPeople.value = [value as UserVO]} else {// 如果是用户ID,需要根据ID获取用户信息selectedPeople.value = []}}
}const emitChange = () => {let value: anyif (props.multiple) {value = selectedPeople.value} else {value = selectedPeople.value.length > 0 ? selectedPeople.value[0] : undefined}emit('update:modelValue', value)emit('change', value, selectedPeople.value)
}const loadDeptTree = async () => {try {deptLoading.value = trueconst res = await deptTreeSelect()deptOptions.value = res.data// 如果指定了部门ID,设置为当前选中if (props.deptId) {currentDeptId.value = props.deptIdnextTick(() => {deptTreeRef.value?.setCurrentKey(props.deptId)})}} catch (error) {console.error('加载部门树失败:', error)ElMessage.error('加载部门信息失败')} finally {deptLoading.value = false}
}const setTableSelection = () => {if (props.multiple && peopleTableRef.value) {// 多选模式:设置复选框选中状态peopleList.value.forEach(person => {const isSelected = tempSelectedPeople.value.some(p => p.userId === person.userId)peopleTableRef.value.toggleRowSelection(person, isSelected)})} else {// 单选模式:设置当前行if (tempSelectedPeople.value.length > 0) {const selected = tempSelectedPeople.value[0]const person = peopleList.value.find(p => p.userId === selected.userId)if (person) {peopleTableRef.value?.setCurrentRow(person)}}}
}const loadPeopleList = async () => {try {peopleLoading.value = true// 设置查询参数const query = { ...peopleQuery }if (currentDeptId.value) {query.deptId = currentDeptId.value}if (props.deptId) {query.deptId = props.deptId}const res = await listUser(query)let list = res.rows || []// 排除指定的用户if (props.excludeUserIds.length > 0) {list = list.filter(user => !props.excludeUserIds.includes(user.userId))}peopleList.value = listpeopleTotal.value = res.total || 0// 设置表格选中状态nextTick(() => {setTableSelection()})} catch (error) {console.error('加载人员列表失败:', error)ElMessage.error('加载人员信息失败')} finally {peopleLoading.value = false}
}// 计算属性
const dialogTitle = computed(() => {return props.title || `人员选择${props.multiple ? '(多选)' : '(单选)'}`
})const singleSelectedName = computed(() => {if (!props.multiple && selectedPeople.value.length > 0) {const person = selectedPeople.value[0]return person.nickName}return ''
})// 监听器
watch(() => props.modelValue, (newVal) => {initSelectedPeople(newVal)
}, { immediate: true, deep: true })watch(() => props.deptId, (newVal) => {if (newVal) {currentDeptId.value = newValpeopleQuery.deptId = newVal}
})// 事件处理方法
const handleOpen = () => {if (props.disabled) return// 复制当前选中的人员到临时变量tempSelectedPeople.value = [...selectedPeople.value]dialogVisible.value = true// 加载数据loadDeptTree()loadPeopleList()
}const handleDeptClick = (data: DeptVO) => {currentDeptId.value = data.idpeopleQuery.deptId = data.idpeopleQuery.pageNum = 1loadPeopleList()
}const handleDeptFilter = (value: string) => {deptTreeRef.value?.filter(value)
}const filterDeptNode = (value: string, data: any) => {if (!value) return true// 支持label或deptName属性const name = data.label || data.deptName || ''return name.includes(value)
}const handlePeopleSearch = () => {peopleQuery.pageNum = 1if (peopleSearchText.value) {// 简单判断是否为手机号if (/^\d+$/.test(peopleSearchText.value)) {peopleQuery.phonenumber = peopleSearchText.valuepeopleQuery.nickName = ''} else {peopleQuery.nickName = peopleSearchText.valuepeopleQuery.phonenumber = ''}} else {peopleQuery.nickName = ''peopleQuery.phonenumber = ''}loadPeopleList()
}const handleSingleSelect = (currentRow: UserVO) => {if (!props.multiple && currentRow) {tempSelectedPeople.value = [currentRow]}
}const handleMultipleSelect = (selection: UserVO[]) => {if (props.multiple) {// 检查数量限制if (props.limit > 0 && selection.length > props.limit) {ElMessage.warning(`最多只能选择 ${props.limit} 个人员`)// 移除超出的选择nextTick(() => {const excess = selection.slice(props.limit)excess.forEach(person => {peopleTableRef.value.toggleRowSelection(person, false)})})return}tempSelectedPeople.value = selection}
}const handlePeopleSizeChange = (size: number) => {peopleQuery.pageSize = sizepeopleQuery.pageNum = 1loadPeopleList()
}const handlePeopleCurrentChange = (page: number) => {peopleQuery.pageNum = pageloadPeopleList()
}const handleRemoveFromSelected = (person: UserVO) => {const index = tempSelectedPeople.value.findIndex(p => p.userId === person.userId)if (index > -1) {tempSelectedPeople.value.splice(index, 1)// 同时更新表格选中状态nextTick(() => {peopleTableRef.value?.toggleRowSelection(person, false)})}
}const handleRemovePerson = (person: UserVO) => {const index = selectedPeople.value.findIndex(p => p.userId === person.userId)if (index > -1) {selectedPeople.value.splice(index, 1)emitChange()}
}const handleClearAll = () => {tempSelectedPeople.value = []// 清空表格选中状态nextTick(() => {if (props.multiple) {peopleTableRef.value?.clearSelection()} else {peopleTableRef.value?.setCurrentRow()}})
}const handleConfirm = () => {if (!props.multiple && tempSelectedPeople.value.length === 0) {ElMessage.warning('请选择一个人员')return}selectedPeople.value = [...tempSelectedPeople.value]emitChange()dialogVisible.value = false
}const handleCancel = () => {// 恢复原来的选择tempSelectedPeople.value = [...selectedPeople.value]dialogVisible.value = false
}// 对外暴露的方法
defineExpose({open: handleOpen,clear: () => {selectedPeople.value = []emitChange()}
})
</script>

组件代码(style部分)

<style lang="scss" scoped>
.select-people-container {width: 100%;
}.people-input {:deep(.el-input) {cursor: pointer;transition: all 0.3s ease;&:hover {border-color: var(--el-color-primary);box-shadow: 0 0 0 1px var(--el-color-primary-light-7);}input {cursor: pointer;}}.text-primary {color: var(--el-color-primary);}
}.people-tags {.tags-container {min-height: 40px;border: 2px solid var(--el-border-color-light);border-radius: 8px;padding: 8px 40px 8px 12px;position: relative;cursor: pointer;transition: all 0.3s ease;background: var(--el-fill-color-blank);&:hover {border-color: var(--el-color-primary);box-shadow: 0 0 0 1px var(--el-color-primary-light-7);}&.disabled {background-color: var(--el-disabled-bg-color);cursor: not-allowed;opacity: 0.6;&:hover {border-color: var(--el-border-color-light);box-shadow: none;}}&.has-tags {display: flex;flex-wrap: wrap;gap: 8px;align-items: center;}.placeholder {color: var(--el-text-color-placeholder);font-size: 14px;display: flex;align-items: center;}.suffix-icon {position: absolute;right: 12px;top: 50%;transform: translateY(-50%);color: var(--el-text-color-regular);transition: transform 0.3s ease;}&:hover .suffix-icon {transform: translateY(-50%) rotate(180deg);}}
}:deep(.select-people-dialog-wrapper) {.el-dialog {border-radius: 16px;overflow: hidden;}.el-dialog__header {background: linear-gradient(135deg, var(--el-color-primary) 0%, var(--el-color-primary-light-3) 100%);color: white;padding: 20px 24px;margin: 0;.el-dialog__title {font-size: 18px;font-weight: 600;}.el-dialog__headerbtn {.el-dialog__close {color: white;font-size: 18px;&:hover {color: var(--el-color-primary-light-7);}}}}.el-dialog__body {padding: 24px;background: var(--el-bg-color-page);}.el-dialog__footer {padding: 20px 24px;background: var(--el-fill-color-blank);border-top: 1px solid var(--el-border-color-light);}
}.select-people-dialog {.section-card {background: white;border-radius: 12px;box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);overflow: hidden;transition: all 0.3s ease;height: 520px;display: flex;flex-direction: column;&:hover {box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);}}.section-header {background: linear-gradient(135deg, var(--el-fill-color-light) 0%, var(--el-fill-color-blank) 100%);padding: 16px 20px;border-bottom: 1px solid var(--el-border-color-lighter);display: flex;justify-content: space-between;align-items: center;flex-shrink: 0;.header-left {display: flex;align-items: center;}.header-icon {font-size: 16px;color: var(--el-color-primary);margin-right: 8px;}.header-title {font-weight: 600;font-size: 15px;color: var(--el-text-color-primary);}.search-input {width: 220px;}.clear-btn {color: var(--el-color-danger);font-weight: 500;&:hover {background: var(--el-color-danger-light-9);}}}.search-wrapper {padding: 16px 20px;background: var(--el-fill-color-blank);flex-shrink: 0;.search-icon {color: var(--el-text-color-regular);}}.tree-wrapper {padding: 0 20px 20px;flex: 1;overflow: hidden;display: flex;flex-direction: column;:deep(.el-tree) {background: transparent;flex: 1;overflow: auto;.el-tree-node {margin-bottom: 2px;.el-tree-node__content {height: 36px;border-radius: 6px;transition: all 0.3s ease;&:hover {background: var(--el-color-primary-light-9);}&.is-current {background: var(--el-color-primary-light-8);color: var(--el-color-primary);font-weight: 500;}}}}.tree-node {display: flex;align-items: center;width: 100%;.node-icon {font-size: 14px;color: var(--el-color-primary);margin-right: 6px;}.node-label {font-size: 14px;color: var(--el-text-color-primary);}}}.table-wrapper {padding: 0 20px;flex: 1;overflow: hidden;display: flex;flex-direction: column;.people-table {border-radius: 8px;overflow: hidden;flex: 1;min-height: 0;:deep(.el-table) {height: 100% !important;}:deep(.el-table__body-wrapper) {overflow: auto;}:deep(.el-table__header) {background: var(--el-fill-color-light);th {background: var(--el-fill-color-light);color: var(--el-text-color-primary);font-weight: 600;}}:deep(.el-table__row) {transition: all 0.3s ease;&:hover {background: var(--el-color-primary-light-9);}}.user-info {display: flex;align-items: center;.user-name {font-weight: 500;color: var(--el-text-color-primary);}}.phone-info {display: flex;align-items: center;color: var(--el-text-color-regular);font-size: 13px;}}}.pagination-wrapper {padding: 16px 20px;display: flex;justify-content: center;background: var(--el-fill-color-blank);border-top: 1px solid var(--el-border-color-lighter);flex-shrink: 0;}.selected-list {padding: 20px;flex: 1;overflow: hidden;display: flex;flex-direction: column;:deep(.el-scrollbar) {flex: 1;}.selected-item {display: flex;justify-content: space-between;align-items: center;padding: 12px;background: var(--el-fill-color-blank);border: 1px solid var(--el-border-color-lighter);border-radius: 8px;margin-bottom: 8px;transition: all 0.3s ease;&:hover {border-color: var(--el-color-primary-light-5);box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);transform: translateY(-1px);}.person-info {display: flex;align-items: center;flex: 1;.person-avatar {margin-right: 10px;background: var(--el-color-primary-light-8);color: var(--el-color-primary);}.person-details {.person-name {font-weight: 500;color: var(--el-text-color-primary);margin-bottom: 2px;font-size: 14px;}.person-dept {font-size: 12px;color: var(--el-text-color-secondary);}}}.remove-btn {color: var(--el-color-danger);opacity: 0.7;transition: all 0.3s ease;&:hover {opacity: 1;background: var(--el-color-danger-light-9);}}}.empty-selected {text-align: center;padding: 60px 20px;color: var(--el-text-color-placeholder);flex: 1;display: flex;flex-direction: column;justify-content: center;align-items: center;.empty-icon {font-size: 48px;color: var(--el-color-info-light-5);margin-bottom: 16px;}.empty-text {font-size: 16px;margin-bottom: 8px;color: var(--el-text-color-regular);}.empty-desc {font-size: 12px;color: var(--el-text-color-placeholder);}}}
}.dialog-footer {display: flex;justify-content: space-between;align-items: center;.footer-info {display: flex;align-items: center;color: var(--el-text-color-regular);font-size: 14px;.info-icon {margin-right: 6px;color: var(--el-color-primary);}}.footer-actions {display: flex;gap: 12px;.el-button {min-width: 80px;border-radius: 6px;font-weight: 500;transition: all 0.3s ease;&:not(.el-button--primary):hover {transform: translateY(-1px);box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);}&.el-button--primary {background: linear-gradient(135deg, var(--el-color-primary) 0%, var(--el-color-primary-light-3) 100%);border: none;&:hover {transform: translateY(-1px);box-shadow: 0 4px 12px rgba(var(--el-color-primary), 0.4);}}}}
}// 响应式设计
@media (max-width: 1200px) {:deep(.select-people-dialog-wrapper) {.el-dialog {width: 90% !important;}}
}// 动画效果
.selected-item {animation: slideInRight 0.3s ease;
}@keyframes slideInRight {from {opacity: 0;transform: translateX(20px);}to {opacity: 1;transform: translateX(0);}
}// 滚动条美化
:deep(.el-scrollbar__bar) {&.is-vertical {.el-scrollbar__thumb {background: var(--el-color-primary-light-5);border-radius: 4px;}}
}
</style>

父组件调用方法(直接贴图吧):

相关文章:

  • PostgreSQL窗口函数测试
  • Docker 安装 Oracle 11G
  • Datawhale-爬虫
  • 论文笔记:Trajectory generation: a survey on methods and techniques
  • 【数据结构】图论实战:DAG空间压缩术——42%存储优化实战解析
  • Java线程池全面解析:原理、实现与最佳实践
  • 视频点播web端AI智能大纲(自动生成视频内容大纲)的代码与演示
  • APISIX 简介:云原生 API 网关的架构与实践
  • 提升教学演示效率:基于交互设计的电子教鞭解决方案
  • 【RocketMQ 生产者和消费者】- 消费者重平衡(1)
  • Scale AI 的王晓磊带着对整个 AI 行业动态的深入了解加入 Meta
  • Javascript的新能力:显式资源管理(Explicit Resource Management)
  • Flask入门指南:从零构建Python微服务
  • WinForms视频播放开发实战指南
  • 公钥加密与签名算法计算详解(含计算题例子)
  • 股票T0程序化交易如何做?
  • MySQL的Sql优化经验总结
  • 【配置教程】新版OpenCV+Android Studio环境配置(4.11测试通过)
  • MySQL 中 DISTINCT 去重的核心注意事项详解
  • 【沉浸式解决问题】Mysql中union连接的子查询是否并行执行
  • 广州市政府网站建设与管理规范/收录入口在线提交
  • 动态网站的功能与特点/关键词优化公司前十排名
  • 建设游戏网站/长沙靠谱seo优化价格
  • asp网站开门/百度高级搜索功能
  • 做网站都有那些步骤/南宁网络推广平台
  • 中天建设第四网站/关键词排名提高