Files
cn-rdms-web/src/views/system/user-management-relation/index.vue
hongawen 4122dfa50d feat(product): 新增产品管理模块与字典组件功能
- 新增产品管理相关路由和页面(dashboard、list、requirement、setting)
- 实现产品基础信息编辑弹窗组件(base-info-dialog.vue)
- 添加运行时字典功能(dict-select、dict-text、dict-tag组件)
- 集成字典管理store和API调用
- 规范ID类型定义为string避免精度丢失问题
- 完善国际化资源文件支持中英文对照
- 新增对象上下文业务域入口页导航实现说明
- 添加Vue DevTools浮动入口注释说明
- 统一权限控制支持全局和对象作用域区分
- 规范分页查询参数类型定义与使用方式
2026-04-23 09:05:55 +08:00

494 lines
13 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.

<script setup lang="ts">
/**
* 用户管理链路管理 - 主页面
*
* 功能说明:
* - 展示用户管理链路的树形结构
* - 支持节点的展开/折叠
* - 支持单选/多选节点
* - 提供新增、编辑、删除(单个/批量)功能
* - 支持按管理者用户 ID 和被管理用户 ID 搜索
*
* 树形结构特点:
* - 根节点:最高领导,没有上级
* - 中间节点:有上级也有下级
* - 叶子节点:基层员工,没有下级
*/
import { computed, nextTick, onMounted, reactive, ref } from 'vue';
import type { ElTree } from 'element-plus';
import { ElButton, ElPopconfirm, ElTag } from 'element-plus';
import { useBoolean } from '@sa/hooks';
import {
fetchBatchDeleteUserManagementRelation,
fetchDeleteUserManagementRelation,
fetchGetUserListByDeptId,
fetchGetUserManagementRelationQuery,
fetchGetUserManagementRelationTree
} from '@/service/api';
import RelationOperateDialog from './modules/relation-operate-dialog.vue';
import RelationSearch from './modules/relation-search.vue';
defineOptions({ name: 'UserManagementRelation' });
/**
* 组件 userQuery 定义
*
* @param fromUserIndex 是否不是从管理链路 index 页面访问(从 user 页面访问时为 true
* @param deptId 部门 ID
* @param orgType 组织类型company/dept/direction/team
*/
interface userQuery {
fromUserIndex?: boolean;
deptId?: number | null;
orgType?: string;
}
// 从user的index组件访问管理链路fromUserIndex为true否则false; dept=100是灿能电力的id
const { fromUserIndex = false, deptId = 100, orgType = 'company' } = defineProps<userQuery>();
/**
* 判断节点是否为母节点(根节点)
*
* 母节点是指树形数据中的第一层节点
*
* @param data 节点数据
*/
/**
* 判断母节点的编辑按钮是否应该隐藏
*
* 当组织类型为部门、方向或团队时,隐藏母节点的编辑按钮
*/
const shouldHideRootEdit = computed(() => {
return fromUserIndex && orgType !== 'company';
});
/**
* 初始化搜索参数
*
* @returns 搜索参数对象
*/
function getInitSearchParams(): Api.SystemManage.UserManagementRelationQueryReqVO {
return {
managerUserId: undefined,
subordinateUserId: undefined
};
}
// 搜索参数
const searchParams = reactive(getInitSearchParams());
// 用户列表(供搜索组件和对话框组件共享)
const userList = ref<Api.SystemManage.UserSimple[]>([]);
// 树形组件引用
const relationTreeRef = ref<InstanceType<typeof ElTree>>();
// 已选中的节点 ID 列表
const checkedNodeKeys = ref<string[]>([]);
// 树形数据
const treeData = ref<Api.SystemManage.UserManagementRelationTreeRespVO[]>([]);
function isRootNode(data: Api.SystemManage.UserManagementRelationTreeRespVO): boolean {
return treeData.value.some(node => node.userId === data.userId);
}
// 加载状态
const loading = ref(false);
// 树形配置
const treeProps: any = {
children: 'children',
label: 'userNickname',
isLeaf: (data: Api.SystemManage.UserManagementRelationTreeRespVO) => !data.children || data.children.length === 0
};
/**
* 加载用户列表
*
* 获取用户简单列表,供搜索组件和对话框组件共享使用
*/
async function loadUserList() {
const { data, error } = await fetchGetUserListByDeptId(deptId);
if (!error) {
userList.value = data || [];
}
}
/**
* 加载树形数据
*
* 调用后端接口获取完整的用户管理链路树
*/
async function loadTreeData() {
loading.value = true;
try {
// 默认不是来自user的index组件访问且deptId=100查询灿能电力及其以下所有部门的用户的管理链路
const query: Api.SystemManage.UserManagementRelationQueryReqVO = {
fromUserIndex,
deptId
};
const { data, error } = await fetchGetUserManagementRelationTree(query);
if (!error) {
treeData.value = data || [];
}
} finally {
loading.value = false;
}
}
/**
* 根据搜索条件查询树形数据
*
* 调用后端接口获取符合条件的用户管理链路树
*
* @param query 查询参数
*/
async function loadTreeDataByQuery(query: Api.SystemManage.UserManagementRelationQueryReqVO) {
loading.value = true;
try {
const { data, error } = await fetchGetUserManagementRelationQuery(query);
if (!error) {
treeData.value = data || [];
}
} finally {
loading.value = false;
}
}
/**
* 刷新树形数据
*
* 清空选中状态并重新加载数据
*/
async function reloadTreeData() {
checkedNodeKeys.value = [];
await loadTreeData();
await nextTick();
relationTreeRef.value?.setCheckedKeys([]);
}
/**
* 处理搜索
*
* 根据搜索条件查询树形数据
* 如果有搜索条件,调用 query 接口;否则调用 tree 接口
*/
async function handleSearch() {
checkedNodeKeys.value = [];
// 判断是否有搜索条件
const hasSearchCondition = searchParams.subordinateUserId !== undefined && searchParams.subordinateUserId !== null;
if (hasSearchCondition) {
// 有搜索条件,调用查询接口
const query: Api.SystemManage.UserManagementRelationQueryReqVO = {
subordinateUserId: searchParams.subordinateUserId,
fromUserIndex,
deptId
};
await loadTreeDataByQuery(query);
} else {
// 无搜索条件,加载完整树
await loadTreeData();
}
await nextTick();
relationTreeRef.value?.setCheckedKeys([]);
}
/**
* 重置搜索
*
* 清空搜索条件并重新加载数据
*/
function resetSearchParams() {
Object.assign(searchParams, getInitSearchParams());
reloadTreeData();
}
// 对话框相关状态
const { bool: operateVisible, setTrue: openOperateModal, setFalse: closeOperateModal } = useBoolean();
const operateType = ref<UI.TableOperateType>('add');
const editingData = ref<Api.SystemManage.UserManagementRelation | null>(null);
/**
* 打开新增对话框
*
* @param item 当前节点数据,用于设置默认管理者为此节点用户
*/
function openAdd(item?: Api.SystemManage.UserManagementRelationTreeRespVO) {
operateType.value = 'add';
// 如果是从某一行的新增按钮触发,则默认管理者为当前节点用户
// 否则默认管理者为当前登录用户(在对话框组件中处理)
editingData.value = item
? {
id: null,
managerUserId: item.userId,
subordinateUserId: null,
effectiveFrom: null,
effectiveUntil: null,
remark: null,
createTime: Date.now()
}
: null;
openOperateModal();
}
/**
* 打开编辑对话框
*
* @param item 要编辑的关系记录
*/
function openEdit(item: Api.SystemManage.UserManagementRelationTreeRespVO) {
operateType.value = 'edit';
// 构建树节点数据为编辑所需格式
editingData.value = item.id
? {
id: item.id,
managerUserId: item.managerUserId,
subordinateUserId: item.userId,
effectiveFrom: null,
effectiveUntil: null,
remark: null,
createTime: Date.now()
}
: null;
openOperateModal();
}
/**
* 删除单个节点的关系记录
*
* @param item 要删除的关系记录
*/
async function handleDelete(item: Api.SystemManage.UserManagementRelationTreeRespVO) {
const { error } = await fetchDeleteUserManagementRelation(item.id);
if (error) {
return;
}
window.$message?.success('删除成功');
await reloadTreeData();
}
/**
* 批量删除关系记录
*
* 删除所有选中的节点
*/
async function handleBatchDelete() {
if (!checkedNodeKeys.value.length) {
return;
}
const { error } = await fetchBatchDeleteUserManagementRelation(checkedNodeKeys.value);
if (error) {
return;
}
window.$message?.success('删除成功');
await reloadTreeData();
}
/**
* 处理树节点勾选变化
*
* @param checkedData 当前选中的节点数据
* @param checkedInfo 包含 checkedKeys 和 halfCheckedKeys 的对象
*/
function handleNodeCheck(checkedData: any, checkedInfo: any) {
checkedNodeKeys.value = checkedInfo.checkedNodes
.map((node: any) => node.id)
.filter((id: string | null): id is string => Boolean(id));
}
/**
* 处理对话框提交事件
*
* @param relationId 提交后的关系 ID
*/
function handleSubmitted(_relationId: string) {
closeOperateModal();
reloadTreeData();
}
/**
* 判断节点是否有子节点
*
* 有子节点的节点不允许删除
*
* @param node 树节点
*/
function hasChildren(node: Api.SystemManage.UserManagementRelationTreeRespVO): boolean {
return Boolean(node.children && node.children.length > 0);
}
/**
* 计算树形数据中所有节点的数量
*
* 递归遍历树形结构,统计所有节点总数
*
* @param nodes 树形数据数组
* @returns 节点总数
*/
function countTreeNodes(nodes: Api.SystemManage.UserManagementRelationTreeRespVO[]): number {
let count = 0;
for (const node of nodes) {
count += 1;
if (node.children && node.children.length > 0) {
count += countTreeNodes(node.children);
}
}
return count;
}
onMounted(async () => {
await loadUserList();
await reloadTreeData();
});
</script>
<template>
<div class="flex-col-stretch gap-16px overflow-hidden">
<!-- 搜索区域 -->
<RelationSearch
v-model:model="searchParams"
:user-list="userList"
@reset="resetSearchParams"
@search="handleSearch"
/>
<!-- 树形卡片区域 -->
<ElCard class="flex-1-hidden card-wrapper">
<template #header>
<div class="flex items-center justify-between gap-12px">
<div class="flex items-center gap-10px">
<p>用户管理链路树</p>
<ElTag effect="plain">{{ countTreeNodes(treeData) }}</ElTag>
</div>
<div class="flex items-center gap-10px">
<ElButton plain type="primary" @click="openAdd()">
<template #icon>
<icon-ic-round-plus class="text-icon" />
</template>
新增
</ElButton>
<!-- <ElPopconfirm title="确认删除选中的关系吗?" @confirm="handleBatchDelete">-->
<!-- <template #reference>-->
<!-- <ElButton type="danger" plain :disabled="checkedNodeKeys.length === 0">-->
<!-- <template #icon>-->
<!-- <icon-ic-round-delete class="text-icon"/>-->
<!-- </template>-->
<!-- 批量删除-->
<!-- </ElButton>-->
<!-- </template>-->
<!-- </ElPopconfirm>-->
<ElButton @click="reloadTreeData">
<template #icon>
<icon-ic-round-refresh class="text-icon" />
</template>
刷新
</ElButton>
</div>
</div>
</template>
<!-- 树形组件 -->
<div class="flex-1 overflow-auto">
<ElTree
ref="relationTreeRef"
v-loading="loading"
:data="treeData"
:props="treeProps"
node-key="userId"
:expand-on-click-node="false"
@check="handleNodeCheck"
>
<template #default="{ node, data }">
<div class="flex flex-1 items-center justify-between">
<span class="flex items-center gap-8px">
<span>{{ node.label }}</span>
<!-- <ElTag v-if="data.managerNickname" size="small" type="info">上级{{ data.managerNickname }}</ElTag>-->
</span>
<div class="flex items-center" style="min-width: 200px">
<ElButton link type="primary" size="default" @click.stop="openAdd(data)">
<template #icon>
<icon-ic-round-plus class="text-icon" />
</template>
新增
</ElButton>
<ElButton
v-if="!(isRootNode(data) && shouldHideRootEdit)"
link
type="primary"
size="small"
@click.stop="openEdit(data)"
>
<template #icon>
<icon-ic-round-edit class="text-icon" />
</template>
编辑
</ElButton>
<ElPopconfirm
v-if="!hasChildren(data) && data.id"
title="确认删除当前关系吗?"
@confirm="handleDelete(data)"
>
<template #reference>
<ElButton link type="danger" size="small">
<template #icon>
<icon-ic-round-delete class="text-icon" />
</template>
删除
</ElButton>
</template>
</ElPopconfirm>
</div>
</div>
</template>
</ElTree>
</div>
</ElCard>
<!-- 操作对话框 -->
<RelationOperateDialog
v-model:visible="operateVisible"
:operate-type="operateType"
:row-data="editingData"
:user-list="userList"
@submitted="handleSubmitted"
/>
</div>
</template>
<style lang="scss" scoped>
:deep(.el-card__body) {
height: calc(100% - 56px);
display: flex;
flex-direction: column;
}
:deep(.el-tree-node__content) {
height: auto;
padding: 10px 0;
border-bottom: 1px solid var(--el-border-color-lighter);
&:hover {
background-color: var(--el-fill-color-light);
}
}
:deep(.el-tree-node__label) {
flex: 1;
display: flex;
align-items: center;
}
</style>