This commit is contained in:
wangzzpeng
2026-09-28 09:27:51 +08:00
parent 7bc84bbe5e
commit 6eb2bfcbfd
25 changed files with 2887 additions and 0 deletions

174
src/utils/projectVo.ts Normal file
View File

@@ -0,0 +1,174 @@
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,
}
}