Files
cz-web/src/utils/projectVo.ts
wangzzpeng 6eb2bfcbfd first1
2026-09-28 09:27:51 +08:00

175 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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`。
*
* <p>为什么不直接改组件:详情页 6 个子组件(InfoCard/Team/Milestones/Files/Finance/
* AfterSales)都按 `Project` 这套字段写的,改数据来源不动组件契约,改动面最小。</p>
*
* <p><b>未立项是常态</b>:需求在 `vm_rd_project` 里没有对应行时,后端返回的
* `projectStage` / `projectMilestones` / `members` / `billPlans` / `invoices` / `contracts`
* 全为空 —— 这里一律兜底成空数组 / 0 / undefined,详情页显示为空态,不报错。</p>
*/
/**
* 状态码值 → 详情页中文。
*
* <p>码值域是两张表的并集(见 {@link ProjectStatus}):需求侧 `INITIATING`/`CANCELLED`,
* 项目侧 `IN_PROGRESS`/`PENDING_ACCEPTANCE`/… 未知码值兜底「待立项」(未立项是最常见态)。</p>
*/
function toProjectStatus(raw: string | null | undefined): ProjectStatus {
const label = PROJECT_STATUS_LABEL[raw ?? '']
return (label as ProjectStatus | undefined) ?? '待立项'
}
/**
* 里程碑码值 → 三态。
*
* <p>详情页 `Milestone.status` 只有「待开始 / 进行中 / 已完成」三态(设计稿如此),
* 而后端 `status` 是自由文本 ⇒ 按关键词归类,认不出的一律「待开始」。</p>
*/
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 '待开始'
}
/**
* 账单/付款计划状态 → 三态。
*
* <p>⚠️ 后端 `BillPlan.status` 的码值语义**未确认**(`vm_bill_plan` 当前 0 行,无样本),
* 只认字面量,其余一律「未到期」——不臆造成「已支付」。</p>
*/
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 ?? '',
}
}
/**
* 付款计划。
*
* <p>`BillPlan` 没有「单据号」也没有「期次名」,所以:`planName` 用 `periodRule`(付不出
* 就用「第 N 期/共 M 期」拼),`billNo` 回落到 `projectNo`。</p>
*/
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),
}
}
/**
* 售后工单。
*
* <p>详情页 `AfterSales` 只有「无 / 处理中」两态,且 `type`(问题类型)后端没有对应字段
* ⇒ 留空,`description` 用售后原因、`remark` 用处理说明。</p>
*/
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,
}
}