import type { FileInfo } from '@/types/api' import type { AfterSales, Milestone, Project, ProjectBill, ProjectStatus, RequirementAfterSale, RequirementAttachment, RequirementBillPlan, RequirementContract, RequirementDetailVo, RequirementInvoice, TeamMember, } from '@/types/business/project' import { toBusinessType } from '@/utils/format' import { PROJECT_STATUS_LABEL } from '@/utils/projectMeta' /** * 需求详情(后端 `ProjectRequirementUserVo`)→ 详情页 `Project`。 * *
为什么不直接改组件:详情页 6 个子组件(InfoCard/Team/Milestones/Files/Finance/ * AfterSales)都按 `Project` 这套字段写的,改数据来源不动组件契约,改动面最小。
* *未立项是常态:需求在 `vm_rd_project` 里没有对应行时,后端返回的 * `projectStage` / `projectMilestones` / `members` / `billPlans` / `invoices` / `contracts` * 全为空 —— 这里一律兜底成空数组 / 0 / undefined,详情页显示为空态,不报错。
*/ /** * 状态码值 → 详情页中文。 * *码值域是两张表的并集(见 {@link ProjectStatus}):需求侧 `INITIATING`/`CANCELLED`, * 项目侧 `IN_PROGRESS`/`PENDING_ACCEPTANCE`/… 未知码值兜底「待立项」(未立项是最常见态)。
*/ function toProjectStatus(raw: string | null | undefined): ProjectStatus { const label = PROJECT_STATUS_LABEL[raw ?? ''] return (label as ProjectStatus | undefined) ?? '待立项' } /** * 里程碑码值 → 三态。 * *详情页 `Milestone.status` 只有「待开始 / 进行中 / 已完成」三态(设计稿如此), * 而后端 `status` 是自由文本 ⇒ 按关键词归类,认不出的一律「待开始」。
*/ function toMilestoneStatus(raw: string | null | undefined): Milestone['status'] { const value = (raw ?? '').toUpperCase() if (value.includes('COMPLET') || value.includes('DONE') || value.includes('FINISH')) return '已完成' if (value.includes('PROGRESS') || value.includes('DOING')) return '进行中' return '待开始' } /** * 账单/付款计划状态 → 三态。 * *⚠️ 后端 `BillPlan.status` 的码值语义**未确认**(`vm_bill_plan` 当前 0 行,无样本), * 只认字面量,其余一律「未到期」——不臆造成「已支付」。
*/ function toBillStatus(raw: string | null | undefined): ProjectBill['status'] { const value = (raw ?? '').toUpperCase() if (value.includes('PAID') || value.includes('支付')) return '已支付' if (value.includes('WAIT') || value.includes('PENDING') || value.includes('待支付')) return '待支付' return '未到期' } /** 需求附件 → 详情页文件项(文件 tab 用) */ function attachmentToFile(item: RequirementAttachment): FileInfo { return { name: item.originalName ?? '', size: item.fileSize ?? 0, url: item.url ?? '', uploadTime: item.createTime ?? '', } } /** 发票 → 详情页文件项(财务 tab 的发票记录只是文件列表) */ function invoiceToFile(item: RequirementInvoice): FileInfo { return { name: item.invoiceTitle ?? item.invoiceNo ?? '发票', size: 0, url: item.invoiceFile ?? '', uploadTime: item.invoiceDate ?? item.createTime ?? '', } } /** 合同 → 详情页文件项(财务 tab 只展示一份合同文件,取第一条) */ function contractToFile(item: RequirementContract): FileInfo { return { name: item.title ?? item.contractNo ?? '合同文件', size: 0, url: item.file ?? '', uploadTime: item.signDate ?? item.createTime ?? '', } } /** * 付款计划。 * *`BillPlan` 没有「单据号」也没有「期次名」,所以:`planName` 用 `periodRule`(付不出 * 就用「第 N 期/共 M 期」拼),`billNo` 回落到 `projectNo`。
*/ function toProjectBill(item: RequirementBillPlan): ProjectBill { const period = item.currentPeriod != null && item.periods != null ? `第${item.currentPeriod}期/共${item.periods}期` : '' return { id: String(item.id), planName: item.periodRule || period || '付款计划', billNo: item.projectNo ?? '', amount: item.totalAmount ?? 0, status: toBillStatus(item.status), projectName: item.projectName ?? '', } } /** 项目成员 → 详情页团队成员卡 */ function toTeamMember(item: RequirementDetailVo['members'][number]): TeamMember { const name = item.userName ?? '' return { role: item.roleName ?? (item.isOwner ? '项目负责人' : '项目成员'), name, avatarChar: name.slice(0, 1), } } /** * 售后工单。 * *详情页 `AfterSales` 只有「无 / 处理中」两态,且 `type`(问题类型)后端没有对应字段 * ⇒ 留空,`description` 用售后原因、`remark` 用处理说明。
*/ function toAfterSales(item: RequirementAfterSale): AfterSales { const value = (item.status ?? '').toUpperCase() const closed = value.includes('CLOSE') || value.includes('CANCEL') || value.includes('WITHDRAW') return { status: closed ? '无' : '处理中', type: '', description: item.reason ?? '', remark: item.processNote ?? '', attachments: [], createTime: item.requestTime ?? '', } } /** 详情接口返回体 → 详情页数据模型(未立项时项目侧字段自动为空) */ export function toProjectDetail(vo: RequirementDetailVo): Project { const stage = vo.projectStage const contract = vo.contracts?.[0] return { id: String(vo.id), projectNo: vo.projectNo ?? '', projectName: vo.projectName ?? '', // 后端下发的是中文类型名(如「硬件电路」),归一化成前端 code;'0' 之类脏值归 other businessType: toBusinessType(vo.businessType), // 列表页的 progress 就是详情侧 developmentProgress;未立项无进度 ⇒ 0 progress: stage?.developmentProgress ?? 0, status: toProjectStatus(vo.status), submitTime: vo.createTime ?? '', startTime: stage?.startTime ?? undefined, endTime: stage?.finishTime ?? stage?.planTime ?? undefined, milestones: (vo.projectMilestones ?? []).map((item) => ({ name: item.milestoneName ?? '', status: toMilestoneStatus(item.status), time: item.startTime ?? undefined, })), team: (vo.members ?? []).map(toTeamMember), files: (vo.requirementAttachments ?? []).map(attachmentToFile), paymentPlans: (vo.billPlans ?? []).map(toProjectBill), invoiceRecords: (vo.invoices ?? []).map(invoiceToFile), contractFile: contract ? contractToFile(contract) : undefined, afterSales: vo.afterSale ? toAfterSales(vo.afterSale) : undefined, } }