/** * 项目管理字段映射 * * 后端 projectManagement 的字段名与业务含义差异很大,映射关系取自管理端 * views/projectManagement/Management/components/formDia.vue 与 projectDetail.vue: * * info 层 no=单据编号、title=项目名称、clientName=客户名称、 * establishTime=立项日期、source=项目来源、managerName=立项人、 * orderAmount=项目金额、reviewStatus=审核状态、 * status=单据状态、stage=计划状态、planStartTime/planEndTime=计划起止 * contractInfo 层 name=联系人、sex='男'/'女'、lineaFissa=企业微信、wx=微信、 * origineEtnica=地址、rappresentanteLegale=备注 * shippingAddress 层 consignee=收货人、contract=联系电话、address=地址 * teamList 项 userId/userName、userRoleId/userRoleName、joinTime/departTime、 * contact=联系方式 * * 双向转换全部收敛在这里,页面里不出现裸字段名。 * 详情返回:detail.info / detail.shippingAddress / detail.contractInfo / * detail.salesLedgerProductList / detail.info.teamList / detail.info.attachmentList * 提交结构:{ info, shippingAddress, contractInfo, salesLedgerProductList } */ export const GENDER_OPTIONS = [ { name: "男", value: "1" }, { name: "女", value: "2" }, ]; let uidSeed = 1; const nextUid = () => `p_${uidSeed++}`; const toStr = value => value === undefined || value === null ? "" : String(value); // 后端存的是中文性别,表单选的是 '1'/'2' const genderToValue = sex => (sex === "男" ? "1" : sex === "女" ? "2" : ""); const genderFromValue = value => value === "1" ? "男" : value === "2" ? "女" : ""; // 字典值后端可能返回数字,选择器按字符串比对 const dictValue = value => value === undefined || value === null || value === "" ? "" : String(value); const idOrNull = value => value === undefined || value === null || value === "" ? null : value; // 收货地址、产品子表都可能为空对象或缺失,统一取数组 const asArray = value => (Array.isArray(value) ? value : []); /** 列表行:列表接口字段 → 展示字段 */ export function toListRow(record = {}) { return { id: record.id, billNo: record.no ?? record.billNo ?? "", projectName: record.title ?? record.projectName ?? "", billStatus: dictValue(record.billStatus ?? record.status), auditStatus: dictValue(record.reviewStatus ?? record.auditStatus), projectStage: dictValue(record.stage ?? record.projectStage), customerName: record.clientName ?? record.customerName ?? "", setupDate: record.establishTime ?? record.setupDate ?? "", projectSource: record.source ?? record.projectSource ?? "", projectClassification: record.departmentName ?? record.projectClassification ?? "", projectAmount: record.orderAmount, salesContractCount: record.salesContractCount, raw: record, }; } /** 详情接口返回 → 详情页各分区 */ export function toDetail(detail = {}) { const info = detail?.info || {}; const shippingAddress = detail?.shippingAddress || {}; const contractInfo = detail?.contractInfo || {}; const rawPhase = detail?.phaseList || detail?.projectPhaseList || detail?.projectStageList || info?.phaseList || info?.projectPhaseList; return { info, shippingAddress: { consignee: shippingAddress.consignee ?? "", contract: shippingAddress.contract ?? "", address: shippingAddress.address ?? "", }, contractInfo: { name: contractInfo.name ?? "", sex: contractInfo.sex ?? "", birthday: contractInfo.birthday ?? "", department: contractInfo.department ?? "", job: contractInfo.job ?? "", phoneNumber: contractInfo.phoneNumber ?? "", email: contractInfo.email ?? "", qq: contractInfo.qq ?? "", wx: contractInfo.wx ?? "", // 后端这两个字段名与业务含义完全无关,UI 上按业务含义展示 workWechat: contractInfo.lineaFissa ?? "", address: contractInfo.origineEtnica ?? "", remark: contractInfo.rappresentanteLegale ?? "", }, productList: asArray(detail?.salesLedgerProductList), teamList: asArray(info.teamList), attachments: asArray(info.attachmentList).map(a => ({ id: a.id ?? a.fileId, name: a.fileName ?? a.name ?? "", url: a.url ?? a.fileUrl ?? a.path ?? "", })), // 表单里没有可编辑的阶段子表(后端 /save 不吃阶段),这里只给详情页只读展示用 phaseList: asArray(rawPhase), }; } /** 详情接口返回 → 表单(含子表) */ export function toForm(detail = {}) { const d = toDetail(detail); const info = d.info; const contractInfo = detail?.contractInfo || {}; const shippingAddress = detail?.shippingAddress || {}; const form = { id: info.id, billNo: info.no ?? "", projectName: info.title ?? "", clientId: info.clientId ?? "", customerName: info.clientName ?? "", parentProjectId: info.projectManagementInfoParentId ?? "", parentProjectName: info.projectManagementInfoParentName ?? "", projectManagementPlanId: info.projectManagementPlanId ?? "", projectSource: info.source ?? "", managerId: info.managerId ?? "", creatorName: info.managerName ?? "", salesmanId: info.salesmanId ?? "", salesmanName: info.salesmanName ?? "", departmentId: info.departmentId ?? "", departmentName: info.departmentName ?? "", setupDate: info.establishTime ?? "", planStartDate: info.planStartTime ?? "", planEndDate: info.planEndTime ?? "", actualStartDate: info.actualStartTime ?? "", actualEndDate: info.actualEndTime ?? "", orderDate: info.orderDate ?? "", projectAmount: info.orderAmount ?? "", billStatus: dictValue(info.status), auditStatus: dictValue(info.reviewStatus), projectStage: dictValue(info.stage ?? info.projectStage), remark: info.remark ?? "", contactName: contractInfo.name ?? "", contactGender: genderToValue(contractInfo.sex), contactBirthday: contractInfo.birthday ?? "", contactDept: contractInfo.department ?? "", contactJob: contractInfo.job ?? "", contactMobile: contractInfo.phoneNumber ?? "", contactEmail: contractInfo.email ?? "", contactQq: contractInfo.qq ?? "", contactWechat: contractInfo.wx ?? "", contactWorkWechat: contractInfo.lineaFissa ?? "", contactAddress: contractInfo.origineEtnica ?? "", contactRemark: contractInfo.rappresentanteLegale ?? "", }; // 收货地址后端只存一条,这里用单行编辑,提交时取第一行 const addressList = shippingAddress?.address ? [ { receiver: shippingAddress.consignee ?? "", phone: shippingAddress.contract ?? "", address: shippingAddress.address ?? "", }, ] : []; const teamList = d.teamList.map(t => ({ memberId: t.userId ?? "", memberName: t.userName ?? "", roleId: t.userRoleId ?? "", roleName: t.userRoleName ?? "", enterDate: t.joinTime ?? "", leaveDate: t.departTime ?? "", phone: t.contact ?? "", remark: t.remark ?? "", })); const productList = d.productList.map(p => ({ uid: nextUid(), productCategoryId: p.productCategoryId ?? "", productCategory: p.productCategory ?? "", productModelId: p.productModelId ?? "", specificationModel: p.specificationModel ?? "", unit: p.unit ?? "", quantity: p.quantity ?? "", taxRate: p.taxRate ?? "", taxInclusiveUnitPrice: p.taxInclusiveUnitPrice ?? "", taxExclusiveTotalPrice: p.taxExclusiveTotalPrice ?? "", invoiceType: p.invoiceType ?? "", })); return { form, teamList, addressList, productList, attachments: d.attachments }; } /** 表单 → 提交结构 */ export function toPayload({ form, teamList, addressList, productList, attachmentIds, }) { const shippingRow = asArray(addressList)[0] || {}; const info = { id: form.id ?? null, no: form.billNo, title: form.projectName, clientId: idOrNull(form.clientId), clientName: form.customerName, projectManagementInfoParentId: idOrNull(form.parentProjectId), projectManagementPlanId: idOrNull(form.projectManagementPlanId), establishTime: form.setupDate, source: form.projectSource, managerId: idOrNull(form.managerId), managerName: form.creatorName, salesmanId: idOrNull(form.salesmanId), salesmanName: form.salesmanName || "", departmentId: idOrNull(form.departmentId), departmentName: form.departmentName || "", planStartTime: form.planStartDate, planEndTime: form.planEndDate, actualStartTime: form.actualStartDate, actualEndTime: form.actualEndDate, orderDate: form.orderDate, orderAmount: Number(form.projectAmount) || 0, status: form.billStatus === "" ? null : Number(form.billStatus), reviewStatus: form.auditStatus === "" ? null : Number(form.auditStatus), stage: form.projectStage === "" ? null : Number(form.projectStage), remark: form.remark, attachmentIds: asArray(attachmentIds), // userName / userRoleName 是下拉 label,后端不反查,必须前端带上 teamList: asArray(teamList).map(t => ({ userId: t.memberId, userName: t.memberName, userRoleId: t.roleId, userRoleName: t.roleName, joinTime: t.enterDate, departTime: t.leaveDate, contact: t.phone, remark: t.remark, })), }; const shippingAddress = { id: undefined, consignee: shippingRow.receiver, contract: shippingRow.phone, address: shippingRow.address, }; const contractInfo = { id: undefined, name: form.contactName, sex: genderFromValue(form.contactGender), birthday: form.contactBirthday, department: form.contactDept, job: form.contactJob, phoneNumber: form.contactMobile, email: form.contactEmail, qq: form.contactQq, wx: form.contactWechat, lineaFissa: form.contactWorkWechat, origineEtnica: form.contactAddress, rappresentanteLegale: form.contactRemark, }; return { info, shippingAddress, contractInfo, salesLedgerProductList: asArray(productList).map(p => ({ productCategoryId: p.productCategoryId, productCategory: p.productCategory, productModelId: p.productModelId, specificationModel: p.specificationModel, unit: p.unit, quantity: Number(p.quantity) || 0, taxRate: p.taxRate, taxInclusiveUnitPrice: Number(p.taxInclusiveUnitPrice) || 0, taxInclusiveTotalPrice: productInclusiveTotal(p), taxExclusiveTotalPrice: Number(p.taxExclusiveTotalPrice) || 0, invoiceType: p.invoiceType, })), }; } /** 含税总价 = 含税单价 × 数量 */ export function productInclusiveTotal(row) { const total = Number(row?.taxInclusiveUnitPrice || 0) * Number(row?.quantity || 0); return Number(total.toFixed(2)); } /** 详情页展示用的空产品行 */ export function createEmptyProduct() { return { uid: nextUid(), productCategoryId: "", productCategory: "", productModelId: "", specificationModel: "", unit: "", quantity: "", taxRate: "", taxInclusiveUnitPrice: "", taxExclusiveTotalPrice: "", invoiceType: "", }; }