You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

379 lines
11 KiB

<template>
<div class="p-2">
<el-row :gutter="20">
<!-- 左侧:虚拟化组织机构树 -->
<el-col :lg="6" :xs="24">
<el-card shadow="hover" class="dept-tree-card">
<el-input
v-model="filterText"
placeholder="搜索单位名称,支持层级搜索如:南山/民政"
prefix-icon="Search"
clearable
class="mb-2"
/>
<el-tree-v2
ref="treeRef"
:data="treeData"
:props="treeProps"
:height="treeHeight"
:expand-on-click-node="false"
:default-expanded-keys="defaultExpandedKeys"
:filter-method="filterMethod"
highlight-current
@node-click="handleNodeClick"
>
<template #default="{ data }">
<span v-html="highlightText(data.label)"></span>
</template>
</el-tree-v2>
</el-card>
</el-col>
<!-- 右侧:用户表格 -->
<el-col :lg="18" :xs="24">
<el-card shadow="hover" class="mb-2">
<el-form :inline="true" :model="queryParams" ref="queryFormRef">
<el-form-item label="姓名" prop="fullname">
<el-input v-model="queryParams.fullname" placeholder="请输入姓名" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="用户名" prop="username">
<el-input v-model="queryParams.username" placeholder="请输入用户名" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="手机号" prop="mobilenumber">
<el-input v-model="queryParams.mobilenumber" placeholder="请输入手机号" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card shadow="hover">
<template #header>
<span v-if="selectedDeptName" class="font-bold">{{ selectedDeptName }} - 人员列表</span>
<span v-else class="text-gray-400">请选择左侧单位查看人员</span>
</template>
<el-table v-loading="loading" border :data="userList" empty-text="暂无数据">
<el-table-column label="姓名" prop="fullname" min-width="100" show-overflow-tooltip />
<el-table-column label="用户名" prop="username" min-width="120" show-overflow-tooltip />
<el-table-column label="手机号" prop="mobilenumber" min-width="130" />
<el-table-column label="性别" prop="gender" width="70" align="center" />
<el-table-column label="职务" prop="positionname" min-width="120" show-overflow-tooltip />
<el-table-column label="人员状态" prop="ryzt" width="100" align="center" />
<el-table-column label="邮箱" prop="email" min-width="160" show-overflow-tooltip />
</el-table>
<pagination
v-show="total > 0"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
:total="total"
@pagination="getUserList"
/>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts" name="SrzDeptUser">
import { getSrzDeptTreeList, listSrzUser } from '@/api/efcode/srz';
import type { SrzDeptMinVo, SrzUserQuery, SrzUserVo } from '@/api/efcode/srz/types';
interface TreeNode {
id: string;
label: string;
children?: TreeNode[];
zzlevel?: string;
}
const filterText = ref('');
const treeRef = ref();
const treeData = ref<TreeNode[]>([]);
const defaultExpandedKeys = ref<string[]>([]);
const treeHeight = ref(600);
const selectedDeptName = ref('');
const loading = ref(false);
const userList = ref<SrzUserVo[]>([]);
const total = ref(0);
const queryFormRef = ref<ElFormInstance>();
const treeProps = {
value: 'id',
label: 'label',
children: 'children'
};
const queryParams = reactive<SrzUserQuery>({
pageNum: 1,
pageSize: 20,
organizationid: undefined,
fullname: undefined,
username: undefined,
mobilenumber: undefined
});
// 保存扁平列表用于计算可见节点
const flatList = ref<SrzDeptMinVo[]>([]);
// 存储过滤后应该显示的节点ID集合
const visibleIds = ref<Set<string>>(new Set());
/**
* 计算可见节点集合,支持层级搜索(用 / 分隔)
* 例如 "南山/民政":先找含"南山"的节点,再在其子孙中找含"民政"的节点
*/
const computeVisibleIds = (query: string) => {
if (!query) {
visibleIds.value = new Set();
return;
}
// 构建 parentId -> childIds 映射 和 id -> parentId 映射
const childrenMap = new Map<string, string[]>();
const parentMap = new Map<string, string>();
const nameMap = new Map<string, string>();
for (const item of flatList.value) {
parentMap.set(item.organizationid, item.parentid);
nameMap.set(item.organizationid, item.name || '');
if (!childrenMap.has(item.parentid)) {
childrenMap.set(item.parentid, []);
}
childrenMap.get(item.parentid)!.push(item.organizationid);
}
// 获取某节点的所有子孙ID
const getAllDescendantIds = (id: string): string[] => {
const result: string[] = [];
const children = childrenMap.get(id) || [];
for (const childId of children) {
result.push(childId);
result.push(...getAllDescendantIds(childId));
}
return result;
};
// 按 / 分割搜索词,过滤空段
const segments = query.split('/').map(s => s.trim()).filter(s => s.length > 0);
if (segments.length === 0) {
visibleIds.value = new Set();
return;
}
// 逐层搜索
// candidateIds: 当前层搜索的候选范围(null表示全量)
let candidateIds: Set<string> | null = null;
for (const segment of segments) {
const matchedInThisLevel: string[] = [];
if (candidateIds === null) {
// 第一层:在全量中搜索
for (const item of flatList.value) {
if (item.name?.includes(segment)) {
matchedInThisLevel.push(item.organizationid);
}
}
} else {
// 后续层:在上一层匹配节点的子孙中搜索
for (const id of candidateIds) {
const descendants = getAllDescendantIds(id);
for (const descId of descendants) {
if (nameMap.get(descId)?.includes(segment)) {
matchedInThisLevel.push(descId);
}
}
}
}
// 当前层的匹配结果作为下一层的候选父节点
candidateIds = new Set(matchedInThisLevel);
}
// 最终结果:最后一层匹配的节点 + 子孙 + 祖先
const ids = new Set<string>();
const addDescendants = (id: string) => {
ids.add(id);
const children = childrenMap.get(id) || [];
for (const childId of children) {
addDescendants(childId);
}
};
const addAncestors = (id: string) => {
const pid = parentMap.get(id);
if (pid && !ids.has(pid)) {
ids.add(pid);
addAncestors(pid);
}
};
if (candidateIds) {
for (const id of candidateIds) {
addDescendants(id);
addAncestors(id);
}
}
visibleIds.value = ids;
};
/** 树节点过滤方法 */
const filterMethod = (query: string, node: TreeNode) => {
if (!query) return true;
return visibleIds.value.has(node.id);
};
/** 高亮匹配文本(高亮所有搜索段) */
const highlightText = (text: string) => {
if (!filterText.value || !text) return text;
const segments = filterText.value.split('/').map(s => s.trim()).filter(s => s.length > 0);
if (segments.length === 0) return text;
let result = text;
for (const segment of segments) {
const escaped = segment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`(${escaped})`, 'gi');
result = result.replace(regex, '<span class="tree-highlight">$1</span>');
}
return result;
};
/** 监听搜索框变化触发过滤 */
watch(filterText, (val) => {
computeVisibleIds(val);
treeRef.value?.filter(val);
});
/** 将扁平列表构建为树结构 */
const buildTree = (list: SrzDeptMinVo[]): TreeNode[] => {
const map = new Map<string, TreeNode>();
const roots: TreeNode[] = [];
// 先创建所有节点
for (const item of list) {
map.set(item.organizationid, {
id: item.organizationid,
label: item.name || '',
zzlevel: item.zzlevel,
children: []
});
}
// 建立父子关系
for (const item of list) {
const node = map.get(item.organizationid)!;
const parent = map.get(item.parentid);
if (parent) {
parent.children!.push(node);
} else {
roots.push(node);
}
}
// 移除空的children数组(叶子节点)
const removeEmptyChildren = (nodes: TreeNode[]) => {
for (const node of nodes) {
if (node.children && node.children.length === 0) {
delete node.children;
} else if (node.children) {
removeEmptyChildren(node.children);
}
}
};
removeEmptyChildren(roots);
return roots;
};
/** 收集前N层节点的id,用于默认展开 */
const collectExpandKeys = (nodes: TreeNode[], currentLevel: number, maxLevel: number) => {
if (currentLevel > maxLevel) return;
for (const node of nodes) {
defaultExpandedKeys.value.push(node.id);
if (node.children?.length) {
collectExpandKeys(node.children, currentLevel + 1, maxLevel);
}
}
};
/** 获取部门树数据 */
const getDeptTree = async () => {
const res = await getSrzDeptTreeList();
flatList.value = res.data;
const tree = buildTree(res.data);
treeData.value = tree;
defaultExpandedKeys.value = [];
collectExpandKeys(tree, 1, 2);
};
/** 节点点击事件 */
const handleNodeClick = (data: TreeNode) => {
selectedDeptName.value = data.label;
queryParams.organizationid = data.id;
queryParams.pageNum = 1;
getUserList();
};
/** 获取用户列表 */
const getUserList = async () => {
if (!queryParams.organizationid) {
userList.value = [];
total.value = 0;
return;
}
loading.value = true;
try {
const res = await listSrzUser(queryParams);
userList.value = res.rows;
total.value = res.total;
} finally {
loading.value = false;
}
};
/** 搜索按钮 */
const handleQuery = () => {
queryParams.pageNum = 1;
getUserList();
};
/** 重置按钮 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
queryParams.fullname = undefined;
queryParams.username = undefined;
queryParams.mobilenumber = undefined;
queryParams.pageNum = 1;
getUserList();
};
/** 计算树高度自适应视口 */
const calcTreeHeight = () => {
treeHeight.value = window.innerHeight - 180;
};
onMounted(() => {
calcTreeHeight();
window.addEventListener('resize', calcTreeHeight);
getDeptTree();
});
onUnmounted(() => {
window.removeEventListener('resize', calcTreeHeight);
});
</script>
<style scoped lang="scss">
.dept-tree-card {
position: sticky;
top: 0;
}
:deep(.tree-highlight) {
color: var(--el-color-primary);
font-weight: bold;
}
</style>