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

@@ -36,14 +36,6 @@ export const RDMS_OBJECT_DIRECTION_LEGACY_DICT_CODE = 'rdms_product_direction';
*/
export const SYSTEM_USER_COMPANY_DICT_CODE = 'system_user_company';
/**
* 需求终态状态字典编码
*
* 对应业务字段:需求相关接口和页面中的 terminal status
* 来源口径:产品需求权限文档中定义,标签包括已关闭、已取消、已拒绝
*/
export const RDMS_REQ_TERMINAL_STATUS_DICT_CODE = 'rdms_req_terminal_status';
/**
* 需求来源类型字典编码
*
@@ -67,11 +59,3 @@ export const RDMS_REQ_PRIORITY_DICT_CODE = 'rdms_req_priority';
* 来源口径:产品需求文档中定义,标签包括工程需求、用户需求、安全需求、体验优化、功能需求
*/
export const RDMS_REQ_CATEGORY_DICT_CODE = 'rdms_req_category';
/**
* 需求状态字典编码
*
* 对应业务字段:需求相关接口和页面中的 statusCode
* 来源口径:产品需求文档中定义
*/
export const RDMS_REQ_STATUS_DICT_CODE = 'rdms_req_status';

View File

@@ -320,6 +320,28 @@ export async function fetchGetRequirementLifecycle(requirementId: string, produc
return mapServiceResult(result as ServiceRequestResult<Api.Product.RequirementLifecycleInfo>, data => data);
}
/** 获取需求所有状态字典 */
export async function fetchGetRequirementStatusDict() {
const result = await request<Api.Product.RequirementStatusDict[]>({
...safeJsonRequestConfig,
url: `${REQUIREMENT_PREFIX}/status/dict`,
method: 'get'
});
return mapServiceResult(result as ServiceRequestResult<Api.Product.RequirementStatusDict[]>, data => data);
}
/** 获取需求终止态状态字典 */
export async function fetchGetRequirementTerminalStatusDict() {
const result = await request<Api.Product.RequirementStatusDict[]>({
...safeJsonRequestConfig,
url: `${REQUIREMENT_PREFIX}/status/dict/terminal`,
method: 'get'
});
return mapServiceResult(result as ServiceRequestResult<Api.Product.RequirementStatusDict[]>, data => data);
}
// ========== 模块管理 API ==========
type RequirementModuleResponse = Omit<Api.Product.RequirementModule, 'id' | 'parentId' | 'productId'> & {
id: string | number;

View File

@@ -319,6 +319,21 @@ declare namespace Api {
children?: RequirementModule[];
}
// ========== 需求状态字典 ==========
interface RequirementStatusDict {
/** 状态编码 */
statusCode: string;
/** 状态名称 */
statusName: string;
/** 排序值 */
sort: number;
/** 是否初始状态 */
initialFlag: boolean;
/** 是否终态 */
terminalFlag: boolean;
}
// ========== 需求生命周期 ==========
interface RequirementLifecycleAction {

View File

@@ -103,7 +103,7 @@ declare namespace Api {
interface OrgLeaderRelation {
id: number;
deptId: number;
userId: number;
userId: string;
userNickname: string;
effectiveFrom?: number | null;
effectiveUntil?: number | null;
@@ -115,7 +115,7 @@ declare namespace Api {
type OrgLeaderRelationList = OrgLeaderRelation[];
interface OrgLeaderCandidateUser {
id: number;
id: string;
nickname: string;
deptId: number;
deptName?: string | null;
@@ -125,7 +125,7 @@ declare namespace Api {
type SaveOrgLeaderRelationParams = {
deptId: number;
userId: number;
userId: string | null;
effectiveFrom?: number | null;
effectiveUntil?: number | null;
remark?: string | null;

View File

@@ -1,23 +1,22 @@
<script setup lang="tsx">
import { computed, reactive, ref, watch } from 'vue';
import { computed, onMounted, reactive, ref, watch } from 'vue';
import type { TableInstance } from 'element-plus';
import { ElButton, ElTag } from 'element-plus';
import dayjs from 'dayjs';
import {
RDMS_REQ_CATEGORY_DICT_CODE,
RDMS_REQ_PRIORITY_DICT_CODE,
RDMS_REQ_STATUS_DICT_CODE,
RDMS_REQ_TERMINAL_STATUS_DICT_CODE
RDMS_REQ_PRIORITY_DICT_CODE
} from '@/constants/dict';
import {
fetchChangeRequirementStatus,
fetchDeleteRequirement,
fetchGetProductMembers,
fetchGetRequirementAllowedTransitions,
fetchGetRequirementStatusDict,
fetchGetRequirementTerminalStatusDict,
fetchGetRequirementTree
} from '@/service/api';
import { useAuth } from '@/hooks/business/auth';
import { useDict } from '@/hooks/business/dict';
import BusinessTableActionCell from '@/components/custom/business-table-action-cell';
import DictTag from '@/components/custom/dict-tag.vue';
import { useCurrentProduct } from '../shared/use-current-product';
@@ -42,7 +41,38 @@ defineOptions({ name: 'ProductRequirement' });
const { currentObjectId } = useCurrentProduct();
const { hasObjectAuth } = useAuth();
const { dictOptions: terminalStatusOptions } = useDict(RDMS_REQ_TERMINAL_STATUS_DICT_CODE);
const statusOptions = ref<Array<{ label: string; value: string }>>([]);
const terminalStatusOptions = ref<string[]>([]);
async function loadStatusOptions() {
const { error, data } = await fetchGetRequirementStatusDict();
if (error || !data) {
statusOptions.value = [];
return;
}
statusOptions.value = data.map(item => ({
label: item.statusName,
value: item.statusCode
}));
}
async function loadTerminalStatusOptions() {
const { error, data } = await fetchGetRequirementTerminalStatusDict();
if (error || !data) {
terminalStatusOptions.value = [];
return;
}
terminalStatusOptions.value = data.map(item => item.statusCode);
}
function getStatusLabel(statusCode: string) {
const item = statusOptions.value.find(opt => opt.value === statusCode);
return item ? item.label : statusCode;
}
const priorityTagTypeMap: Record<number, UI.ThemeColor> = {
0: 'info',
@@ -60,7 +90,7 @@ function formatDateTime(value?: string | null) {
}
function isTerminalStatus(statusCode: string) {
return terminalStatusOptions.value.some(option => option.value === statusCode);
return terminalStatusOptions.value.some(option => option === statusCode);
}
function canSplitRequirement(row: Api.Product.Requirement) {
@@ -233,15 +263,15 @@ const columns = computed(() => [
minWidth: 120,
formatter: (row: Api.Product.Requirement) => row.category
},
{
prop: 'description',
label: '描述',
minWidth: 200,
showOverflowTooltip: true,
formatter: (row: Api.Product.Requirement) => {
return row.description?.replace(/<[^>]+>/g, '').trim() || '--';
}
},
// {
// prop: 'description',
// label: '描述',
// minWidth: 200,
// showOverflowTooltip: true,
// formatter: (row: Api.Product.Requirement) => {
// return row.description?.replace(/<[^>]+>/g, '').trim() || '--';
// }
// },
{
prop: 'priority',
label: '优先级',
@@ -257,11 +287,9 @@ const columns = computed(() => [
width: 100,
align: 'center',
formatter: (row: Api.Product.Requirement) => (
<DictTag
dictCode={RDMS_REQ_STATUS_DICT_CODE}
value={row.statusCode}
type={getRequirementStatusTagType(row.statusCode)}
/>
<ElTag type={getRequirementStatusTagType(row.statusCode)}>
{getStatusLabel(row.statusCode)}
</ElTag>
)
},
{
@@ -601,6 +629,10 @@ watch(
},
{ immediate: true }
);
onMounted(async () => {
await Promise.all([loadStatusOptions(), loadTerminalStatusOptions()]);
});
</script>
<template>

View File

@@ -1,5 +1,7 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { RDMS_REQ_SOURCE_TYPE_DICT_CODE } from '@/constants/dict';
import { fetchGetRequirementStatusDict } from '@/service/api';
import DictSelect from '@/components/custom/dict-select.vue';
import TableSearchPanel from '@/components/custom/table-search-panel.vue';
import MemberSelectOption from './member-select-option.vue';
@@ -29,16 +31,21 @@ const emit = defineEmits<Emits>();
const model = defineModel<Api.Product.RequirementSearchParams>('model', { required: true });
const requirementStatusOptions = [
{ label: '待确认', value: 'pending_confirm' },
{ label: '待评审', value: 'pending_review' },
{ label: '待分流', value: 'pending_dispatch' },
{ label: '实施中', value: 'implementing' },
{ label: '已验收', value: 'accepted' },
{ label: '已关闭', value: 'closed' },
{ label: '已拒绝', value: 'rejected' },
{ label: '已取消', value: 'cancelled' }
];
const requirementStatusOptions = ref<Array<{ label: string; value: string }>>([]);
async function loadStatusOptions() {
const { error, data } = await fetchGetRequirementStatusDict();
if (error || !data) {
requirementStatusOptions.value = [];
return;
}
requirementStatusOptions.value = data.map(item => ({
label: item.statusName,
value: item.statusCode
}));
}
function reset() {
emit('reset');
@@ -47,6 +54,10 @@ function reset() {
function search() {
emit('search');
}
onMounted(async () => {
await loadStatusOptions();
});
</script>
<template>

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>