diff --git a/src/service/api/product.ts b/src/service/api/product.ts index 00be6f4..4d5f187 100644 --- a/src/service/api/product.ts +++ b/src/service/api/product.ts @@ -91,7 +91,7 @@ function createProductActivityTimelinePageQuery(params: Api.Product.ProductActiv return query.toString(); } -/** 鑾峰彇浜у搧鍒嗛〉 */ +/** 获取产品分页 */ export async function fetchGetProductPage(params?: Api.Product.ProductSearchParams) { const result = await request({ ...safeJsonRequestConfig, @@ -115,7 +115,7 @@ export function fetchGetProductOverviewSummary() { }); } -/** 鑾峰彇浜у搧璇︽儏 */ +/** 获取产品详情 */ export async function fetchGetProduct(id: string) { const result = await request({ ...safeJsonRequestConfig, @@ -127,7 +127,7 @@ export async function fetchGetProduct(id: string) { return mapServiceResult(result as ServiceRequestResult, normalizeProduct); } -/** 鍒涘缓浜у搧 */ +/** 新增产品 */ export async function fetchCreateProduct(data: Api.Product.SaveProductParams) { const result = await request({ ...safeJsonRequestConfig, @@ -139,7 +139,7 @@ export async function fetchCreateProduct(data: Api.Product.SaveProductParams) { return mapServiceResult(result as ServiceRequestResult, normalizeStringId); } -/** 鏇存柊浜у搧 */ +/** 更新产品 */ export function fetchUpdateProduct(data: Api.Product.UpdateProductParams) { return request({ url: `${PRODUCT_PREFIX}/update`, @@ -148,7 +148,7 @@ export function fetchUpdateProduct(data: Api.Product.UpdateProductParams) { }); } -/** 鍙樻洿浜у搧鐘舵€? */ +/** 改变产品状态 */ export function fetchChangeProductStatus(data: Api.Product.ChangeProductStatusParams) { return request({ url: `${PRODUCT_PREFIX}/change-status`, @@ -157,7 +157,7 @@ export function fetchChangeProductStatus(data: Api.Product.ChangeProductStatusPa }); } -/** 鍒犻櫎浜у搧 */ +/** 删除产品 */ export function fetchDeleteProduct(data: Api.Product.DeleteProductParams) { return request({ url: `${PRODUCT_PREFIX}/delete`, @@ -353,6 +353,26 @@ export async function fetchGetRequirementTerminalStatusDict() { return mapServiceResult(result as ServiceRequestResult, data => data); } +/** 判断产品需求是否已分流生成项目需求 */ +export async function fetchHasDispatchedProjectRequirement(requirementId: string, productId: string) { + return request({ + ...safeJsonRequestConfig, + url: `${REQUIREMENT_PREFIX}/has-dispatched`, + method: 'get', + params: { requirementId, productId } + }); +} + +/** 根据当前产品需求id获取对应地,所流转到项目侧的项目需求id */ +export async function fetchGetDispatchedProjectLink(productRequirementId: string) { + return request<{ projectRequirementId: string; projectId: string }>({ + ...safeJsonRequestConfig, + url: `${REQUIREMENT_PREFIX}/dispatched-project-link`, + method: 'get', + params: { productRequirementId } + }); +} + // ========== 模块管理 API ========== type RequirementModuleResponse = Omit & { id: string | number; diff --git a/src/service/api/project.ts b/src/service/api/project.ts index 995e528..df085a7 100644 --- a/src/service/api/project.ts +++ b/src/service/api/project.ts @@ -542,3 +542,256 @@ export function fetchChangeProjectTaskStatus( data: payload.data }); } + +// ========== 项目需求 API ========== +const PROJECT_REQUIREMENT_PREFIX = `${WEB_SERVICE_PREFIX}/project/project/requirement`; + +type ProjectRequirementResponse = Omit< + Api.Project.ProjectRequirement, + 'id' | 'projectId' | 'parentId' | 'moduleId' | 'proposerId' | 'currentHandlerUserId' | 'sourceBizId' +> & { + id: string | number; + projectId: string | number; + parentId: string | number; + moduleId: string | number; + proposerId: string | number; + currentHandlerUserId?: string | number | null; + sourceBizId?: string | number | null; + children?: ProjectRequirementResponse[]; +}; + +type ProjectRequirementPageResponse = Api.Project.PageResult; + +type ProjectRequirementModuleResponse = Omit & { + id: string | number; + parentId: string | number; + projectId: string | number; + children?: ProjectRequirementModuleResponse[]; +}; + +function normalizeProjectRequirement(requirement: ProjectRequirementResponse): Api.Project.ProjectRequirement { + return { + ...requirement, + id: normalizeStringId(requirement.id), + projectId: normalizeStringId(requirement.projectId), + parentId: normalizeStringId(requirement.parentId), + moduleId: normalizeStringId(requirement.moduleId), + proposerId: normalizeStringId(requirement.proposerId), + currentHandlerUserId: normalizeNullableStringId(requirement.currentHandlerUserId), + sourceBizId: normalizeNullableStringId(requirement.sourceBizId), + children: requirement.children?.map(normalizeProjectRequirement) + }; +} + +function normalizeProjectRequirementModule( + module: ProjectRequirementModuleResponse +): Api.Project.ProjectRequirementModule { + return { + ...module, + id: normalizeStringId(module.id), + parentId: normalizeStringId(module.parentId), + projectId: normalizeStringId(module.projectId), + children: module.children?.map(normalizeProjectRequirementModule) + }; +} + +/** 获取项目需求分页列表 */ +export async function fetchGetProjectRequirementPage(params?: Api.Project.ProjectRequirementSearchParams) { + const result = await request({ + ...safeJsonRequestConfig, + url: `${PROJECT_REQUIREMENT_PREFIX}/page`, + method: 'get', + params + }); + + return mapServiceResult(result as ServiceRequestResult, data => ({ + ...data, + list: data.list.map(normalizeProjectRequirement) + })); +} + +/** 获取项目需求树形列表 */ +export async function fetchGetProjectRequirementTree(params?: Api.Project.ProjectRequirementSearchParams) { + const result = await request({ + ...safeJsonRequestConfig, + url: `${PROJECT_REQUIREMENT_PREFIX}/tree`, + method: 'get', + params + }); + + return mapServiceResult(result as ServiceRequestResult, data => ({ + ...data, + list: data.list.map(normalizeProjectRequirement) + })); +} + +/** 获取项目需求详情 */ +export async function fetchGetProjectRequirement(id: string, projectId: string) { + const result = await request({ + ...safeJsonRequestConfig, + url: `${PROJECT_REQUIREMENT_PREFIX}/get`, + method: 'get', + params: { id, projectId } + }); + + return mapServiceResult(result as ServiceRequestResult, normalizeProjectRequirement); +} + +/** 创建项目需求 */ +export async function fetchCreateProjectRequirement(data: Api.Project.SaveProjectRequirementParams) { + const result = await request({ + ...safeJsonRequestConfig, + url: `${PROJECT_REQUIREMENT_PREFIX}/create`, + method: 'post', + data + }); + + return mapServiceResult(result as ServiceRequestResult, normalizeStringId); +} + +/** 更新项目需求 */ +export function fetchUpdateProjectRequirement(data: Api.Project.UpdateProjectRequirementParams) { + return request({ + ...safeJsonRequestConfig, + url: `${PROJECT_REQUIREMENT_PREFIX}/update`, + method: 'put', + data + }); +} + +/** 变更项目需求状态 */ +export function fetchChangeProjectRequirementStatus(data: Api.Project.ChangeProjectRequirementStatusParams) { + return request({ + ...safeJsonRequestConfig, + url: `${PROJECT_REQUIREMENT_PREFIX}/change-status`, + method: 'post', + data + }); +} + +/** 删除项目需求 */ +export function fetchDeleteProjectRequirement(data: Api.Project.DeleteProjectRequirementParams) { + return request({ + ...safeJsonRequestConfig, + url: `${PROJECT_REQUIREMENT_PREFIX}/delete`, + method: 'post', + data + }); +} + +/** 拆分项目需求 */ +export async function fetchSplitProjectRequirement(data: Api.Project.SplitProjectRequirementParams) { + const result = await request({ + ...safeJsonRequestConfig, + url: `${PROJECT_REQUIREMENT_PREFIX}/split`, + method: 'post', + data + }); + + return mapServiceResult(result as ServiceRequestResult, normalizeStringId); +} + +/** 关闭项目需求 */ +export function fetchCloseProjectRequirement(data: Api.Project.CloseProjectRequirementParams) { + return request({ + ...safeJsonRequestConfig, + url: `${PROJECT_REQUIREMENT_PREFIX}/close`, + method: 'post', + data + }); +} + +/** 获取项目需求可执行状态动作列表 */ +export async function fetchGetProjectRequirementAllowedTransitions(requirementId: string, projectId: string) { + const result = await request({ + ...safeJsonRequestConfig, + url: `${PROJECT_REQUIREMENT_PREFIX}/allowed-transitions`, + method: 'get', + params: { requirementId, projectId } + }); + + return mapServiceResult( + result as ServiceRequestResult, + data => data + ); +} + +/** 获取项目需求生命周期信息 */ +export async function fetchGetProjectRequirementLifecycle(requirementId: string, projectId: string) { + const result = await request({ + ...safeJsonRequestConfig, + url: `${PROJECT_REQUIREMENT_PREFIX}/lifecycle`, + method: 'get', + params: { requirementId, projectId } + }); + + return mapServiceResult(result as ServiceRequestResult, data => data); +} + +/** 获取项目需求状态字典 */ +export async function fetchGetProjectRequirementStatusDict() { + const result = await request({ + ...safeJsonRequestConfig, + url: `${PROJECT_REQUIREMENT_PREFIX}/status/dict`, + method: 'get' + }); + + return mapServiceResult(result as ServiceRequestResult, data => data); +} + +/** 获取项目需求终态状态字典 */ +export async function fetchGetProjectRequirementTerminalStatusDict() { + const result = await request({ + ...safeJsonRequestConfig, + url: `${PROJECT_REQUIREMENT_PREFIX}/status/dict/terminal`, + method: 'get' + }); + + return mapServiceResult(result as ServiceRequestResult, data => data); +} + +/** 获取项目需求模块树 */ +export async function fetchGetProjectRequirementModuleTree(projectId: string) { + const result = await request({ + ...safeJsonRequestConfig, + url: `${PROJECT_REQUIREMENT_PREFIX}/module/tree`, + method: 'get', + params: { projectId } + }); + + return mapServiceResult(result as ServiceRequestResult, data => + data.map(normalizeProjectRequirementModule) + ); +} + +/** 创建项目需求模块 */ +export async function fetchCreateProjectRequirementModule(data: Api.Project.SaveProjectRequirementModuleParams) { + const result = await request({ + ...safeJsonRequestConfig, + url: `${PROJECT_REQUIREMENT_PREFIX}/module/create`, + method: 'post', + data + }); + + return mapServiceResult(result as ServiceRequestResult, normalizeStringId); +} + +/** 更新项目需求模块 */ +export function fetchUpdateProjectRequirementModule(data: Api.Project.SaveProjectRequirementModuleParams) { + return request({ + ...safeJsonRequestConfig, + url: `${PROJECT_REQUIREMENT_PREFIX}/module/update`, + method: 'put', + data + }); +} + +/** 删除项目需求模块 */ +export function fetchDeleteProjectRequirementModule(data: Api.Project.DeleteProjectRequirementModuleParams) { + return request({ + ...safeJsonRequestConfig, + url: `${PROJECT_REQUIREMENT_PREFIX}/module/delete`, + method: 'post', + data + }); +} diff --git a/src/typings/api/project.d.ts b/src/typings/api/project.d.ts index 0323a9d..12e8a25 100644 --- a/src/typings/api/project.d.ts +++ b/src/typings/api/project.d.ts @@ -446,5 +446,219 @@ declare namespace Api { interface InactiveProjectMemberParams { reason: string | null; } + + // ========== 项目需求相关类型定义 ========== + /** 项目需求状态编码 */ + type ProjectRequirementStatusCode = + | 'pending_confirm' + | 'pending_review' + | 'implementing' + | 'accepted' + | 'closed' + | 'rejected' + | 'cancelled'; + + /** 项目需求来源类型 */ + type ProjectRequirementSourceType = 'manual' | 'work_order' | 'product_requirement'; + + /** 项目需求优先级 */ + type ProjectRequirementPriority = 0 | 1 | 2 | 3; + + /** 是否需要评审 */ + type ProjectRequirementReviewRequired = 0 | 1; + + interface ProjectRequirement { + /** 需求 ID */ + id: string; + /** 所属项目 ID */ + projectId: string; + /** 父需求 ID,0 表示顶级需求 */ + parentId: string; + /** 所属模块 ID */ + moduleId: string; + /** 是否需要评审 */ + reviewRequired: ProjectRequirementReviewRequired; + /** 需求标题 */ + title: string; + /** 需求描述 */ + description?: string | null; + /** 需求分类字典值 */ + category: string; + /** 需求分类名称 */ + categoryName?: string | null; + /** 需求来源类型 */ + sourceType: ProjectRequirementSourceType; + /** 来源业务 ID */ + sourceBizId?: string | null; + /** 优先级 */ + priority: ProjectRequirementPriority; + /** 优先级名称 */ + priorityName?: string | null; + /** 当前状态编码 */ + statusCode: ProjectRequirementStatusCode; + /** 当前状态名称 */ + statusName?: string | null; + /** 最近一次状态动作原因 */ + lastStatusReason?: string | null; + /** 提出人用户 ID */ + proposerId: string; + /** 提出人昵称 */ + proposerNickname?: string | null; + /** 当前处理人用户 ID */ + currentHandlerUserId?: string | null; + /** 当前处理人昵称 */ + currentHandlerUserNickname?: string | null; + /** 所需工时 */ + workHours: number; + /** 排序值 */ + sort: number; + /** 创建时间 */ + createTime: string; + /** 更新时间 */ + updateTime: string; + /** 子需求列表 */ + children?: ProjectRequirement[]; + /** 是否终态 */ + terminal?: boolean; + } + + interface ProjectRequirementModule { + /** 模块 ID */ + id: string; + /** 父模块 ID,0 表示顶级 */ + parentId: string; + /** 所属项目 ID */ + projectId: string; + /** 模块名称 */ + moduleName: string; + /** 模块说明 */ + remark?: string | null; + /** 图标 */ + icon?: string | null; + /** 排序值 */ + sort: number; + /** 子模块列表 */ + children?: ProjectRequirementModule[]; + } + + interface ProjectRequirementStatusDict { + /** 状态编码 */ + statusCode: string; + /** 状态名称 */ + statusName: string; + /** 排序值 */ + sort: number; + /** 是否初始状态 */ + initialFlag: boolean; + /** 是否终态 */ + terminalFlag: boolean; + } + + interface ProjectRequirementLifecycleAction { + actionCode: string; + actionName: string; + toStatusCode: string; + toStatusName: string; + needReason: boolean; + } + + interface ProjectRequirementLifecycleInfo { + statusCode: ProjectRequirementStatusCode; + statusName?: string | null; + lastStatusReason?: string | null; + terminal: boolean; + allowEdit: boolean; + availableActions: ProjectRequirementLifecycleAction[]; + } + + /** 项目需求分页查询参数 */ + type ProjectRequirementSearchParams = CommonType.RecordNullable< + Pick & + Pick< + ProjectRequirement, + 'moduleId' | 'parentId' | 'category' | 'priority' | 'statusCode' | 'currentHandlerUserId' | 'sourceType' + > & { + projectId: string; + title: string; + } + >; + + /** 创建项目需求参数 */ + type SaveProjectRequirementParams = Pick< + ProjectRequirement, + | 'projectId' + | 'moduleId' + | 'reviewRequired' + | 'title' + | 'description' + | 'category' + | 'priority' + | 'proposerId' + | 'proposerNickname' + | 'currentHandlerUserId' + | 'currentHandlerUserNickname' + | 'workHours' + | 'sort' + >; + + /** 更新项目需求参数 */ + type UpdateProjectRequirementParams = { id: string } & SaveProjectRequirementParams; + + /** 变更项目需求状态参数 */ + interface ChangeProjectRequirementStatusParams { + id: string; + projectId: string; + actionCode: string; + reason?: string | null; + } + + /** 关闭项目需求参数 */ + interface CloseProjectRequirementParams { + id: string; + projectId: string; + reason: string; + } + + /** 拆分项目需求参数 */ + type SplitProjectRequirementParams = Pick< + ProjectRequirement, + | 'parentId' + | 'projectId' + | 'moduleId' + | 'reviewRequired' + | 'title' + | 'description' + | 'category' + | 'priority' + | 'proposerId' + | 'proposerNickname' + | 'currentHandlerUserId' + | 'currentHandlerUserNickname' + | 'workHours' + | 'sort' + >; + + /** 删除项目需求参数 */ + interface DeleteProjectRequirementParams { + id: string; + projectId: string; + } + + /** 保存项目需求模块参数 */ + interface SaveProjectRequirementModuleParams { + id?: string; + projectId: string; + parentId?: string | null; + moduleName: string; + remark?: string | null; + icon?: string | null; + sort?: number; + } + + /** 删除项目需求模块参数 */ + interface DeleteProjectRequirementModuleParams { + id?: string; + projectId: string; + } } } diff --git a/src/views/product/requirement/index.vue b/src/views/product/requirement/index.vue index ea0e490..3ae3f89 100644 --- a/src/views/product/requirement/index.vue +++ b/src/views/product/requirement/index.vue @@ -1,5 +1,6 @@ + + diff --git a/src/views/project/project/requirement/modules/member-select-option.vue b/src/views/project/project/requirement/modules/member-select-option.vue new file mode 100644 index 0000000..abd57cc --- /dev/null +++ b/src/views/project/project/requirement/modules/member-select-option.vue @@ -0,0 +1,39 @@ + + + + + diff --git a/src/views/project/project/requirement/modules/module-tree-node.vue b/src/views/project/project/requirement/modules/module-tree-node.vue new file mode 100644 index 0000000..64a979d --- /dev/null +++ b/src/views/project/project/requirement/modules/module-tree-node.vue @@ -0,0 +1,332 @@ + + + + + diff --git a/src/views/project/project/requirement/modules/requirement-action-dialog.vue b/src/views/project/project/requirement/modules/requirement-action-dialog.vue new file mode 100644 index 0000000..aa4e8aa --- /dev/null +++ b/src/views/project/project/requirement/modules/requirement-action-dialog.vue @@ -0,0 +1,97 @@ + + + + + diff --git a/src/views/project/project/requirement/modules/requirement-create-dialog.vue b/src/views/project/project/requirement/modules/requirement-create-dialog.vue new file mode 100644 index 0000000..8bc9120 --- /dev/null +++ b/src/views/project/project/requirement/modules/requirement-create-dialog.vue @@ -0,0 +1,303 @@ + + + + + diff --git a/src/views/project/project/requirement/modules/requirement-detail-dialog.vue b/src/views/project/project/requirement/modules/requirement-detail-dialog.vue new file mode 100644 index 0000000..8ea8dca --- /dev/null +++ b/src/views/project/project/requirement/modules/requirement-detail-dialog.vue @@ -0,0 +1,390 @@ + + + + + diff --git a/src/views/project/project/requirement/modules/requirement-module-tree.vue b/src/views/project/project/requirement/modules/requirement-module-tree.vue new file mode 100644 index 0000000..e48b061 --- /dev/null +++ b/src/views/project/project/requirement/modules/requirement-module-tree.vue @@ -0,0 +1,312 @@ + + + + + diff --git a/src/views/project/project/requirement/modules/requirement-search.vue b/src/views/project/project/requirement/modules/requirement-search.vue new file mode 100644 index 0000000..645279c --- /dev/null +++ b/src/views/project/project/requirement/modules/requirement-search.vue @@ -0,0 +1,133 @@ + + + + + diff --git a/src/views/project/project/requirement/modules/requirement-split-dialog.vue b/src/views/project/project/requirement/modules/requirement-split-dialog.vue new file mode 100644 index 0000000..aa45d45 --- /dev/null +++ b/src/views/project/project/requirement/modules/requirement-split-dialog.vue @@ -0,0 +1,241 @@ + + + + + diff --git a/src/views/project/project/requirement/shared/requirement-master-data.ts b/src/views/project/project/requirement/shared/requirement-master-data.ts new file mode 100644 index 0000000..ace86b8 --- /dev/null +++ b/src/views/project/project/requirement/shared/requirement-master-data.ts @@ -0,0 +1,122 @@ +import { markRaw } from 'vue'; +import IconMdiPencilOutline from '~icons/mdi/pencil-outline'; +import IconMdiCheckOutline from '~icons/mdi/check-outline'; +import IconMdiCheckCircleOutline from '~icons/mdi/check-circle-outline'; +import IconMdiSync from '~icons/mdi/sync'; +import IconMdiPowerSettingsNew from '~icons/mdi/power-settings-new'; +import IconMdiDeleteOutline from '~icons/mdi/delete-outline'; +import IconMdiGlasses from '~icons/mdi/glasses'; +import IconMdiClose from '~icons/mdi/close'; +import IconTablerSitemap from '~icons/tabler/sitemap'; +import IconTablerCircleX from '~icons/tabler/circle-x'; + +/** + * 项目需求状态记录 + * + * 来源:rdms_object_status_model 表 object_type='project_requirement' 的 7 条记录 + */ +export const projectRequirementStatusRecord: Record = { + pending_confirm: '待确认', + pending_review: '待评审', + implementing: '实施中', + accepted: '已验收', + closed: '已关闭', + rejected: '已拒绝', + cancelled: '已取消' +}; + +/** + * 终态状态码集合 + * + * 来源:terminal_flag=1 的状态: closed, rejected, cancelled + */ +const TERMINAL_STATUS_SET = new Set(['closed', 'rejected', 'cancelled']); + +/** + * 操作按钮图标映射 + * + * 将操作类型映射到对应的 Iconify 图标组件 + */ +export const ACTION_ICON_MAP: Record = { + split: markRaw(IconTablerSitemap), + edit: markRaw(IconMdiPencilOutline), + claim_to_review: markRaw(IconMdiCheckOutline), + claim_to_implement: markRaw(IconMdiCheckCircleOutline), + pass_review: markRaw(IconMdiGlasses), + accept: markRaw(IconMdiCheckCircleOutline), + reject: markRaw(IconMdiClose), + cancel: markRaw(IconTablerCircleX), + close: markRaw(IconMdiPowerSettingsNew), + delete: markRaw(IconMdiDeleteOutline) +}; + +export const DEFAULT_ACTION_ICON = markRaw(IconMdiSync); + +function resolveActionKeyword(actionCode: string) { + return Object.keys(ACTION_ICON_MAP).find(keyword => actionCode.includes(keyword)); +} + +/** + * 获取项目需求状态的标签颜色 + */ +export function getProjectRequirementStatusTagType(status: Api.Project.ProjectRequirementStatusCode): UI.ThemeColor { + const statusTagTypeMap: Record = { + pending_confirm: 'info', + pending_review: 'warning', + implementing: 'primary', + accepted: 'success', + closed: 'info', + rejected: 'danger', + cancelled: 'danger' + }; + + return statusTagTypeMap[status]; +} + +/** + * 判断状态是否为终态 + */ +export function isProjectRequirementTerminal(statusCode: string) { + return TERMINAL_STATUS_SET.has(statusCode as Api.Project.ProjectRequirementStatusCode); +} + +/** + * 判断动作是否为终态动作 + * + * 终态动作定义:执行后需求进入终态(closed/rejected/cancelled) + * 包括:reject, cancel, close + */ +export function isProjectRequirementActionTerminal(actionCode: string) { + const terminalActions = new Set(['reject', 'cancel', 'close']); + return terminalActions.has(actionCode); +} + +/** + * 获取操作按钮的颜色类型 + * + * 成功/审批类 → success,危险操作 → danger,其他 → primary + */ +export function getProjectRequirementActionButtonType(actionCode: string): 'primary' | 'success' | 'danger' { + if (['reject', 'cancel', 'close', 'delete'].includes(actionCode)) { + return 'danger'; + } + // if (['accept', 'pass_review'].includes(actionCode)) { + // return 'success'; + // } + return 'primary'; +} + +/** + * 获取操作动作的展示名称 + */ +export function getProjectRequirementActionDisplayName(action: Api.Project.ProjectRequirementLifecycleAction) { + return action.actionName || action.actionCode; +} + +/** + * 根据 actionCode 获取对应图标 + */ +export function getProjectRequirementActionIcon(actionCode: string) { + const actionKeyword = resolveActionKeyword(actionCode); + return actionKeyword ? ACTION_ICON_MAP[actionKeyword] : DEFAULT_ACTION_ICON; +}