- 新增产品管理相关路由和页面(dashboard、list、requirement、setting) - 实现产品基础信息编辑弹窗组件(base-info-dialog.vue) - 添加运行时字典功能(dict-select、dict-text、dict-tag组件) - 集成字典管理store和API调用 - 规范ID类型定义为string避免精度丢失问题 - 完善国际化资源文件支持中英文对照 - 新增对象上下文业务域入口页导航实现说明 - 添加Vue DevTools浮动入口注释说明 - 统一权限控制支持全局和对象作用域区分 - 规范分页查询参数类型定义与使用方式
494 lines
13 KiB
Vue
494 lines
13 KiB
Vue
<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>
|