fix(产品需求): 修复产品需求使用状态和终止态字典的问题。

fix(组织): 修复组织编码下拉框的数据显示问题、修复组织编码负责人无法新增的问题。
fix(管理链路): 修复管理链路高度没固定,节点全部收缩等问题。
This commit is contained in:
dk
2026-05-07 17:09:53 +08:00
parent 991cbb5278
commit f4f43814b3
12 changed files with 287 additions and 82 deletions

View File

@@ -7,7 +7,7 @@
* - 支持节点的展开/折叠
* - 支持单选/多选节点
* - 提供新增、编辑、删除(单个/批量)功能
* - 支持按管理者用户 ID 和被管理用户 ID 搜索
* - 支持按上级用户 ID 和下级用户 ID 搜索
*
* 树形结构特点:
* - 根节点:最高领导,没有上级
@@ -134,6 +134,10 @@ async function loadTreeData() {
if (!error) {
treeData.value = data || [];
// 数据加载完成后,展开前两层节点
await nextTick();
expandFirstTwoLevels();
}
} finally {
loading.value = false;
@@ -167,6 +171,9 @@ async function loadTreeDataByQuery(query: Api.SystemManage.UserManagementRelatio
* 清空选中状态并重新加载数据
*/
async function reloadTreeData() {
// 保存当前展开状态
saveExpandedState();
checkedNodeKeys.value = [];
await loadTreeData();
await nextTick();
@@ -216,15 +223,25 @@ const { bool: operateVisible, setTrue: openOperateModal, setFalse: closeOperateM
const operateType = ref<UI.TableOperateType>('add');
const editingData = ref<Api.SystemManage.UserManagementRelation | null>(null);
/**
* 是否在管理链路树中选中节点后点击新增
* 用于控制新增对话框中上级用户下拉框是否禁用
*/
const isAddFromTreeNode = ref(false);
/**
* 打开新增对话框
*
* @param item 当前节点数据,用于设置默认管理者为此节点用户
* @param item 当前节点数据,用于设置默认上级为此节点用户
*/
function openAdd(item?: Api.SystemManage.UserManagementRelationTreeRespVO) {
operateType.value = 'add';
// 如果是从某一行的新增按钮触发,则默认管理者为当前节点用户
// 否则默认管理者为当前登录用户(在对话框组件中处理)
// 如果是从树节点点击的新增按钮,标记为来自树节点
isAddFromTreeNode.value = Boolean(item);
// 如果是从某一行的新增按钮触发,则默认上级为当前节点用户
// 否则默认上级为当前登录用户(在对话框组件中处理)
editingData.value = item
? {
id: null,
@@ -309,14 +326,127 @@ function handleNodeCheck(checkedData: any, checkedInfo: any) {
.filter((id: string | null): id is string => Boolean(id));
}
/**
* 保存当前展开的节点 ID 列表
* 用于在刷新数据后恢复展开状态
*/
const expandedNodeKeys = ref<string[]>([]);
/**
* 保存当前展开的节点状态
*/
function saveExpandedState() {
if (!relationTreeRef.value) {
return;
}
const store = (relationTreeRef.value as any).store;
if (!store) {
return;
}
const allNodes = store.nodesMap || {};
expandedNodeKeys.value = [];
Object.keys(allNodes).forEach(key => {
const node = allNodes[key];
if (node && node.expanded && node.data && node.data.userId) {
expandedNodeKeys.value.push(node.data.userId);
}
});
}
/**
* 处理对话框提交事件
*
* @param relationId 提交后的关系 ID
*/
function handleSubmitted(_relationId: string) {
async function handleSubmitted(_relationId: string) {
closeOperateModal();
reloadTreeData();
await reloadTreeData();
// 操作完成后恢复树节点的展开状态
await restoreExpandedState();
// 重置标记
isAddFromTreeNode.value = false;
}
/**
* 展开所有子节点(递归)
*
* @param tree 树形组件实例
* @param nodes 节点数据数组
*/
function expandNodes(tree: InstanceType<typeof ElTree>, nodes: Api.SystemManage.UserManagementRelationTreeRespVO[]) {
if (!tree || !nodes || !nodes.length) {
return;
}
for (const node of nodes) {
// 展开当前节点
const treeNode = tree.getNode(node.userId);
if (treeNode) {
treeNode.expand();
}
// 递归展开子节点
if (node.children && node.children.length > 0) {
expandNodes(tree, node.children);
}
}
}
/**
* 展开树的前两层节点
*
* 只展开根节点和它们的直接子节点,第三层及更深层保持折叠
*/
function expandFirstTwoLevels() {
const tree = relationTreeRef.value;
if (!tree || !treeData.value.length) {
return;
}
// 展开第一层(根节点)
for (const rootNode of treeData.value) {
const treeNode = tree.getNode(rootNode.userId);
if (treeNode) {
treeNode.expand();
}
// 展开第二层(根节点的直接子节点)
if (rootNode.children && rootNode.children.length > 0) {
for (const childNode of rootNode.children) {
const childTreeNode = tree.getNode(childNode.userId);
if (childTreeNode) {
childTreeNode.expand();
}
}
}
}
}
/**
* 恢复树节点的展开状态
*
* 根据之前保存的展开状态,恢复对应的节点展开
*/
async function restoreExpandedState() {
await nextTick();
const tree = relationTreeRef.value;
if (!tree || !expandedNodeKeys.value.length) {
return;
}
// 恢复之前展开的节点
for (const key of expandedNodeKeys.value) {
const node = tree.getNode(key);
if (node) {
node.expand();
}
}
}
/**
@@ -356,7 +486,7 @@ onMounted(async () => {
</script>
<template>
<div class="flex-col-stretch gap-16px overflow-hidden">
<div class="flex-col-stretch gap-16px overflow-hidden" style="height: calc(70vh - 120px)">
<!-- 搜索区域 -->
<RelationSearch
v-model:model="searchParams"
@@ -366,7 +496,7 @@ onMounted(async () => {
/>
<!-- 树形卡片区域 -->
<ElCard class="flex-1-hidden card-wrapper">
<ElCard class="flex-1-hidden card-wrapper min-h-0">
<template #header>
<div class="flex items-center justify-between gap-12px">
<div class="flex items-center gap-10px">
@@ -463,6 +593,7 @@ onMounted(async () => {
:operate-type="operateType"
:row-data="editingData"
:user-list="userList"
:is-add-from-tree-node="isAddFromTreeNode"
@submitted="handleSubmitted"
/>
</div>

View File

@@ -8,8 +8,8 @@
* - 表单验证和提交
*
* 表单字段:
* - 管理者用户:必填,下拉选择,默认当前登录用户
* - 被管理用户:必填,下拉选择,默认空
* - 上级用户:必填,下拉选择,默认当前登录用户
* - 下级用户用户:必填,下拉选择,默认空
* - 生效开始时间:可选
* - 生效结束时间:可选
* - 备注:可选
@@ -39,6 +39,8 @@ interface Props {
rowData?: Api.SystemManage.UserManagementRelation | null;
/** 用户列表,由父组件统一提供 */
userList: Api.SystemManage.UserSimple[];
/** 是否从树节点点击新增(用于控制上级用户下拉框禁用) */
isAddFromTreeNode?: boolean;
}
const props = defineProps<Props>();
@@ -115,8 +117,8 @@ function createDefaultModel(): Model {
* 表单验证规则
*/
const rules = {
managerUserId: createRequiredRule('请选择管理者用户'),
subordinateUserId: createRequiredRule('请选择被管理用户')
managerUserId: createRequiredRule('请选择上级用户'),
subordinateUserId: createRequiredRule('请选择下级用户')
} satisfies Record<string, App.Global.FormRule>;
/**
@@ -162,16 +164,16 @@ async function initModel() {
model.value = createDefaultModel();
if (!isEdit.value) {
// 新增模式:设置管理者用户
// 优先使用 rowData 中传入的管理者用户 ID如从树形节点新增
// 新增模式:设置上级用户
// 优先使用 rowData 中传入的上级用户 ID如从树形节点新增
// 否则使用当前登录用户
let managerUserIdToSet = resolveDefaultManagerUserId();
if (props.rowData && props.rowData.managerUserId) {
// 从树形节点点击新增,管理者为当前节点用户
// 从树形节点点击新增,上级为当前节点用户
managerUserIdToSet = props.rowData.managerUserId;
} else if (authStore.userInfo.userId) {
// 头部新增,管理者为当前登录用户
// 头部新增,上级为当前登录用户
const currentUserId = authStore.userInfo.userId;
const currentUserName = authStore.userInfo.userName;
@@ -301,18 +303,24 @@ watch(visible, value => {
<ElForm ref="formRef" :model="model" :rules="rules" label-position="top">
<ElRow :gutter="16">
<ElCol :span="12">
<ElFormItem label="管理者用户" prop="managerUserId">
<ElSelect v-model="model.managerUserId" class="w-full" placeholder="请选择管理者用户" filterable>
<ElFormItem label="上级用户" prop="managerUserId">
<ElSelect
v-model="model.managerUserId"
class="w-full"
placeholder="请选择上级用户"
filterable
:disabled="props.operateType === 'add' && props.isAddFromTreeNode"
>
<ElOption v-for="user in props.userList" :key="user.id" :label="user.nickname" :value="user.id" />
</ElSelect>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="被管理用户" prop="subordinateUserId">
<ElFormItem label="下级用户" prop="subordinateUserId">
<ElSelect
v-model="model.subordinateUserId"
class="w-full"
placeholder="请选择被管理用户"
placeholder="请选择下级用户"
filterable
:disabled="isEdit"
>

View File

@@ -3,7 +3,7 @@
* 用户管理链路搜索组件
*
* 功能说明:
* - 提供管理者和被管理者用户下拉选择
* - 提供上级和下级用户下拉选择
* - 支持搜索和重置操作
* - 与树形结构数据联动
*
@@ -63,11 +63,11 @@ function search() {
<template>
<TableSearchPanel :model="model" :action-col-lg="8" @reset="reset" @search="search">
<!-- <ElCol :lg="8" :md="12" :sm="12">-->
<!-- <ElFormItem label="管理者用户" prop="managerUserId">-->
<!-- <ElFormItem label="上级用户" prop="managerUserId">-->
<!-- <ElSelect-->
<!-- v-model="model.managerUserId"-->
<!-- class="w-full"-->
<!-- placeholder="请选择管理者用户"-->
<!-- placeholder="请选择上级用户"-->
<!-- clearable-->
<!-- filterable-->
<!-- >-->

View File

@@ -66,7 +66,7 @@ function mapUsersToCandidateUsers(users: Api.SystemManage.User[]): Api.SystemMan
return users
.filter(item => !item.resignedAt || item.resignedAt > now)
.map(item => ({
id: item.id,
id: String(item.id),
nickname: item.nickname?.trim() || item.username,
deptId: item.deptId,
deptName: item.deptName ?? null

View File

@@ -44,7 +44,7 @@ const title = computed(() => {
});
type Model = {
userId: number | null;
userId: string | null;
effectiveFrom: Date | null;
effectiveUntil: Date | null;
remark: string;
@@ -119,7 +119,7 @@ async function handleSubmit() {
const payload: Api.SystemManage.SaveOrgLeaderRelationParams = {
deptId: props.dept.id,
userId: Number(model.value.userId),
userId: model.value.userId,
effectiveFrom,
effectiveUntil,
remark: model.value.remark.trim() || null
@@ -129,10 +129,10 @@ async function handleSubmit() {
const request =
isEdit.value && props.rowData
? fetchUpdateOrgLeaderRelation({ id: props.rowData.id, ...payload })
: fetchCreateOrgLeaderRelation(payload);
? await fetchUpdateOrgLeaderRelation({ id: props.rowData.id, ...payload })
: await fetchCreateOrgLeaderRelation(payload);
const { error } = await request;
const { error } = request;
submitting.value = false;
@@ -186,10 +186,11 @@ watch(visible, async value => {
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem :label="$t('page.system.user.effectiveFrom')" prop="effectiveFrom">
<ElFormItem :label="$t('page.system.user.effectiveFrom')" prop="effectiveFrom" style="width: 100%">
<ElDatePicker
v-model="model.effectiveFrom"
class="w-full"
style="width: 100%"
type="datetime"
clearable
:placeholder="$t('page.system.user.form.effectiveFrom')"
@@ -197,10 +198,11 @@ watch(visible, async value => {
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem :label="$t('page.system.user.effectiveUntil')" prop="effectiveUntil">
<ElFormItem :label="$t('page.system.user.effectiveUntil')" prop="effectiveUntil" style="width: 100%">
<ElDatePicker
v-model="model.effectiveUntil"
class="w-full"
style="width: 100%"
type="datetime"
clearable
:placeholder="$t('page.system.user.form.effectiveUntil')"

View File

@@ -232,7 +232,7 @@ watch(visible, async value => {
<ElOption
v-for="item in props.orgCodeOptions"
:key="item.value"
:label="item.value"
:label="item.label"
:value="item.value"
/>
</ElSelect>