zhzf/client/src/components/OfficerSelector.vue

551 lines
16 KiB
Vue
Raw Normal View History

2025-02-21 11:25:09 +08:00
<template>
<div class="officer-selector-container">
<!-- 触发下拉框的输入框 -->
<el-popover
v-model:visible="popoverVisible"
placement="top-start"
:width="800"
trigger="click"
popper-class="officer-selector-popover"
:hide-after="0"
>
<!-- 下拉内容人员选择面板 -->
<template #default>
<el-card class="department-personnel-selector" shadow="never">
<div class="selector-content">
<el-row :gutter="20">
<!-- 部门树 -->
<el-col :span="8">
<el-card shadow="never" class="department-tree-card">
<template #header>
<div class="department-header">
<span>部门结构</span>
<el-button v-if="selectedDepartment" type="text" size="small" @click="selectedDepartment = null">
清除
</el-button>
</div>
</template>
<el-scrollbar height="300px">
<el-tree
:data="departmentsTree"
node-key="id"
:props="{
label: 'name',
children: 'children',
}"
:highlight-current="true"
@node-click="handleDepartmentSelect"
>
<template #default="{ node, data }">
<div class="custom-tree-node">
<span>{{ node.label }}</span>
<el-tag size="small" type="info" effect="plain">
{{ countPeopleInDepartment(data.path) }}
</el-tag>
</div>
</template>
</el-tree>
</el-scrollbar>
</el-card>
</el-col>
<!-- 人员列表 -->
<el-col :span="16">
<el-card shadow="never" class="personnel-list-card">
<div class="search-section">
<div class="search-row">
<el-input v-model="searchTerm" placeholder="搜索人员、职位或部门..." clearable>
<template #prefix>
<font-awesome-icon icon="fa-search" />
</template>
</el-input>
<!-- 全选和清空按钮 -->
<div class="selection-actions">
<el-button
type="primary"
size="small"
:disabled="filteredPeople.length === 0"
@click.stop="selectAll"
plain
>
<font-awesome-icon icon="fa-check-double" class="action-icon" />
全选
</el-button>
<el-button
type="danger"
size="small"
:disabled="selectedPeople.length === 0"
@click.stop="clearSelection"
plain
>
<font-awesome-icon icon="fa-times" class="action-icon" />
清空
</el-button>
</div>
</div>
<div v-if="selectedDepartment" class="selected-department">
<span class="label">当前部门:</span>
<el-tag closable @close="selectedDepartment = null">
{{ selectedDepartment.name }}
</el-tag>
</div>
</div>
<el-scrollbar height="300px" class="personnel-scrollbar">
<el-empty v-if="loading" description="加载中..." />
<el-empty v-else-if="filteredPeople.length === 0" description="未找到匹配的人员" />
<div v-else class="personnel-list">
<div
v-for="person in filteredPeople"
:key="person.id"
class="personnel-item"
:class="{ 'is-selected': isPersonSelected(person.id) }"
@click="togglePerson(person)"
>
<el-avatar :size="40" :src="person.image">
{{ person.name.slice(0, 1) }}
</el-avatar>
<div class="personnel-info">
<div class="personnel-name">{{ person.name }}</div>
<div class="personnel-details">
<el-tag size="small" effect="plain">{{ person.roleName || '未知角色' }}</el-tag>
<el-divider direction="vertical" />
<el-tooltip :content="person.departmentName" placement="top">
<span class="department-path">{{ person.departmentName }}</span>
</el-tooltip>
</div>
</div>
<font-awesome-icon v-if="isPersonSelected(person.id)" class="check-icon" icon="fa-check" />
</div>
</div>
</el-scrollbar>
</el-card>
</el-col>
</el-row>
<!-- 已选择人员 -->
<div v-if="selectedPeople.length > 0" class="selected-personnel">
<h3>已选择 ({{ selectedPeople.length }})</h3>
<div class="selected-tags">
<el-tag
v-for="person in selectedPeople"
:key="person.id"
closable
@close="removePerson(person.id)"
class="personnel-tag"
>
{{ person.name }}
<span class="department-label">({{ person.departmentName }} · {{ person.roleName || '未知角色' }})</span>
</el-tag>
</div>
</div>
<el-button
type="primary"
class="confirm-button"
@click.stop="confirmSelection"
>
确认选择 ({{ selectedPeople.length }})
</el-button>
</div>
</el-card>
</template>
<!-- 触发元素 -->
<template #reference>
<div class="reference-input">
<el-input
readonly
:placeholder="placeholder"
:value="displayValue"
@click.stop="handleInputClick"
class="selector-input"
>
<template #suffix>
<font-awesome-icon icon="fa-chevron-down" />
</template>
</el-input>
</div>
</template>
</el-popover>
</div>
</template>
<script setup>
import { ref, computed, defineProps, defineEmits, onMounted } from 'vue'
import { agencies } from '../api/lawenforcement/Agency'
import { officers } from '../api/lawenforcement/Officer'
// 定义props和emits
const props = defineProps({
modelValue: {
type: Array,
default: () => [],
},
placeholder: {
type: String,
default: '请选择人员',
},
agencyId:{
type: String,
default: '1'
}
})
const emit = defineEmits(['update:modelValue'])
// 弹出框可见性
const popoverVisible = ref(false)
// 处理输入框点击事件
function handleInputClick() {
popoverVisible.value = true
}
// 部门树型结构 - 使用API获取的机构数据
const departmentsTree = ref([])
const loading = ref(false)
// 人员数据 - 从API获取
const people = ref([])
// 响应式状态
// 使用props.modelValue初始化selectedPeople确保与v-model同步
const selectedPeople = ref([...props.modelValue])
const selectedDepartment = ref(null)
const searchTerm = ref('')
// 获取机构树和人员数据
onMounted(async () => {
try {
loading.value = true
// 获取机构树
const agencyResponse = await agencies.tree()
if (agencyResponse.data && agencyResponse.data.length > 0) {
// 处理机构树数据,确保字段名称正确
departmentsTree.value = processAgencyTree(agencyResponse.data)
}
// 加载所有人员数据
await loadAllOfficers()
} catch (error) {
console.error('加载机构和人员数据失败:', error)
} finally {
loading.value = false
}
})
// 处理机构树数据,确保字段名称与组件期望的一致
function processAgencyTree(agencies) {
return agencies.map(agency => ({
id: agency.agencyId, // 使用 agencyId 作为唯一标识
agencyId: agency.agencyId, // 保留原始 agencyId 字段
name: agency.agencyName, // 使用 agencyName 作为显示名称
path: agency.agencyPath || agency.agencyName, // 使用 agencyPath 或 agencyName 作为路径
code: agency.agencyCode, // 使用 agencyCode 作为代码
level: agency.agencyLevel, // 使用 agencyLevel 作为级别
leaf: agency.leaf, // 使用 leaf 表示是否为叶节点
parentId: agency.parent?.agencyId || '0', // 使用父节点的 agencyId 作为 parentId
// 递归处理子节点
children: agency.children && agency.children.length > 0 ? processAgencyTree(agency.children) : []
}))
}
// 直接加载所有执法人员数据
async function loadAllOfficers() {
try {
loading.value = true
// 使用officers.querylist接口直接查询全部执法人员信息
const response = await officers.querylist()
if (response.success && response.data) {
// 处理返回的数据,确保数据格式一致
const officersData = response.data.map(officer => ({
id: officer.officerId,
name: officer.officerName,
role: officer.role || 'zfry',
roleName: officer.roleName || '执法人员',
departmentName: officer.agency?.agencyName || '未知部门',
departmentPath: officer.agency?.agencyPath || '',
certificateNo: officer.certificateNo || '',
avatar: officer.avatar || ''
}))
// 更新人员列表,确保不重复
const uniqueOfficers = officersData.filter(newOfficer =>
!people.value.some(existingOfficer => existingOfficer.id === newOfficer.id)
)
people.value = [...people.value, ...uniqueOfficers]
}
} catch (error) {
console.error('加载所有人员数据失败:', error)
} finally {
loading.value = false
}
}
// 计算属性:根据选中的部门和搜索词过滤人员
const filteredPeople = computed(() => {
return people.value.filter((person) => {
const matchesSearch =
searchTerm.value === '' ||
person.name.toLowerCase().includes(searchTerm.value.toLowerCase()) ||
person.role.toLowerCase().includes(searchTerm.value.toLowerCase()) ||
person.departmentName.toLowerCase().includes(searchTerm.value.toLowerCase())
const matchesDepartment = !selectedDepartment.value || person.departmentPath.startsWith(selectedDepartment.value.path)
return matchesSearch && matchesDepartment
})
})
// 计算显示值
const displayValue = computed(() => {
if (!props.modelValue || props.modelValue.length === 0) return ''
if (props.modelValue.length === 1) return props.modelValue[0].name
return `已选择 ${props.modelValue.length}`
})
// 方法
function handleDepartmentSelect(data) {
// 只设置选中的部门,不再加载数据
selectedDepartment.value = data
}
// 确认选择更新v-model并关闭弹窗
function confirmSelection(e) {
emit('update:modelValue', [...selectedPeople.value])
popoverVisible.value = false
// 阻止事件冒泡
if (e) e.stopPropagation()
}
function togglePerson(person) {
const index = selectedPeople.value.findIndex((p) => p.id === person.id)
if (index === -1) {
selectedPeople.value.push(person)
} else {
selectedPeople.value.splice(index, 1)
}
// 不需要在这里emit事件只在确认选择时更新父组件
}
function removePerson(personId) {
const index = selectedPeople.value.findIndex((p) => p.id === personId)
if (index !== -1) {
selectedPeople.value.splice(index, 1)
}
}
// 全选当前筛选出的人员
function selectAll() {
// 将所有筛选出的人员添加到已选择列表中(避免重复)
filteredPeople.value.forEach((person) => {
if (!isPersonSelected(person.id)) {
selectedPeople.value.push(person)
}
})
// 不立即更新v-model等用户点击确认按钮时再更新
}
2025-02-21 11:25:09 +08:00
function isPersonSelected(personId) {
return selectedPeople.value.some((p) => p.id === personId)
}
function countPeopleInDepartment(departmentPath) {
return people.value.filter((p) => p.departmentPath.startsWith(departmentPath)).length
}
</script>
<style scoped>
.officer-selector-container {
width: 100%;
position: relative;
}
.selector-input {
width: 100%;
}
.selected-tags-preview {
margin-top: 8px;
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.reference-input {
width: 100%;
}
:deep(.officer-selector-popover) {
max-width: 800px;
padding: 0;
}
.department-personnel-selector {
width: 100%;
max-width: 1200px;
margin: 0 auto;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-header h2 {
font-size: 18px;
margin: 0;
}
.selector-content {
display: flex;
flex-direction: column;
gap: 20px;
}
.department-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.custom-tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-right: 8px;
}
.search-section {
margin-bottom: 16px;
}
.search-row {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
.selection-actions {
display: flex;
gap: 5px;
white-space: nowrap;
}
.action-icon {
margin-right: 4px;
}
.selected-department {
margin-top: 10px;
display: flex;
align-items: center;
gap: 8px;
}
.selected-department .label {
font-size: 13px;
color: #909399;
}
.personnel-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.personnel-item {
display: flex;
align-items: center;
padding: 10px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
position: relative;
}
.personnel-item:hover {
background-color: #f5f7fa;
}
.personnel-item.is-selected {
background-color: #ecf5ff;
}
.personnel-info {
margin-left: 12px;
flex: 1;
min-width: 0;
}
.personnel-name {
font-weight: 500;
margin-bottom: 4px;
}
.personnel-details {
font-size: 12px;
color: #909399;
display: flex;
align-items: center;
}
.department-path {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 200px;
display: inline-block;
}
.check-icon {
color: #409eff;
font-size: 16px;
margin-left: 8px;
}
.selected-personnel {
margin-top: 16px;
}
.selected-personnel h3 {
font-size: 14px;
margin-bottom: 10px;
}
.selected-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.personnel-tag {
display: flex;
align-items: center;
gap: 4px;
}
.department-label {
font-size: 12px;
color: #909399;
}
.confirm-button {
margin-top: 16px;
width: 100%;
}
</style>
//