zhzf/client/src/components/OfficerSelector.vue

551 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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等用户点击确认按钮时再更新
}
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>
//