Browse Source

修改市认证机构树功能

dev-V1.0.1
庄锐波 6 months ago
parent
commit
20c17b8784
  1. 16
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/controller/SrzDeptController.java
  2. 8
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/controller/SrzUserController.java
  3. 2
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/SrzDeptServiceImpl.java
  4. 4
      zdxt-web-client/zdxt-admin-plus-ui/src/api/efcode/srz/index.ts
  5. 380
      zdxt-web-client/zdxt-admin-plus-ui/src/views/efcode/components/srz-dept-full-load-tree.vue
  6. 379
      zdxt-web-client/zdxt-admin-plus-ui/src/views/efcode/srz/index.vue

16
zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/controller/SrzDeptController.java

@ -32,7 +32,7 @@ import org.dromara.common.mybatis.core.page.TableDataInfo;
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/enforcementcode/dept")
@RequestMapping("/enforcementcode/srz_dept")
public class SrzDeptController extends BaseController {
private final ISrzDeptService srzDeptService;
@ -40,7 +40,7 @@ public class SrzDeptController extends BaseController {
/**
* 获取全量组织机构精简列表(用于前端虚拟化树展示)
*/
@SaCheckPermission("enforcementcode:dept:list")
@SaCheckPermission("enforcementcode:srz_dept:list")
@GetMapping("/treeList")
public R<List<SrzDeptMinVo>> treeList() {
return R.ok(srzDeptService.queryMinList());
@ -49,7 +49,7 @@ public class SrzDeptController extends BaseController {
/**
* 查询市认证对接---组织机构信息列表
*/
@SaCheckPermission("enforcementcode:dept:list")
@SaCheckPermission("enforcementcode:srz_dept:list")
@GetMapping("/list")
public TableDataInfo<SrzDeptVo> list(SrzDeptBo bo, PageQuery pageQuery) {
return srzDeptService.queryPageList(bo, pageQuery);
@ -58,7 +58,7 @@ public class SrzDeptController extends BaseController {
/**
* 导出市认证对接---组织机构信息列表
*/
@SaCheckPermission("enforcementcode:dept:export")
@SaCheckPermission("enforcementcode:srz_dept:export")
@Log(title = "市认证对接---组织机构信息", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(SrzDeptBo bo, HttpServletResponse response) {
@ -71,7 +71,7 @@ public class SrzDeptController extends BaseController {
*
* @param organizationid 主键
*/
@SaCheckPermission("enforcementcode:dept:query")
@SaCheckPermission("enforcementcode:srz_dept:query")
@GetMapping("/{organizationid}")
public R<SrzDeptVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable String organizationid) {
@ -81,7 +81,7 @@ public class SrzDeptController extends BaseController {
/**
* 新增市认证对接---组织机构信息
*/
@SaCheckPermission("enforcementcode:dept:add")
@SaCheckPermission("enforcementcode:srz_dept:add")
@Log(title = "市认证对接---组织机构信息", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
@ -92,7 +92,7 @@ public class SrzDeptController extends BaseController {
/**
* 修改市认证对接---组织机构信息
*/
@SaCheckPermission("enforcementcode:dept:edit")
@SaCheckPermission("enforcementcode:srz_dept:edit")
@Log(title = "市认证对接---组织机构信息", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
@ -105,7 +105,7 @@ public class SrzDeptController extends BaseController {
*
* @param organizationids 主键串
*/
@SaCheckPermission("enforcementcode:dept:remove")
@SaCheckPermission("enforcementcode:srz_dept:remove")
@Log(title = "市认证对接---组织机构信息", businessType = BusinessType.DELETE)
@DeleteMapping("/{organizationids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")

8
zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/controller/SrzUserController.java

@ -31,7 +31,7 @@ import org.dromara.common.mybatis.core.page.TableDataInfo;
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/enforcementcode/user")
@RequestMapping("/enforcementcode/srz_user")
public class SrzUserController extends BaseController {
private final ISrzUserService srzUserService;
@ -39,7 +39,7 @@ public class SrzUserController extends BaseController {
/**
* 查询市认证用户原始列表
*/
@SaCheckPermission("enforcementcode:user:list")
@SaCheckPermission("enforcementcode:srz_user:list")
@GetMapping("/list")
public TableDataInfo<SrzUserVo> list(SrzUserBo bo, PageQuery pageQuery) {
return srzUserService.queryPageList(bo, pageQuery);
@ -48,7 +48,7 @@ public class SrzUserController extends BaseController {
/**
* 导出市认证用户原始列表
*/
@SaCheckPermission("enforcementcode:user:export")
@SaCheckPermission("enforcementcode:srz_user:export")
@Log(title = "市认证用户原始", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(SrzUserBo bo, HttpServletResponse response) {
@ -61,7 +61,7 @@ public class SrzUserController extends BaseController {
*
* @param userid 主键
*/
@SaCheckPermission("enforcementcode:user:query")
@SaCheckPermission("enforcementcode:srz_user:query")
@GetMapping("/{userid}")
public R<SrzUserVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable String userid) {

2
zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/SrzDeptServiceImpl.java

@ -177,7 +177,7 @@ public class SrzDeptServiceImpl implements ISrzDeptService {
LambdaQueryWrapper<SrzDept> lqw = Wrappers.lambdaQuery();
lqw.select(SrzDept::getOrganizationid, SrzDept::getName, SrzDept::getParentid,
SrzDept::getZzlevel, SrzDept::getZzisleaf);
lqw.orderByAsc(SrzDept::getSequence);
lqw.orderByAsc(SrzDept::getZzlevel).orderByAsc(SrzDept::getSequence);
List<SrzDept> list = baseMapper.selectList(lqw);
return list.stream().map(dept -> {
SrzDeptMinVo vo = new SrzDeptMinVo();

4
zdxt-web-client/zdxt-admin-plus-ui/src/api/efcode/srz/index.ts

@ -7,7 +7,7 @@ import { SrzDeptMinVo, SrzUserQuery, SrzUserVo } from './types';
*/
export const getSrzDeptTreeList = (): AxiosPromise<SrzDeptMinVo[]> => {
return request({
url: '/enforcementcode/dept/treeList',
url: '/enforcementcode/srz_dept/treeList',
method: 'get'
});
};
@ -17,7 +17,7 @@ export const getSrzDeptTreeList = (): AxiosPromise<SrzDeptMinVo[]> => {
*/
export const listSrzUser = (query: SrzUserQuery) => {
return request({
url: '/enforcementcode/user/list',
url: '/enforcementcode/srz_user/list',
method: 'get',
params: query
});

380
zdxt-web-client/zdxt-admin-plus-ui/src/views/efcode/components/srz-dept-full-load-tree.vue

@ -0,0 +1,380 @@
<template>
<el-card shadow="hover" class="dept-tree-card">
<div class="search-row">
<el-input
v-model="filterText"
placeholder="搜索单位名称"
prefix-icon="Search"
clearable
/>
<el-popover placement="bottom-end" :width="360" trigger="hover">
<template #reference>
<el-button :icon="QuestionFilled" link class="help-btn" />
</template>
<div class="search-help">
<p class="help-title">搜索语法说明</p>
<el-divider style="margin: 8px 0" />
<ul class="help-list">
<li><code>福田</code> 查找含"福田"的节点</li>
<li><code>福田+</code> 查找含"福田"的节点,并显示其所有子孙</li>
<li><code>福田/民政</code> 先找"福田",再在其下级中找"民政"</li>
<li><code>福田+/民政</code> 找"福田"并展开子孙,再在其中找"民政"</li>
<li><code>福田+/民政+</code> 同上,且"民政"也展开子孙</li>
</ul>
<el-divider style="margin: 8px 0" />
<p class="help-note"><b>/</b> 分隔层级,<b>+</b> 后缀展开子孙节点</p>
</div>
</el-popover>
</div>
<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>
</template>
<script setup lang="ts" name="SrzDeptFullLoadTree">
import { QuestionFilled } from '@element-plus/icons-vue';
import { getSrzDeptTreeList } from '@/api/efcode/srz';
import type { SrzDeptMinVo } from '@/api/efcode/srz/types';
interface TreeNode {
id: string;
label: string;
children?: TreeNode[];
zzlevel?: string;
}
const emit = defineEmits<{
(e: 'node-click', data: { id: string; label: string }): void;
}>();
const filterText = ref('');
const treeRef = ref();
const treeData = ref<TreeNode[]>([]);
const defaultExpandedKeys = ref<string[]>([]);
const treeHeight = ref(600);
const treeProps = {
value: 'id',
label: 'label',
children: 'children'
};
// 保存扁平列表用于计算可见节点
const flatList = ref<SrzDeptMinVo[]>([]);
// 存储过滤后应该显示的节点ID集合
const visibleIds = ref<Set<string>>(new Set());
/** 解析搜索段:提取关键词和是否带 + 后缀 */
interface SearchSegment {
keyword: string;
showDescendants: boolean;
}
const parseSegments = (query: string): SearchSegment[] => {
return query.split('/').map(s => s.trim()).filter(s => s.length > 0).map(s => {
if (s.endsWith('+')) {
return { keyword: s.slice(0, -1), showDescendants: true };
}
return { keyword: s, showDescendants: false };
}).filter(s => s.keyword.length > 0);
};
/**
* 计算可见节点集合,支持层级搜索(/ 分隔)和子孙展开(+ 后缀)
* - 福田:仅显示匹配节点 + 祖先
* - 福田+:显示匹配节点 + 所有子孙 + 祖先
* - 福田/民政:在福田子孙中找民政,显示民政匹配 + 祖先
* - 福田+/民政:福田全部子孙可见,在其中找民政匹配 + 祖先
* - 福田+/民政+:福田全部子孙可见,民政匹配 + 民政子孙 + 祖先
*/
const computeVisibleIds = (query: string) => {
if (!query) {
visibleIds.value = new Set();
return;
}
const segments = parseSegments(query);
if (segments.length === 0) {
visibleIds.value = new Set();
return;
}
// 构建映射
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 stack = [id];
while (stack.length) {
const current = stack.pop()!;
const children = childrenMap.get(current) || [];
for (const childId of children) {
result.push(childId);
stack.push(childId);
}
}
return result;
};
// 逐层搜索,记录每层匹配结果
const levelMatches: string[][] = [];
let scopeIds: Set<string> | null = null; // null = 全量
for (const segment of segments) {
const matched: string[] = [];
if (scopeIds === null) {
for (const item of flatList.value) {
if (item.name?.includes(segment.keyword)) {
matched.push(item.organizationid);
}
}
} else {
for (const id of scopeIds) {
const descendants = getAllDescendantIds(id);
for (const descId of descendants) {
if (nameMap.get(descId)?.includes(segment.keyword)) {
matched.push(descId);
}
}
}
}
levelMatches.push(matched);
// 下一层的搜索范围 = 当前层匹配节点(作为父节点向下搜索)
scopeIds = new Set(matched);
}
// 构建可见集合
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);
}
};
// 每层的贡献
for (let i = 0; i < segments.length; i++) {
const segment = segments[i];
const matched = levelMatches[i];
const isLast = i === segments.length - 1;
if (segment.showDescendants) {
// 带 + :匹配节点 + 所有子孙
for (const id of matched) {
addDescendants(id);
}
} else if (isLast) {
// 最后一段无 + :仅匹配节点
for (const id of matched) {
ids.add(id);
}
}
// 中间段无 + :不直接贡献可见节点(会作为后续匹配的祖先自动出现)
}
// 补充祖先链路保持树结构完整
const currentIds = [...ids];
for (const id of currentIds) {
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 = parseSegments(filterText.value);
if (segments.length === 0) return text;
let result = text;
for (const segment of segments) {
const escaped = segment.keyword.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, 1);
};
/** 节点点击事件 - 向外分发 */
const handleNodeClick = (data: TreeNode) => {
emit('node-click', { id: data.id, label: data.label });
};
/** 计算树高度自适应视口 */
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;
}
.search-row {
display: flex;
align-items: center;
gap: 4px;
margin-bottom: 8px;
}
.help-btn {
font-size: 18px;
color: var(--el-text-color-secondary);
flex-shrink: 0;
}
:deep(.tree-highlight) {
color: var(--el-color-primary);
font-weight: bold;
}
.search-help {
.help-title {
font-weight: bold;
font-size: 14px;
margin: 0;
}
.help-list {
margin: 0;
padding-left: 16px;
li {
margin-bottom: 6px;
font-size: 13px;
code {
background: var(--el-fill-color-light);
padding: 1px 6px;
border-radius: 3px;
font-family: monospace;
color: var(--el-color-primary);
}
}
}
.help-note {
margin: 0;
font-size: 12px;
color: var(--el-text-color-secondary);
}
}
</style>

379
zdxt-web-client/zdxt-admin-plus-ui/src/views/efcode/srz/index.vue

@ -3,63 +3,21 @@
<el-row :gutter="20">
<!-- 左侧:虚拟化组织机构树 -->
<el-col :lg="6" :xs="24">
<el-card shadow="hover" class="dept-tree-card">
<div class="search-row">
<el-input
v-model="filterText"
placeholder="搜索单位名称"
prefix-icon="Search"
clearable
/>
<el-popover placement="bottom-end" :width="320" trigger="hover">
<template #reference>
<el-button :icon="QuestionFilled" link class="help-btn" />
</template>
<div class="search-help">
<p class="help-title">搜索语法说明</p>
<el-divider style="margin: 8px 0" />
<ul class="help-list">
<li><code>福田</code> 查找含"福田"的节点</li>
<li><code>福田+</code> 查找含"福田"的节点,并显示其所有子孙</li>
<li><code>福田/民政</code> 先找"福田",再在其下级中找"民政"</li>
<li><code>福田+/民政</code> 找"福田"并展开子孙,再在其中找"民政"</li>
<li><code>福田+/民政+</code> 同上,且"民政"也展开子孙</li>
</ul>
<el-divider style="margin: 8px 0" />
<p class="help-note"><b>/</b> 分隔层级,<b>+</b> 后缀展开子孙节点</p>
</div>
</el-popover>
</div>
<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>
<srz-dept-full-load-tree @node-click="handleNodeClick" />
</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 :inline="true" :model="queryParams" ref="queryFormRef" class="search-form">
<el-form-item label="姓名" prop="fullname">
<el-input v-model="queryParams.fullname" placeholder="请输入姓名" clearable @keyup.enter="handleQuery" />
<el-input v-model="queryParams.fullname" placeholder="请输入姓名" clearable @keyup.enter="handleQuery" style="width: 160px" />
</el-form-item>
<el-form-item label="用户名" prop="username">
<el-input v-model="queryParams.username" placeholder="请输入用户名" clearable @keyup.enter="handleQuery" />
<el-input v-model="queryParams.username" placeholder="请输入用户名" clearable @keyup.enter="handleQuery" style="width: 160px" />
</el-form-item>
<el-form-item label="手机号" prop="mobilenumber">
<el-input v-model="queryParams.mobilenumber" placeholder="请输入手机号" clearable @keyup.enter="handleQuery" />
<el-input v-model="queryParams.mobilenumber" placeholder="请输入手机号" clearable @keyup.enter="handleQuery" style="width: 160px" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
@ -71,7 +29,6 @@
<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="暂无数据">
@ -98,34 +55,16 @@
</template>
<script setup lang="ts" name="SrzDeptUser">
import { QuestionFilled } from '@element-plus/icons-vue';
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;
}
import SrzDeptFullLoadTree from '@/views/efcode/components/srz-dept-full-load-tree.vue';
import { listSrzUser } from '@/api/efcode/srz';
import type { SrzUserQuery, SrzUserVo } from '@/api/efcode/srz/types';
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,
@ -135,241 +74,8 @@ const queryParams = reactive<SrzUserQuery>({
mobilenumber: undefined
});
// 保存扁平列表用于计算可见节点
const flatList = ref<SrzDeptMinVo[]>([]);
// 存储过滤后应该显示的节点ID集合
const visibleIds = ref<Set<string>>(new Set());
/** 解析搜索段:提取关键词和是否带 + 后缀 */
interface SearchSegment {
keyword: string;
showDescendants: boolean;
}
const parseSegments = (query: string): SearchSegment[] => {
return query.split('/').map(s => s.trim()).filter(s => s.length > 0).map(s => {
if (s.endsWith('+')) {
return { keyword: s.slice(0, -1), showDescendants: true };
}
return { keyword: s, showDescendants: false };
}).filter(s => s.keyword.length > 0);
};
/**
* 计算可见节点集合,支持层级搜索(/ 分隔)和子孙展开(+ 后缀)
* - 福田:仅显示匹配节点 + 祖先
* - 福田+:显示匹配节点 + 所有子孙 + 祖先
* - 福田/民政:在福田子孙中找民政,显示民政匹配 + 祖先
* - 福田+/民政:福田全部子孙可见,在其中找民政匹配 + 祖先
* - 福田+/民政+:福田全部子孙可见,民政匹配 + 民政子孙 + 祖先
*/
const computeVisibleIds = (query: string) => {
if (!query) {
visibleIds.value = new Set();
return;
}
const segments = parseSegments(query);
if (segments.length === 0) {
visibleIds.value = new Set();
return;
}
// 构建映射
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 stack = [id];
while (stack.length) {
const current = stack.pop()!;
const children = childrenMap.get(current) || [];
for (const childId of children) {
result.push(childId);
stack.push(childId);
}
}
return result;
};
// 逐层搜索,记录每层匹配结果
const levelMatches: string[][] = [];
let scopeIds: Set<string> | null = null; // null = 全量
for (const segment of segments) {
const matched: string[] = [];
if (scopeIds === null) {
for (const item of flatList.value) {
if (item.name?.includes(segment.keyword)) {
matched.push(item.organizationid);
}
}
} else {
for (const id of scopeIds) {
const descendants = getAllDescendantIds(id);
for (const descId of descendants) {
if (nameMap.get(descId)?.includes(segment.keyword)) {
matched.push(descId);
}
}
}
}
levelMatches.push(matched);
// 下一层的搜索范围 = 当前层匹配节点(作为父节点向下搜索)
scopeIds = new Set(matched);
}
// 构建可见集合
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);
}
};
// 每层的贡献
for (let i = 0; i < segments.length; i++) {
const segment = segments[i];
const matched = levelMatches[i];
const isLast = i === segments.length - 1;
if (segment.showDescendants) {
// 带 + :匹配节点 + 所有子孙
for (const id of matched) {
addDescendants(id);
}
} else if (isLast) {
// 最后一段无 + :仅匹配节点
for (const id of matched) {
ids.add(id);
}
}
// 中间段无 + :不直接贡献可见节点(会作为后续匹配的祖先自动出现)
}
// 补充祖先链路保持树结构完整
const currentIds = [...ids];
for (const id of currentIds) {
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 = parseSegments(filterText.value);
if (segments.length === 0) return text;
let result = text;
for (const segment of segments) {
const escaped = segment.keyword.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) => {
/** 树节点点击事件 */
const handleNodeClick = (data: { id: string; label: string }) => {
selectedDeptName.value = data.label;
queryParams.organizationid = data.id;
queryParams.pageNum = 1;
@ -408,72 +114,19 @@ const resetQuery = () => {
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(.el-card__body) {
padding: 15px !important;
}
.search-row {
.search-form {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 4px;
margin-bottom: 8px;
}
.help-btn {
font-size: 18px;
color: var(--el-text-color-secondary);
flex-shrink: 0;
}
:deep(.tree-highlight) {
color: var(--el-color-primary);
font-weight: bold;
}
.search-help {
.help-title {
font-weight: bold;
font-size: 14px;
margin: 0;
}
.help-list {
margin: 0;
padding-left: 16px;
li {
margin-bottom: 6px;
font-size: 13px;
code {
background: var(--el-fill-color-light);
padding: 1px 6px;
border-radius: 3px;
font-family: monospace;
color: var(--el-color-primary);
}
}
}
.help-note {
margin: 0;
font-size: 12px;
color: var(--el-text-color-secondary);
:deep(.el-form-item) {
margin-bottom: 0;
}
}
</style>

Loading…
Cancel
Save