# Conflicts:
#	src/views/product/requirement/index.vue
#	src/views/system/user-management-relation/index.vue
This commit is contained in:
dk
2026-05-09 13:44:08 +08:00
104 changed files with 13044 additions and 1028 deletions

19
src/service/api/file.ts Normal file
View File

@@ -0,0 +1,19 @@
import { SYSTEM_SERVICE_PREFIX } from '@/constants/service';
import { request } from '../request';
const FILE_PREFIX = `${SYSTEM_SERVICE_PREFIX}/file`;
/** 上传文件(模式一:后端中转) */
export function uploadFile(file: File, directory?: string) {
const formData = new FormData();
formData.append('file', file);
if (directory) {
formData.append('directory', directory);
}
return request<string>({
url: `${FILE_PREFIX}/upload`,
method: 'post',
data: formData
});
}

View File

@@ -1,6 +1,9 @@
export * from './auth';
export * from './dict';
export * from './file';
export * from './object-context';
export * from './product';
export * from './project';
export * from './project-shared';
export * from './route';
export * from './system-manage';

View File

@@ -0,0 +1,199 @@
import { normalizeNullableStringId, normalizeStringId } from './shared';
export interface BackendObjectContextMenuDTO {
key?: string | null;
label?: string | null;
routeKey?: string | null;
routePath?: string | null;
id?: string | number | null;
name?: string | null;
path?: string | null;
icon?: string | null;
sort?: number | null;
children?: BackendObjectContextMenuDTO[] | null;
}
interface BackendProductContextProductDTO {
id?: string | number | null;
code?: string | null;
directionCode?: string | null;
name?: string | null;
managerUserId?: string | number | null;
statusCode?: string | null;
}
interface BackendProjectContextProjectDTO {
id?: string | number | null;
projectCode?: string | null;
projectName?: string | null;
projectType?: string | null;
productId?: string | number | null;
managerUserId?: string | number | null;
statusCode?: string | null;
}
interface BackendObjectContextRoleDTO {
roleId?: string | number | null;
roleCode?: string | null;
roleName?: string | null;
guestFlag?: boolean | null;
}
export interface BackendObjectContextDTO {
domainKey?: string | null;
objectType?: string | null;
objectId?: string | number | null;
objectName?: string | null;
objectSummary?: Record<string, unknown> | null;
menus?: BackendObjectContextMenuDTO[] | null;
contextScopedMenus?: BackendObjectContextMenuDTO[] | null;
buttonCodes?: string[] | null;
currentProduct?: BackendProductContextProductDTO | null;
currentProject?: BackendProjectContextProjectDTO | null;
currentRole?: BackendObjectContextRoleDTO | null;
navs?: BackendObjectContextMenuDTO[] | null;
buttons?: string[] | null;
defaultRouteKey?: string | null;
defaultRoutePath?: string | null;
}
function normalizeString(value: string | number | null | undefined) {
if (value === null || value === undefined) {
return '';
}
return String(value);
}
function normalizeRoutePath(path: string | null | undefined) {
const normalizedPath = normalizeString(path).trim();
if (!normalizedPath) {
return '';
}
if (normalizedPath.startsWith('/')) {
return normalizedPath;
}
return `/${normalizedPath}`;
}
function normalizeCurrentProduct(
product: BackendProductContextProductDTO
): Record<'id' | 'code' | 'directionCode' | 'name' | 'managerUserId' | 'statusCode', string> {
return {
id: normalizeStringId(product.id || ''),
code: normalizeString(product.code),
directionCode: normalizeString(product.directionCode),
name: normalizeString(product.name),
managerUserId: normalizeNullableStringId(product.managerUserId) ?? '',
statusCode: normalizeString(product.statusCode)
};
}
function normalizeCurrentProject(project: BackendProjectContextProjectDTO) {
return {
id: normalizeStringId(project.id || ''),
projectCode: normalizeString(project.projectCode),
projectName: normalizeString(project.projectName),
projectType: normalizeString(project.projectType),
productId: normalizeNullableStringId(project.productId),
managerUserId: normalizeNullableStringId(project.managerUserId) ?? '',
statusCode: normalizeString(project.statusCode)
};
}
function normalizeCurrentRole(role: BackendObjectContextRoleDTO) {
return {
roleId: normalizeStringId(role.roleId || ''),
roleCode: normalizeString(role.roleCode),
roleName: normalizeString(role.roleName),
guestFlag: Boolean(role.guestFlag)
};
}
function normalizeMenu(menu: BackendObjectContextMenuDTO): App.ObjectContext.Menu {
const routeKey = normalizeString(menu.routeKey);
const routePath = normalizeRoutePath(menu.routePath || menu.path);
const key = normalizeString(menu.key || routeKey || routePath || menu.id);
return {
key,
label: normalizeString(menu.label || menu.name),
routeKey: routeKey || null,
routePath: routePath || null,
children: menu.children?.map(child => normalizeMenu(child)) || []
};
}
function getFirstNonEmptyMenuSource(data: BackendObjectContextDTO) {
const menuSources = [data.contextScopedMenus, data.menus, data.navs];
return menuSources.find(source => Array.isArray(source) && source.length > 0) || [];
}
function getFirstRoutableMenu(menus: App.ObjectContext.Menu[]): App.ObjectContext.Menu | null {
for (const menu of menus) {
if (menu.routeKey || menu.routePath) {
return menu;
}
const firstChildMenu = menu.children?.length ? getFirstRoutableMenu(menu.children) : null;
if (firstChildMenu) {
return firstChildMenu;
}
}
return null;
}
function normalizeObjectSummary(data: BackendObjectContextDTO): App.ObjectContext.Summary | null {
if (data.objectSummary) {
return data.objectSummary;
}
const summary: App.ObjectContext.Summary = {};
if (data.currentProduct) {
summary.currentProduct = normalizeCurrentProduct(data.currentProduct);
}
if (data.currentProject) {
summary.currentProject = normalizeCurrentProject(data.currentProject);
}
if (data.currentRole !== undefined) {
summary.currentRole = data.currentRole ? normalizeCurrentRole(data.currentRole) : null;
}
return Object.keys(summary).length ? summary : null;
}
// 待重构:拆 helper 以降低复杂度,暂以 disable 注释临时放行
// eslint-disable-next-line complexity
export function normalizeObjectContext(
config: App.ObjectContext.DomainConfig,
objectId: string,
data: BackendObjectContextDTO
): Api.ObjectContext.ContextInfo {
const rawMenus = getFirstNonEmptyMenuSource(data);
const contextScopedMenus = rawMenus.map(menu => normalizeMenu(menu));
const firstRoutableMenu = getFirstRoutableMenu(contextScopedMenus);
const currentProduct = data.currentProduct ? normalizeCurrentProduct(data.currentProduct) : null;
const currentProject = data.currentProject ? normalizeCurrentProject(data.currentProject) : null;
return {
domainKey: (data.domainKey || config.domainKey) as App.ObjectContext.DomainKey,
objectType: (data.objectType || config.objectType) as App.ObjectContext.ObjectType,
objectId: normalizeString(data.objectId) || currentProduct?.id || currentProject?.id || objectId,
objectName: normalizeString(data.objectName || currentProduct?.name || currentProject?.projectName),
objectSummary: normalizeObjectSummary(data),
contextScopedMenus,
buttonCodes: data.buttonCodes ?? data.buttons ?? [],
defaultRouteKey: data.defaultRouteKey || firstRoutableMenu?.routeKey || '',
defaultRoutePath:
normalizeRoutePath(data.defaultRoutePath) || firstRoutableMenu?.routePath || config.fallbackDefaultRoutePath
};
}

View File

@@ -1,145 +1,7 @@
import type { LocationQueryValue } from 'vue-router';
import { request } from '../request';
import {
type ServiceRequestResult,
normalizeNullableStringId,
normalizeStringId,
safeJsonRequestConfig
} from './shared';
interface BackendObjectContextMenuDTO {
key?: string | null;
label?: string | null;
routeKey?: string | null;
routePath?: string | null;
id?: string | number | null;
name?: string | null;
path?: string | null;
children?: BackendObjectContextMenuDTO[] | null;
}
interface BackendProductContextProductDTO {
id?: string | number | null;
code?: string | null;
directionCode?: string | null;
name?: string | null;
managerUserId?: string | number | null;
statusCode?: string | null;
}
interface BackendProductContextRoleDTO {
roleId?: string | number | null;
roleCode?: string | null;
roleName?: string | null;
}
interface BackendObjectContextDTO {
domainKey?: string | null;
objectType?: string | null;
objectId?: string | number | null;
objectName?: string | null;
objectSummary?: Record<string, unknown> | null;
menus?: BackendObjectContextMenuDTO[] | null;
contextScopedMenus?: BackendObjectContextMenuDTO[] | null;
buttonCodes?: string[] | null;
currentProduct?: BackendProductContextProductDTO | null;
currentRole?: BackendProductContextRoleDTO | null;
navs?: BackendObjectContextMenuDTO[] | null;
buttons?: string[] | null;
defaultRouteKey?: string | null;
defaultRoutePath?: string | null;
}
function normalizeString(value: string | number | null | undefined) {
if (value === null || value === undefined) {
return '';
}
return String(value);
}
function normalizeRoutePath(path: string | null | undefined) {
const normalizedPath = normalizeString(path).trim();
if (!normalizedPath) {
return '';
}
if (normalizedPath.startsWith('/')) {
return normalizedPath;
}
return `/${normalizedPath}`;
}
function normalizeCurrentProduct(
product: BackendProductContextProductDTO
): Record<'id' | 'code' | 'directionCode' | 'name' | 'managerUserId' | 'statusCode', string> {
return {
id: normalizeStringId(product.id || ''),
code: normalizeString(product.code),
directionCode: normalizeString(product.directionCode),
name: normalizeString(product.name),
managerUserId: normalizeNullableStringId(product.managerUserId) ?? '',
statusCode: normalizeString(product.statusCode)
};
}
function normalizeCurrentRole(role: BackendProductContextRoleDTO) {
return {
roleId: normalizeStringId(role.roleId || ''),
roleCode: normalizeString(role.roleCode),
roleName: normalizeString(role.roleName)
};
}
function normalizeMenu(menu: BackendObjectContextMenuDTO): App.ObjectContext.Menu {
const routeKey = normalizeString(menu.routeKey);
const routePath = normalizeRoutePath(menu.routePath || menu.path);
const key = normalizeString(menu.key || routeKey || routePath || menu.id);
return {
key,
label: normalizeString(menu.label || menu.name),
routeKey: routeKey || null,
routePath: routePath || null,
children: menu.children?.map(child => normalizeMenu(child)) || []
};
}
function getFirstRoutableMenu(menus: App.ObjectContext.Menu[]): App.ObjectContext.Menu | null {
for (const menu of menus) {
if (menu.routeKey || menu.routePath) {
return menu;
}
const firstChildMenu = menu.children?.length ? getFirstRoutableMenu(menu.children) : null;
if (firstChildMenu) {
return firstChildMenu;
}
}
return null;
}
function normalizeObjectSummary(data: BackendObjectContextDTO): App.ObjectContext.Summary | null {
if (data.objectSummary) {
return data.objectSummary;
}
const summary: App.ObjectContext.Summary = {};
if (data.currentProduct) {
summary.currentProduct = normalizeCurrentProduct(data.currentProduct);
}
if (data.currentRole !== undefined) {
summary.currentRole = data.currentRole ? normalizeCurrentRole(data.currentRole) : null;
}
return Object.keys(summary).length ? summary : null;
}
import { type ServiceRequestResult, safeJsonRequestConfig } from './shared';
import { type BackendObjectContextDTO, normalizeObjectContext } from './object-context-normalize';
function createContextApiUrl(config: App.ObjectContext.DomainConfig, objectId: string) {
if (config.contextApiObjectIdPlacement !== 'path') {
@@ -151,30 +13,6 @@ function createContextApiUrl(config: App.ObjectContext.DomainConfig, objectId: s
return config.contextApiPath.replace(placeholder, encodeURIComponent(objectId));
}
function normalizeObjectContext(
config: App.ObjectContext.DomainConfig,
objectId: string,
data: BackendObjectContextDTO
): Api.ObjectContext.ContextInfo {
const rawMenus = data.contextScopedMenus ?? data.menus ?? data.navs ?? [];
const contextScopedMenus = rawMenus.map(menu => normalizeMenu(menu));
const firstRoutableMenu = getFirstRoutableMenu(contextScopedMenus);
const currentProduct = data.currentProduct ? normalizeCurrentProduct(data.currentProduct) : null;
return {
domainKey: (data.domainKey || config.domainKey) as App.ObjectContext.DomainKey,
objectType: (data.objectType || config.objectType) as App.ObjectContext.ObjectType,
objectId: normalizeString(data.objectId) || currentProduct?.id || objectId,
objectName: normalizeString(data.objectName || currentProduct?.name),
objectSummary: normalizeObjectSummary(data),
contextScopedMenus,
buttonCodes: data.buttonCodes ?? data.buttons ?? [],
defaultRouteKey: data.defaultRouteKey || firstRoutableMenu?.routeKey || '',
defaultRoutePath:
normalizeRoutePath(data.defaultRoutePath) || firstRoutableMenu?.routePath || config.fallbackDefaultRoutePath
};
}
export async function fetchGetObjectContext(
config: App.ObjectContext.DomainConfig,
objectId: string

View File

@@ -106,6 +106,15 @@ export async function fetchGetProductPage(params?: Api.Product.ProductSearchPara
}));
}
/** 获取产品入口页概览统计 */
export function fetchGetProductOverviewSummary() {
return request<Api.Product.ProductOverviewSummary>({
...safeJsonRequestConfig,
url: `${PRODUCT_PREFIX}/overview-summary`,
method: 'get'
});
}
/** 鑾峰彇浜у搧璇︽儏 */
export async function fetchGetProduct(id: string) {
const result = await request<ProductResponse>({

View File

@@ -0,0 +1,244 @@
import { normalizeNullableStringId, normalizeStringId } from './shared';
type ProjectStatusCode = Api.Project.ProjectStatusCode;
type ProjectStatusActionCode = Exclude<Api.Project.ProjectStatusActionCode, 'auto_start'>;
type StringIdResponse = string | number;
export type ProjectLocalDateValue = string | number[] | null;
export type LifecycleActionResponse<ActionCode extends string> = Partial<Api.Project.LifecycleAction<ActionCode>> & {
actionCode: ActionCode;
};
export type ProjectExecutionResponse = Omit<
Api.Project.ProjectExecution,
| 'id'
| 'projectId'
| 'projectRequirementId'
| 'ownerId'
| 'availableActions'
| 'plannedStartDate'
| 'plannedEndDate'
| 'actualStartDate'
| 'actualEndDate'
| 'progressRate'
> & {
id: StringIdResponse;
projectId: StringIdResponse;
projectRequirementId?: StringIdResponse | null;
ownerId: StringIdResponse;
availableActions?: LifecycleActionResponse<Api.Project.ProjectExecutionActionCode>[] | null;
plannedStartDate?: ProjectLocalDateValue;
plannedEndDate?: ProjectLocalDateValue;
actualStartDate?: ProjectLocalDateValue;
actualEndDate?: ProjectLocalDateValue;
progressRate?: number | null;
};
export type ExecutionMemberResponse = Omit<Api.Project.ExecutionMember, 'id' | 'executionId' | 'userId'> & {
id: StringIdResponse;
executionId: StringIdResponse;
userId: StringIdResponse;
};
export type ExecutionMemberLogResponse = Omit<
Api.Project.ExecutionMemberLog,
'id' | 'executionId' | 'userId' | 'operatorUserId'
> & {
id: StringIdResponse;
executionId: StringIdResponse;
userId: StringIdResponse;
operatorUserId: StringIdResponse;
};
export type ProjectTaskResponse = Omit<
Api.Project.ProjectTask,
| 'id'
| 'projectId'
| 'executionId'
| 'parentTaskId'
| 'ownerId'
| 'availableActions'
| 'plannedStartDate'
| 'plannedEndDate'
| 'actualStartDate'
| 'actualEndDate'
| 'progressRate'
> & {
id: StringIdResponse;
projectId: StringIdResponse;
executionId: StringIdResponse;
parentTaskId?: StringIdResponse | null;
ownerId: StringIdResponse;
availableActions?: LifecycleActionResponse<Api.Project.ProjectTaskActionCode>[] | null;
plannedStartDate?: ProjectLocalDateValue;
plannedEndDate?: ProjectLocalDateValue;
actualStartDate?: ProjectLocalDateValue;
actualEndDate?: ProjectLocalDateValue;
progressRate?: number | null;
};
export interface ProjectMemberResponse {
id: string | number;
userId: string | number;
userNickname: string;
roleId: string | number;
roleName: string;
roleCode: string;
managerFlag: boolean;
status: 0 | 1;
joinedTime: string;
leftTime?: string | null;
remark?: string | null;
}
const projectLifecycleActionNameMap: Record<ProjectStatusActionCode, string> = {
pause: '暂停项目',
resume: '恢复项目',
complete: '完成项目',
cancel: '取消项目',
reopen: '重新开启',
archive: '归档项目'
};
const projectLifecycleActionReasonRequiredMap: Record<ProjectStatusActionCode, boolean> = {
pause: true,
resume: false,
complete: true,
cancel: true,
reopen: true,
archive: false
};
const projectLifecycleActionMap: Record<ProjectStatusCode, ProjectStatusActionCode[]> = {
pending: ['cancel'],
active: ['pause', 'complete', 'cancel'],
paused: ['resume', 'cancel'],
completed: ['reopen', 'archive'],
cancelled: [],
archived: []
};
export function getProjectLifecycleActions(statusCode: ProjectStatusCode): Api.Project.ProjectLifecycleAction[] {
return projectLifecycleActionMap[statusCode].map(actionCode => ({
actionCode,
actionName: projectLifecycleActionNameMap[actionCode],
needReason: projectLifecycleActionReasonRequiredMap[actionCode]
}));
}
export function normalizeProjectLocalDate(value: ProjectLocalDateValue | undefined) {
if (value === null || value === undefined || value === '') {
return null;
}
if (Array.isArray(value)) {
const [year, month, day] = value;
if (!year || !month || !day) {
return null;
}
return [year, month, day].map(item => String(item).padStart(2, '0')).join('-');
}
return String(value);
}
export function normalizeLifecycleActions<ActionCode extends string>(
actions: LifecycleActionResponse<ActionCode>[] | null | undefined
): Api.Project.LifecycleAction<ActionCode>[] {
return (actions ?? []).map(action => ({
actionCode: action.actionCode,
actionName: action.actionName ?? '',
needReason: Boolean(action.needReason)
}));
}
export function normalizeProjectMember(response: ProjectMemberResponse): Api.Project.ProjectMember {
return {
id: normalizeStringId(response.id),
userId: normalizeStringId(response.userId),
userNickname: response.userNickname || '',
roleId: normalizeStringId(response.roleId),
roleName: response.roleName || '',
roleCode: response.roleCode || '',
managerFlag: Boolean(response.managerFlag),
status: response.status,
joinedTime: response.joinedTime,
leftTime: response.leftTime ?? null,
remark: response.remark ?? null
};
}
export function normalizeProjectExecution(response: ProjectExecutionResponse): Api.Project.ProjectExecution {
return {
...response,
id: normalizeStringId(response.id),
projectId: normalizeStringId(response.projectId),
projectRequirementId: normalizeNullableStringId(response.projectRequirementId),
ownerId: normalizeStringId(response.ownerId),
ownerNickname: response.ownerNickname ?? null,
statusName: response.statusName ?? null,
terminal: Boolean(response.terminal),
allowEdit: Boolean(response.allowEdit),
availableActions: normalizeLifecycleActions(response.availableActions),
plannedStartDate: normalizeProjectLocalDate(response.plannedStartDate),
plannedEndDate: normalizeProjectLocalDate(response.plannedEndDate),
actualStartDate: normalizeProjectLocalDate(response.actualStartDate),
actualEndDate: normalizeProjectLocalDate(response.actualEndDate),
progressRate: typeof response.progressRate === 'number' ? response.progressRate : 0,
executionDesc: response.executionDesc ?? null,
lastStatusReason: response.lastStatusReason ?? null
};
}
export function normalizeExecutionMember(response: ExecutionMemberResponse): Api.Project.ExecutionMember {
return {
...response,
id: normalizeStringId(response.id),
executionId: normalizeStringId(response.executionId),
userId: normalizeStringId(response.userId),
userNickname: response.userNickname ?? null,
joinedAt: response.joinedAt ?? null,
removedAt: response.removedAt ?? null,
removedReason: response.removedReason ?? null
};
}
export function normalizeExecutionMemberLog(response: ExecutionMemberLogResponse): Api.Project.ExecutionMemberLog {
return {
...response,
id: normalizeStringId(response.id),
executionId: normalizeStringId(response.executionId),
userId: normalizeStringId(response.userId),
operatorUserId: normalizeStringId(response.operatorUserId),
userNicknameSnapshot: response.userNicknameSnapshot ?? null,
operatorNicknameSnapshot: response.operatorNicknameSnapshot ?? null,
reason: response.reason ?? null
};
}
export function normalizeProjectTask(response: ProjectTaskResponse): Api.Project.ProjectTask {
return {
...response,
id: normalizeStringId(response.id),
projectId: normalizeStringId(response.projectId),
executionId: normalizeStringId(response.executionId),
parentTaskId: normalizeNullableStringId(response.parentTaskId),
ownerId: normalizeStringId(response.ownerId),
ownerNickname: response.ownerNickname ?? null,
statusName: response.statusName ?? null,
terminal: Boolean(response.terminal),
allowEdit: Boolean(response.allowEdit),
availableActions: normalizeLifecycleActions(response.availableActions),
progressRate: typeof response.progressRate === 'number' ? response.progressRate : 0,
plannedStartDate: normalizeProjectLocalDate(response.plannedStartDate),
plannedEndDate: normalizeProjectLocalDate(response.plannedEndDate),
actualStartDate: normalizeProjectLocalDate(response.actualStartDate),
actualEndDate: normalizeProjectLocalDate(response.actualEndDate),
taskDesc: response.taskDesc ?? null,
lastStatusReason: response.lastStatusReason ?? null
};
}

532
src/service/api/project.ts Normal file
View File

@@ -0,0 +1,532 @@
import { WEB_SERVICE_PREFIX } from '@/constants/service';
import { request } from '../request';
import {
type ServiceRequestResult,
mapServiceResult,
normalizeNullableStringId,
normalizeStringId,
safeJsonRequestConfig
} from './shared';
import {
type ExecutionMemberLogResponse,
type ExecutionMemberResponse,
type ProjectExecutionResponse,
type ProjectLocalDateValue,
type ProjectMemberResponse,
type ProjectTaskResponse,
getProjectLifecycleActions,
normalizeExecutionMember,
normalizeExecutionMemberLog,
normalizeProjectExecution,
normalizeProjectLocalDate,
normalizeProjectMember,
normalizeProjectTask
} from './project-shared';
const PROJECT_PREFIX = `${WEB_SERVICE_PREFIX}/project/project`;
type ProjectResponse = Omit<
Api.Project.Project,
'id' | 'managerUserId' | 'productId' | 'plannedStartDate' | 'plannedEndDate' | 'actualStartDate' | 'actualEndDate'
> & {
id: string | number;
managerUserId?: string | number | null;
productId?: string | number | null;
plannedStartDate?: ProjectLocalDateValue;
plannedEndDate?: ProjectLocalDateValue;
actualStartDate?: ProjectLocalDateValue;
actualEndDate?: ProjectLocalDateValue;
};
type ProjectPageResponse = Api.Project.PageResult<ProjectResponse>;
type ProjectExecutionPageResponse = Api.Project.PageResult<ProjectExecutionResponse>;
type ProjectTaskPageResponse = Api.Project.PageResult<ProjectTaskResponse>;
type StatusBoardResponse = Api.Project.StatusBoard;
type ProjectContextResponse = Omit<Api.Project.ProjectContext, 'currentProject' | 'navs'> & {
currentProject: Omit<Api.Project.ProjectContext['currentProject'], 'id'> & { id: string | number };
navs: Array<Omit<Api.Project.ProjectContext['navs'][number], 'id'> & { id: string | number }>;
};
function getExecutionPrefix(projectId: string) {
return `${PROJECT_PREFIX}/${projectId}/executions`;
}
function getTaskPrefix(projectId: string, executionId: string) {
return `${getExecutionPrefix(projectId)}/${executionId}/tasks`;
}
/** 归一化项目数据 */
function normalizeProject(project: ProjectResponse): Api.Project.Project {
return {
...project,
id: normalizeStringId(project.id),
managerUserId: normalizeNullableStringId(project.managerUserId) ?? '',
productId: normalizeNullableStringId(project.productId),
plannedStartDate: normalizeProjectLocalDate(project.plannedStartDate),
plannedEndDate: normalizeProjectLocalDate(project.plannedEndDate),
actualStartDate: normalizeProjectLocalDate(project.actualStartDate),
actualEndDate: normalizeProjectLocalDate(project.actualEndDate)
};
}
/** 将项目详情组装为设置页数据 */
function createProjectSettings(project: Api.Project.Project): Api.Project.ProjectSettings {
return {
baseInfo: {
id: project.id,
projectCode: project.projectCode,
projectName: project.projectName,
directionCode: project.directionCode,
projectType: project.projectType,
productId: project.productId,
productName: project.productName ?? null,
managerUserId: project.managerUserId,
managerUserNickname: project.managerUserNickname ?? null,
statusCode: project.statusCode,
plannedStartDate: project.plannedStartDate,
plannedEndDate: project.plannedEndDate,
actualStartDate: project.actualStartDate,
actualEndDate: project.actualEndDate,
projectDesc: project.projectDesc,
lastStatusReason: project.lastStatusReason
},
lifecycle: {
statusCode: project.statusCode,
lastStatusReason: project.lastStatusReason,
availableActions: getProjectLifecycleActions(project.statusCode)
}
};
}
/** 获取项目分页 */
export async function fetchGetProjectPage(params?: Api.Project.ProjectSearchParams) {
const result = await request<ProjectPageResponse>({
...safeJsonRequestConfig,
url: `${PROJECT_PREFIX}/page`,
method: 'get',
params
});
return mapServiceResult(result as ServiceRequestResult<ProjectPageResponse>, data => ({
...data,
list: data.list.map(normalizeProject)
}));
}
/** 获取项目入口页概览统计 */
export function fetchGetProjectOverviewSummary() {
return request<Api.Project.ProjectOverviewSummary>({
...safeJsonRequestConfig,
url: `${PROJECT_PREFIX}/overview-summary`,
method: 'get'
});
}
/** 获取项目详情 */
export async function fetchGetProject(id: string) {
const result = await request<ProjectResponse>({
...safeJsonRequestConfig,
url: `${PROJECT_PREFIX}/get`,
method: 'get',
params: { id }
});
return mapServiceResult(result as ServiceRequestResult<ProjectResponse>, normalizeProject);
}
/** 创建项目 */
export async function fetchCreateProject(data: Api.Project.SaveProjectParams) {
const result = await request<string | number>({
...safeJsonRequestConfig,
url: `${PROJECT_PREFIX}/create`,
method: 'post',
data
});
return mapServiceResult(result as ServiceRequestResult<string | number>, normalizeStringId);
}
/** 更新项目 */
export function fetchUpdateProject(data: Api.Project.UpdateProjectParams) {
return request<boolean>({
url: `${PROJECT_PREFIX}/update`,
method: 'put',
data
});
}
/** 变更项目状态 */
export function fetchChangeProjectStatus(data: Api.Project.ChangeProjectStatusParams) {
return request<boolean>({
url: `${PROJECT_PREFIX}/change-status`,
method: 'post',
data
});
}
/** 删除项目 */
export function fetchDeleteProject(data: Api.Project.DeleteProjectParams) {
return request<boolean>({
url: `${PROJECT_PREFIX}/delete`,
method: 'post',
data
});
}
/** 获取项目上下文 */
export async function fetchGetProjectContext(id: string) {
const result = await request<ProjectContextResponse>({
...safeJsonRequestConfig,
url: `${PROJECT_PREFIX}/${id}/context`,
method: 'get'
});
return mapServiceResult(result as ServiceRequestResult<ProjectContextResponse>, data => ({
...data,
currentProject: {
...data.currentProject,
id: normalizeStringId(data.currentProject.id)
},
navs: data.navs.map(nav => ({
...nav,
id: normalizeStringId(nav.id)
}))
}));
}
/** 获取项目成员列表 */
export async function fetchGetProjectMembers(id: string) {
const result = await request<ProjectMemberResponse[]>({
...safeJsonRequestConfig,
url: `${PROJECT_PREFIX}/${id}/members`,
method: 'get'
});
return mapServiceResult(result as ServiceRequestResult<ProjectMemberResponse[]>, data =>
data.map(normalizeProjectMember)
);
}
/** 创建项目成员 */
export async function fetchCreateProjectMember(id: string, data: Api.Project.CreateProjectMemberParams) {
const result = await request<string | number>({
...safeJsonRequestConfig,
url: `${PROJECT_PREFIX}/${id}/members`,
method: 'post',
data
});
return mapServiceResult(result as ServiceRequestResult<string | number>, normalizeStringId);
}
/** 更新项目成员 */
export function fetchUpdateProjectMember(id: string, memberId: string, data: Api.Project.UpdateProjectMemberParams) {
return request<boolean>({
...safeJsonRequestConfig,
url: `${PROJECT_PREFIX}/${id}/members/${memberId}`,
method: 'put',
data
});
}
/** 移出项目成员 */
export function fetchInactiveProjectMember(
id: string,
memberId: string,
data: Api.Project.InactiveProjectMemberParams
) {
return request<boolean>({
...safeJsonRequestConfig,
url: `${PROJECT_PREFIX}/${id}/members/${memberId}/inactive`,
method: 'post',
data
});
}
/** 获取项目设置 */
export async function fetchGetProjectSettings(id: string) {
const result = await fetchGetProject(id);
if (result.error || !result.data) {
return result as ServiceRequestResult<Api.Project.ProjectSettings>;
}
return {
...result,
data: createProjectSettings(result.data)
};
}
/** 更新项目设置基础信息 */
export async function fetchUpdateProjectSettingBaseInfo(
id: string,
data: Api.Project.UpdateProjectSettingBaseInfoParams
) {
const detailResult = await fetchGetProject(id);
if (detailResult.error || !detailResult.data) {
return detailResult as ServiceRequestResult<boolean>;
}
return fetchUpdateProject({
id,
projectCode: detailResult.data.projectCode,
projectName: data.projectName,
directionCode: data.directionCode,
projectType: data.projectType,
productId: detailResult.data.productId,
managerUserId: detailResult.data.managerUserId,
plannedStartDate: data.plannedStartDate,
plannedEndDate: data.plannedEndDate,
actualStartDate: detailResult.data.actualStartDate,
actualEndDate: detailResult.data.actualEndDate,
projectDesc: data.projectDesc
});
}
/** 获取项目执行分页 */
export async function fetchGetProjectExecutionPage(
projectId: string,
params?: Api.Project.ProjectExecutionSearchParams
) {
const result = await request<ProjectExecutionPageResponse>({
...safeJsonRequestConfig,
url: `${getExecutionPrefix(projectId)}/page`,
method: 'get',
params
});
return mapServiceResult(result as ServiceRequestResult<ProjectExecutionPageResponse>, data => ({
...data,
list: data.list.map(normalizeProjectExecution)
}));
}
/** 获取项目执行状态看板 */
export function fetchGetProjectExecutionStatusBoard(
projectId: string,
params?: Api.Project.ProjectExecutionStatusBoardParams
) {
return request<StatusBoardResponse>({
...safeJsonRequestConfig,
url: `${getExecutionPrefix(projectId)}/status-board`,
method: 'get',
params
});
}
/** 获取项目执行详情 */
export async function fetchGetProjectExecution(projectId: string, executionId: string) {
const result = await request<ProjectExecutionResponse>({
...safeJsonRequestConfig,
url: `${getExecutionPrefix(projectId)}/${executionId}`,
method: 'get'
});
return mapServiceResult(result as ServiceRequestResult<ProjectExecutionResponse>, normalizeProjectExecution);
}
/** 创建项目执行 */
export async function fetchCreateProjectExecution(projectId: string, data: Api.Project.SaveProjectExecutionParams) {
const result = await request<string | number>({
...safeJsonRequestConfig,
url: getExecutionPrefix(projectId),
method: 'post',
data
});
return mapServiceResult(result as ServiceRequestResult<string | number>, normalizeStringId);
}
/** 更新项目执行 */
export function fetchUpdateProjectExecution(
projectId: string,
executionId: string,
data: Api.Project.SaveProjectExecutionParams
) {
return request<boolean>({
...safeJsonRequestConfig,
url: `${getExecutionPrefix(projectId)}/${executionId}`,
method: 'put',
data
});
}
/** 变更项目执行负责人 */
export function fetchChangeProjectExecutionOwner(
projectId: string,
executionId: string,
data: Api.Project.ChangeExecutionOwnerParams
) {
return request<boolean>({
...safeJsonRequestConfig,
url: `${getExecutionPrefix(projectId)}/${executionId}/change-owner`,
method: 'post',
data
});
}
/** 变更项目执行状态 */
export function fetchChangeProjectExecutionStatus(
projectId: string,
executionId: string,
data: Api.Project.ChangeExecutionStatusParams
) {
return request<boolean>({
...safeJsonRequestConfig,
url: `${getExecutionPrefix(projectId)}/${executionId}/change-status`,
method: 'post',
data
});
}
/** 获取项目执行成员 */
export async function fetchGetProjectExecutionMembers(projectId: string, executionId: string) {
const result = await request<ExecutionMemberResponse[]>({
...safeJsonRequestConfig,
url: `${getExecutionPrefix(projectId)}/${executionId}/members`,
method: 'get'
});
return mapServiceResult(result as ServiceRequestResult<ExecutionMemberResponse[]>, data =>
data.map(normalizeExecutionMember)
);
}
/** 创建项目执行成员 */
export async function fetchCreateProjectExecutionMember(
projectId: string,
executionId: string,
data: Api.Project.CreateExecutionMemberParams
) {
const result = await request<string | number>({
...safeJsonRequestConfig,
url: `${getExecutionPrefix(projectId)}/${executionId}/members`,
method: 'post',
data
});
return mapServiceResult(result as ServiceRequestResult<string | number>, normalizeStringId);
}
/** 移除项目执行成员 */
export function fetchInactiveProjectExecutionMember(
projectId: string,
executionId: string,
payload: { memberId: string; data: Api.Project.InactiveExecutionMemberParams }
) {
return request<boolean>({
...safeJsonRequestConfig,
url: `${getExecutionPrefix(projectId)}/${executionId}/members/${payload.memberId}/inactive`,
method: 'post',
data: payload.data
});
}
/** 获取项目执行成员变更历史分页 */
export async function fetchGetProjectExecutionMemberLogPage(
projectId: string,
executionId: string,
params?: Api.Project.ExecutionMemberLogSearchParams
) {
const result = await request<Api.Project.PageResult<ExecutionMemberLogResponse>>({
...safeJsonRequestConfig,
url: `${getExecutionPrefix(projectId)}/${executionId}/member-logs`,
method: 'get',
params
});
return mapServiceResult(result as ServiceRequestResult<Api.Project.PageResult<ExecutionMemberLogResponse>>, data => ({
...data,
list: data.list.map(normalizeExecutionMemberLog)
}));
}
/** 获取项目任务分页 */
export async function fetchGetProjectTaskPage(
projectId: string,
executionId: string,
params?: Api.Project.ProjectTaskSearchParams
) {
const result = await request<ProjectTaskPageResponse>({
...safeJsonRequestConfig,
url: `${getTaskPrefix(projectId, executionId)}/page`,
method: 'get',
params
});
return mapServiceResult(result as ServiceRequestResult<ProjectTaskPageResponse>, data => ({
...data,
list: data.list.map(normalizeProjectTask)
}));
}
/** 获取项目任务状态看板 */
export function fetchGetProjectTaskStatusBoard(
projectId: string,
executionId: string,
params?: Api.Project.ProjectTaskStatusBoardParams
) {
return request<StatusBoardResponse>({
...safeJsonRequestConfig,
url: `${getTaskPrefix(projectId, executionId)}/status-board`,
method: 'get',
params
});
}
/** 获取项目任务详情 */
export async function fetchGetProjectTask(projectId: string, executionId: string, taskId: string) {
const result = await request<ProjectTaskResponse>({
...safeJsonRequestConfig,
url: `${getTaskPrefix(projectId, executionId)}/${taskId}`,
method: 'get'
});
return mapServiceResult(result as ServiceRequestResult<ProjectTaskResponse>, normalizeProjectTask);
}
/** 创建项目任务 */
export async function fetchCreateProjectTask(
projectId: string,
executionId: string,
data: Api.Project.SaveProjectTaskParams
) {
const result = await request<string | number>({
...safeJsonRequestConfig,
url: getTaskPrefix(projectId, executionId),
method: 'post',
data
});
return mapServiceResult(result as ServiceRequestResult<string | number>, normalizeStringId);
}
/** 更新项目任务 */
export function fetchUpdateProjectTask(
projectId: string,
executionId: string,
payload: { taskId: string; data: Api.Project.SaveProjectTaskParams }
) {
return request<boolean>({
...safeJsonRequestConfig,
url: `${getTaskPrefix(projectId, executionId)}/${payload.taskId}`,
method: 'put',
data: payload.data
});
}
/** 变更项目任务状态 */
export function fetchChangeProjectTaskStatus(
projectId: string,
executionId: string,
payload: { taskId: string; data: Api.Project.ChangeTaskStatusParams }
) {
return request<boolean>({
...safeJsonRequestConfig,
url: `${getTaskPrefix(projectId, executionId)}/${payload.taskId}/change-status`,
method: 'post',
data: payload.data
});
}

View File

@@ -95,31 +95,78 @@ function replaceWithStaticObjectContextDomainRoute(routes: Api.Route.MenuRoute[]
return;
}
const wrappedDomainRoute = cloneStaticRouteAsMenuRoute(staticDomainRoute, `object-context:${config.domainKey}`);
const entryRouteIndex = normalizedRoutes.findIndex(route => route.id === entryRoute.id);
const domainRouteIds = new Set(domainTopLevelRoutes.map(route => route.id));
// Create a map of backend routes by name for quick lookup
const backendRouteMap = new Map<string, Api.Route.MenuRoute>();
domainTopLevelRoutes.forEach(route => {
if (route.name) {
backendRouteMap.set(String(route.name), route);
}
});
if (entryRoute.meta) {
const nextMeta: RouteMeta = {
title: wrappedDomainRoute.meta?.title || config.domainKey,
...(wrappedDomainRoute.meta || {})
// Clone static route but preserve backend route's meta for children
// 待重构:拆 helper 以降低复杂度,暂以 disable 注释临时放行
// eslint-disable-next-line complexity
function cloneStaticRoutePreservingBackendMeta(route: ElegantConstRoute, idPrefix: string): Api.Route.MenuRoute {
const backendRoute = route.name ? backendRouteMap.get(String(route.name)) : undefined;
const { children: _children, ...routeWithoutChildren } = route;
const baseRoute: Api.Route.MenuRoute = {
...routeWithoutChildren,
id: `${idPrefix}:${String(route.name || route.path)}`
};
if (entryRoute.meta.icon) {
nextMeta.icon = entryRoute.meta.icon;
// If there's a backend route, preserve its meta
if (backendRoute?.meta) {
baseRoute.meta = {
...baseRoute.meta,
title: backendRoute.meta.title || baseRoute.meta?.title || String(route.name || route.path),
icon: backendRoute.meta.icon || baseRoute.meta?.icon,
localIcon: backendRoute.meta.localIcon || baseRoute.meta?.localIcon,
order:
backendRoute.meta.order !== undefined && backendRoute.meta.order !== null
? backendRoute.meta.order
: baseRoute.meta?.order,
keepAlive:
backendRoute.meta.keepAlive !== undefined && backendRoute.meta.keepAlive !== null
? backendRoute.meta.keepAlive
: baseRoute.meta?.keepAlive,
i18nKey: backendRoute.meta.i18nKey || baseRoute.meta?.i18nKey
};
}
if (entryRoute.meta.localIcon) {
nextMeta.localIcon = entryRoute.meta.localIcon;
// Recursively process children
if (route.children?.length) {
baseRoute.children = route.children.map(child => cloneStaticRoutePreservingBackendMeta(child, idPrefix));
}
if (entryRoute.meta.order !== undefined) {
nextMeta.order = entryRoute.meta.order;
}
wrappedDomainRoute.meta = nextMeta;
return baseRoute;
}
const wrappedDomainRoute = cloneStaticRoutePreservingBackendMeta(
staticDomainRoute,
`object-context:${config.domainKey}`
);
// Merge entry route's meta to domain route
if (entryRoute.meta) {
wrappedDomainRoute.meta = {
...wrappedDomainRoute.meta,
title: entryRoute.meta.title || wrappedDomainRoute.meta?.title || config.domainKey,
icon: entryRoute.meta.icon || wrappedDomainRoute.meta?.icon,
localIcon: entryRoute.meta.localIcon || wrappedDomainRoute.meta?.localIcon,
order:
entryRoute.meta.order !== undefined && entryRoute.meta.order !== null
? entryRoute.meta.order
: wrappedDomainRoute.meta?.order,
keepAlive:
entryRoute.meta.keepAlive !== undefined && entryRoute.meta.keepAlive !== null
? entryRoute.meta.keepAlive
: wrappedDomainRoute.meta?.keepAlive
};
}
const entryRouteIndex = normalizedRoutes.findIndex(route => route.id === entryRoute.id);
const domainRouteIds = new Set(domainTopLevelRoutes.map(route => route.id));
normalizedRoutes = normalizedRoutes.filter(route => !domainRouteIds.has(route.id));
normalizedRoutes.splice(entryRouteIndex < 0 ? normalizedRoutes.length : entryRouteIndex, 0, wrappedDomainRoute);
});

View File

@@ -74,6 +74,7 @@ function createBatchDeleteQuery(ids: Array<string | number>) {
type UserSimpleResponse = Omit<Api.SystemManage.UserSimple, 'id'> & {
id: string | number;
deptId?: string | number | null;
};
type RoleResponse = Omit<Api.SystemManage.Role, 'id'> & {
@@ -120,7 +121,8 @@ type UserManagementRelationTreeResponse = Omit<
function normalizeUserSimple(user: UserSimpleResponse): Api.SystemManage.UserSimple {
return {
...user,
id: normalizeStringId(user.id)
id: normalizeStringId(user.id),
deptId: normalizeNullableStringId(user.deptId)
};
}