This commit is contained in:
zlwu committed 2026-08-27 14:23:18 +08:00
1 parent e56d227fe8
commit 4611dbc2b2
237 files changed
+1384060 -24942

No files matched your search

+13
View File
@@ -35,6 +35,19 @@ export function buildAmapSearchUrl(keyword?: string | null): string | null {
return `https://www.amap.com/search?query=${encodeURIComponent(text)}`
}
/** 优先坐标导航,无完整坐标时按地址关键词搜索兜底 */
export function buildAmapNavigationUrlWithFallback(options: {
lng?: number | null
lat?: number | null
name?: string
address?: string
}): string | null {
const label = (options.name || options.address || '').trim() || '目的地'
const nav = buildAmapNavigationUrl(options.lng, options.lat, label)
if (nav) return nav
return buildAmapSearchUrl(options.address || options.name || label)
}
/** 优先坐标标记,否则按地址搜索 */
export function buildAmapLocationUrl(options: {
lng?: number | null
+129 -8
View File
@@ -1,27 +1,138 @@
import type { InventoryRecord } from '@/api/db/inventory'
import type { TransportInventoryLink } from '@/api/pl'
import type { PlanDelivery, PlanLoad, PlanStopProduct } from '@/api/pl'
import { formatDate } from '@/utils/format'
import { isPlInternalCustomerName } from '@/utils/pl-internal-customer'
const SITE_INSTRUCTION = '说明:请打开链接上传现场附件并填写备注。'
type SiteSummaryRecord = {
export type SiteSummaryStopBlock = {
label: string
customerName?: string
businessType?: string
address?: string
productLines: string[]
}
export type SiteSummaryRecord = {
productName?: string
productText?: string
batchNo?: string
planNo?: string
siteUrl?: string
businessType?: string
siteFeedbackRemark?: string
siteFeedbackTime?: string
}
function formatSiteSummaryProductLine(record: SiteSummaryRecord) {
return record.productText || record.productName || '—'
}
function isSegmentSiteRecord(record: SiteSummaryRecord) {
return (record.businessType || '').includes('按段')
}
function formatStopProductLine(product: PlanStopProduct) {
const name = (product.productName || '').trim() || '—'
const qty = product.planQty
const unit = (product.unit || '吨').trim()
if (qty == null) return name
return `${name} ${qty}${unit}`
}
function isSiteVisibleStop(customerName?: string | null, generateDibang?: number | null) {
return isPlInternalCustomerName(customerName) && Number(generateDibang || 0) === 1
}
export function buildSiteStopBlocksFromPlan(
plan?: {
load?: PlanLoad | null
deliveries?: PlanDelivery[] | null
} | null
): SiteSummaryStopBlock[] {
if (!plan) return []
const blocks: SiteSummaryStopBlock[] = []
const load = plan.load
if (load && isSiteVisibleStop(load.customerName, load.generateDibang)) {
blocks.push({
label: '装货',
customerName: load.customerName || undefined,
businessType: load.dibangDirection || undefined,
address: load.address || undefined,
productLines: (load.products || []).map(formatStopProductLine).filter(Boolean)
})
}
for (const delivery of plan.deliveries || []) {
if (!isSiteVisibleStop(delivery.customerName, delivery.generateDibang)) continue
const seq = delivery.seq != null ? Number(delivery.seq) : 0
blocks.push({
label: seq > 0 ? `送货 #${seq}` : '送货',
customerName: delivery.customerName || undefined,
businessType: delivery.dibangDirection || undefined,
address: delivery.address || undefined,
productLines: (delivery.products || []).map(formatStopProductLine).filter(Boolean)
})
}
return blocks
}
function appendSiteRecordCardLines(
lines: string[],
record: SiteSummaryRecord,
options: { showHeadline?: boolean }
) {
const headline = (record.productName || '').trim()
const productText = formatSiteSummaryProductLine(record)
if (options.showHeadline && headline) {
lines.push(headline)
}
if (record.businessType && record.businessType !== '—') {
lines.push(`业务类型:${record.businessType}`)
}
if (record.siteFeedbackTime && record.siteFeedbackTime !== '—') {
lines.push(`保存时间:${record.siteFeedbackTime}`)
}
if (record.batchNo) {
lines.push(`出入库单号:${record.batchNo}`)
}
if (productText && productText !== '—' && productText !== headline) {
lines.push(`产品:${productText}`)
}
if (record.siteFeedbackRemark && record.siteFeedbackRemark !== '—') {
lines.push(`现场备注:${record.siteFeedbackRemark}`)
}
lines.push(`现场链接:${record.siteUrl}`)
}
function formatSiteDibangDirectionLabel(direction?: string | null) {
const value = (direction || '').trim()
if (!value || value === 'none') return ''
if (value === 'outbound') return '出库'
if (value === 'inbound') return '入库'
return value
}
function appendStopBlockLines(lines: string[], stop: SiteSummaryStopBlock) {
const bizLabel = formatSiteDibangDirectionLabel(stop.businessType)
const biz = bizLabel ? `(${bizLabel})` : ''
lines.push(`【${stop.label} · ${stop.customerName || '—'}】${biz}`)
if (stop.address) {
lines.push(`地址:${stop.address}`)
}
if (stop.productLines.length) {
stop.productLines.forEach((line) => lines.push(`产品:${line}`))
} else {
lines.push('产品:—')
}
}
export function buildSiteSummaryFromInventoryLinks(options: {
planNo?: string
licensePlate?: string
pickupTime?: string
planDate?: string
records: SiteSummaryRecord[]
stopBlocks?: SiteSummaryStopBlock[]
}) {
const lines = ['【运输现场】']
const planNo = options.planNo || options.records[0]?.planNo
@@ -30,23 +141,33 @@ export function buildSiteSummaryFromInventoryLinks(options: {
const time = formatDate(options.pickupTime) || options.planDate
if (time) lines.push(`时间:${time}`)
const validRecords = options.records.filter(item => item.siteUrl)
const validRecords = options.records.filter((item) => item.siteUrl)
if (!validRecords.length) {
lines.push('现场链接:—')
return lines.join('\n')
}
if (validRecords.length === 1) {
const stopBlocks = options.stopBlocks || []
const useSegmentLayout =
stopBlocks.length > 0 && validRecords.some((record) => isSegmentSiteRecord(record))
if (useSegmentLayout) {
lines.push('')
stopBlocks.forEach((stop, index) => {
if (index > 0) lines.push('')
appendStopBlockLines(lines, stop)
})
lines.push('')
const record = validRecords[0]
lines.push(`产品:${formatSiteSummaryProductLine(record)}`)
if (record.batchNo) lines.push(`出入库单号:${record.batchNo}`)
lines.push(`现场链接:${record.siteUrl}`)
} else if (validRecords.length === 1) {
lines.push('')
appendSiteRecordCardLines(lines, validRecords[0], { showHeadline: true })
} else {
lines.push('')
validRecords.forEach((record, index) => {
lines.push(`【${index + 1}】${formatSiteSummaryProductLine(record)}`)
if (record.batchNo) lines.push(`出入库单号:${record.batchNo}`)
lines.push(`现场链接:${record.siteUrl}`)
appendSiteRecordCardLines(lines, record, { showHeadline: false })
if (index < validRecords.length - 1) lines.push('')
})
}
@@ -0,0 +1,149 @@
/** 送(提)货单:按送货客户聚合(一车多卸) */
import type { Plan, PlanDelivery, PlanProduct, PlanStopProduct } from '@/api/pl'
export interface DeliveryPrintCustomerOption {
customerId: string
customerName: string
deliveryCount: number
}
function customerKey(customerId?: string | null, customerName?: string | null) {
const id = String(customerId || '').trim()
if (id) return `id:${id}`
const name = String(customerName || '').trim()
if (name) return `name:${name}`
return ''
}
function resolveCustomerId(customerId?: string | null, customerName?: string | null) {
const id = String(customerId || '').trim()
if (id) return id
return String(customerName || '').trim()
}
function deliveryMatchesCustomer(delivery: PlanDelivery, customerId: string) {
const target = String(customerId || '').trim()
if (!target) return false
const id = String(delivery.customerId || '').trim()
if (id && id === target) return true
const name = String(delivery.customerName || '').trim()
return !id && name === target
}
/** 从计划送货段统计去重客户(仅 1 个时可跳过选客户弹窗) */
export function listDeliveryPrintCustomers(plan?: Pick<Plan, 'deliveries'> | null): DeliveryPrintCustomerOption[] {
const deliveries = plan?.deliveries || []
const grouped = new Map<string, DeliveryPrintCustomerOption & { minSeq: number }>()
for (const delivery of deliveries) {
const key = customerKey(delivery.customerId, delivery.customerName)
if (!key) continue
const existing = grouped.get(key)
if (!existing) {
grouped.set(key, {
customerId: resolveCustomerId(delivery.customerId, delivery.customerName),
customerName: String(delivery.customerName || '').trim() || '—',
deliveryCount: 1,
minSeq: delivery.seq ?? 9999
})
continue
}
existing.deliveryCount += 1
existing.minSeq = Math.min(existing.minSeq, delivery.seq ?? 9999)
}
return [...grouped.values()]
.sort((a, b) => a.minSeq - b.minSeq || a.customerName.localeCompare(b.customerName, 'zh-CN'))
.map(({ customerId, customerName, deliveryCount }) => ({
customerId,
customerName,
deliveryCount
}))
}
function stopProductToPlanProduct(product: PlanStopProduct, planId: string): PlanProduct {
return {
id: product.id || product.planProductId || product.productId,
planId,
productId: product.productId,
productNo: product.productNo,
productName: product.productName,
model: product.model,
unit: product.unit,
packageSpec: product.packageSpec,
deliveryMethod: product.deliveryMethod,
quantity: product.planQty,
outboundEstimateQty: product.planQty,
outboundQty: product.outboundQty,
outboundPrice: product.outboundPrice,
outboundAmount: product.outboundAmount,
inboundQty: product.inboundQty,
inboundPrice: product.inboundPrice,
inboundAmount: product.inboundAmount,
isExplosive: 0,
isToxic: 0,
remark: product.remark
}
}
function mergePlanProducts(products: PlanProduct[]): PlanProduct[] {
const merged = new Map<string, PlanProduct>()
const order: string[] = []
for (const product of products) {
const key = String(product.productId || '').trim() || product.id
const existing = merged.get(key)
if (!existing) {
merged.set(key, { ...product })
order.push(key)
continue
}
const sum = (a?: number | null, b?: number | null) => {
if (a == null && b == null) return undefined
return Number(a || 0) + Number(b || 0)
}
existing.quantity = sum(existing.quantity, product.quantity)
existing.outboundEstimateQty = sum(existing.outboundEstimateQty, product.outboundEstimateQty)
existing.outboundQty = sum(existing.outboundQty, product.outboundQty)
existing.inboundQty = sum(existing.inboundQty, product.inboundQty)
if (!String(existing.remark || '').trim() && String(product.remark || '').trim()) {
existing.remark = product.remark
}
}
return order.map((key) => merged.get(key)!)
}
/**
* 按送货客户生成送(提)货单打印视图:
* - 收货单位 = 客户名
* - 联系方式/地址取 seq 最小送货段
* - 同客户多段产品行合并
*/
export function applyDeliveryCustomerToPlan(plan: Plan, customerId: string): Plan {
const deliveries = [...(plan.deliveries || [])]
.filter((item) => deliveryMatchesCustomer(item, customerId))
.sort((a, b) => (a.seq ?? 9999) - (b.seq ?? 9999))
if (!deliveries.length) {
throw new Error('未找到匹配的送货客户')
}
const primary = deliveries[0]
const products = mergePlanProducts(
deliveries.flatMap((delivery) =>
(delivery.products || []).map((product) => stopProductToPlanProduct(product, plan.id))
)
)
return {
...plan,
customerId: resolveCustomerId(primary.customerId, primary.customerName),
customerName: String(primary.customerName || '').trim(),
destinationName: String(primary.customerName || '').trim(),
destinationAddress: String(primary.address || '').trim() || plan.destinationAddress,
deliveryContact: String(primary.contact || '').trim() || plan.deliveryContact,
destinationNotes: String(primary.notes || '').trim() || plan.destinationNotes,
products: products.length ? products : plan.products
}
}
/** 入库单:送货单位 = 装货客户 */
export function resolveInboundDeliveryUnit(plan: Plan): string {
const loadName = String(plan.load?.customerName || '').trim()
if (loadName) return loadName
return String(plan.supplierName || '').trim() || String(plan.customerName || '').trim()
}
@@ -160,6 +160,10 @@ function buildReceivingUnit(plan: Plan): string {
return plan.customerName?.trim() || plan.destinationName?.trim() || ''
}
function buildReceiveContact(plan: Plan): string {
return plan.deliveryContact?.trim() || plan.contactPhone?.trim() || plan.contactName?.trim() || ''
}
function formatDeliverySlipProductGridCellContent(content: string): string {
const plain = content
.trim()
@@ -1856,7 +1860,7 @@ export function replaceDeliverySlipPlaceholders(
'{{companyName}}': DELIVERY_SLIP_DEFAULTS.companyName,
'{{receivingUnit}}': isBlank ? '' : buildReceivingUnit(plan),
'{{planNo}}': isBlank ? '' : plan.planNo || '',
'{{receiveContact}}': '',
'{{receiveContact}}': isBlank ? '' : buildReceiveContact(plan),
'{{driverInfo}}': '',
'{{planDateChinese}}': isBlank ? '' : formatPlanDateChinese(plan.planDate),
'{{productTable}}': buildProductRows(plan.products, plan.planNo || '', ctx, options),
+252
View File
@@ -0,0 +1,252 @@
import type { PlanDelivery, PlanLoad, PlanProduct, PlanStopProduct } from '@/api/pl'
import {
appendAmapNavLine,
buildAmapNavigationUrlWithFallback
} from '@/utils/amap/navigation'
import { formatDate } from '@/utils/format'
import { formatPlanProductListText } from '@/utils/pl-display'
export type DriverSummaryStopBlock = {
label: string
customerName?: string
stopTime?: string
address?: string
lng?: number | null
lat?: number | null
contact?: string
notes?: string
productLines: string[]
}
type DriverSummaryPlan = {
planNo?: string
licensePlate?: string
pickupTime?: string
planDate?: string
personnelSummary?: string
pickupAddress?: string
pickupLng?: number | null
pickupLat?: number | null
pickupContact?: string
pickupNotes?: string
destinationAddress?: string
destinationName?: string
destinationLng?: number | null
destinationLat?: number | null
deliveryContact?: string
destinationNotes?: string
startPoint?: string
endPoint?: string
load?: PlanLoad | null
deliveries?: PlanDelivery[] | null
}
function formatDriverStopProductLine(
product: PlanStopProduct,
getUnitLabel?: (unit: string) => string
) {
return formatPlanProductListText(
{
productName: product.productName,
model: product.model,
packageSpec: product.packageSpec,
unit: product.unit,
quantity: product.planQty
},
getUnitLabel,
{ usePlannedQuantity: true }
)
}
function formatPlanLevelProductLines(
products: PlanProduct[],
getUnitLabel?: (unit: string) => string
) {
return products
.map((product) => formatPlanProductListText(product, getUnitLabel, { usePlannedQuantity: true }))
.filter(Boolean)
}
function appendDriverStopBlockLines(lines: string[], stop: DriverSummaryStopBlock) {
lines.push(`【${stop.label} · ${stop.customerName || '—'}】`)
if (stop.stopTime) {
lines.push(`时间:${stop.stopTime}`)
}
if (stop.address) {
lines.push(`地址:${stop.address}`)
}
appendAmapNavLine(
lines,
'导航',
buildAmapNavigationUrlWithFallback({
lng: stop.lng,
lat: stop.lat,
address: stop.address,
name: stop.label
})
)
if (stop.contact) {
lines.push(`联系方式:${stop.contact}`)
}
if (stop.notes) {
lines.push(`注意事项:${stop.notes}`)
}
if (stop.productLines.length) {
stop.productLines.forEach((line) => lines.push(`产品:${line}`))
} else {
lines.push('产品:—')
}
}
export function buildDriverStopBlocksFromPlan(
plan?: DriverSummaryPlan | null,
getUnitLabel?: (unit: string) => string
): DriverSummaryStopBlock[] {
if (!plan) return []
const blocks: DriverSummaryStopBlock[] = []
const load = plan.load
if (load) {
blocks.push({
label: '装货',
customerName: load.customerName || undefined,
stopTime: formatDate(load.loadTime) || undefined,
address: load.address || undefined,
lng: load.lng,
lat: load.lat,
contact: load.contact || undefined,
notes: load.notes || undefined,
productLines: (load.products || []).map((product) =>
formatDriverStopProductLine(product, getUnitLabel)
)
})
}
for (const delivery of plan.deliveries || []) {
const seq = delivery.seq != null ? Number(delivery.seq) : 0
blocks.push({
label: seq > 0 ? `送货 #${seq}` : '送货',
customerName: delivery.customerName || undefined,
stopTime: formatDate(delivery.deliveryTime) || undefined,
address: delivery.address || undefined,
lng: delivery.lng,
lat: delivery.lat,
contact: delivery.contact || undefined,
notes: delivery.notes || undefined,
productLines: (delivery.products || []).map((product) =>
formatDriverStopProductLine(product, getUnitLabel)
)
})
}
return blocks
}
function buildLegacyDriverStopBlocks(
plan: DriverSummaryPlan,
overrides: {
pickupAddress?: string
pickupLng?: number | null
pickupLat?: number | null
pickupContact?: string
pickupNotes?: string
destinationAddress?: string
destinationLng?: number | null
destinationLat?: number | null
deliveryContact?: string
destinationNotes?: string
startPoint?: string
endPoint?: string
},
productLines: string[]
): DriverSummaryStopBlock[] {
const pickupAddress =
overrides.pickupAddress || plan.pickupAddress || overrides.startPoint || plan.startPoint
const destinationAddress =
overrides.destinationAddress ||
plan.destinationAddress ||
plan.destinationName ||
overrides.endPoint ||
plan.endPoint
const blocks: DriverSummaryStopBlock[] = []
if (pickupAddress) {
blocks.push({
label: '取货',
customerName: plan.load?.customerName || undefined,
stopTime: formatDate(plan.pickupTime) || undefined,
address: pickupAddress,
lng: overrides.pickupLng ?? plan.pickupLng,
lat: overrides.pickupLat ?? plan.pickupLat,
contact: overrides.pickupContact || plan.pickupContact || undefined,
notes: overrides.pickupNotes || plan.pickupNotes || undefined,
productLines: productLines.length ? productLines : []
})
}
if (destinationAddress) {
blocks.push({
label: '送货',
customerName: undefined,
stopTime: undefined,
address: destinationAddress,
lng: overrides.destinationLng ?? plan.destinationLng,
lat: overrides.destinationLat ?? plan.destinationLat,
contact: overrides.deliveryContact || plan.deliveryContact || undefined,
notes: overrides.destinationNotes || plan.destinationNotes || undefined,
productLines: []
})
}
return blocks
}
export function buildDriverSummaryFromPlan(options: {
plan?: DriverSummaryPlan | null
planNo?: string
licensePlate?: string
pickupTime?: string
planDate?: string
personnelSummary?: string
driverUrl?: string
products?: PlanProduct[]
stopBlocks?: DriverSummaryStopBlock[]
getUnitLabel?: (unit: string) => string
pickupAddress?: string
pickupLng?: number | null
pickupLat?: number | null
destinationAddress?: string
destinationLng?: number | null
destinationLat?: number | null
pickupContact?: string
pickupNotes?: string
deliveryContact?: string
destinationNotes?: string
startPoint?: string
endPoint?: string
}) {
const plan = options.plan || {}
const plate = options.licensePlate || plan.licensePlate || '—'
const lines = [`【${plate} - 运输计划】`]
lines.push(`计划号:${options.planNo || plan.planNo || '—'}`)
lines.push(`车牌:${plate}`)
const time = formatDate(options.pickupTime || plan.pickupTime) || options.planDate || plan.planDate
if (time) lines.push(`时间:${time}`)
const productLines = formatPlanLevelProductLines(options.products || [], options.getUnitLabel)
let stopBlocks =
options.stopBlocks ||
buildDriverStopBlocksFromPlan(plan, options.getUnitLabel)
if (!stopBlocks.length) {
stopBlocks = buildLegacyDriverStopBlocks(plan, options, productLines)
}
if (stopBlocks.length) {
lines.push('')
stopBlocks.forEach((stop, index) => {
if (index > 0) lines.push('')
appendDriverStopBlockLines(lines, stop)
})
} else if (productLines.length) {
lines.push('')
lines.push(`产品:${productLines.join(';')}`)
}
lines.push('')
lines.push(`司机链接:${options.driverUrl || '—'}`)
return lines.join('\n')
}
@@ -151,7 +151,9 @@ function formatQty(value?: number | string | null): string {
}
function buildDeliveryUnit(plan: Plan): string {
return plan.customerName?.trim() || plan.destinationName?.trim() || ''
const loadName = String(plan.load?.customerName || '').trim()
if (loadName) return loadName
return plan.supplierName?.trim() || plan.customerName?.trim() || plan.destinationName?.trim() || ''
}
function formatInboundSlipProductGridCellContent(content: string): string {
@@ -8,3 +8,29 @@ export function isPlInternalCustomerName(name?: string | null): boolean {
const n = (name || '').trim()
return (PL_INTERNAL_CUSTOMER_NAMES as readonly string[]).includes(n)
}
/** 装货或送货段任一段客户为山峡/云栋时,需要现场链接。 */
export function planRequiresSiteLink(
plan?: {
load?: { customerName?: string | null } | null
deliveries?: { customerName?: string | null }[] | null
supplierName?: string | null
customerName?: string | null
destinationName?: string | null
} | null
): boolean {
if (!plan) return false
if (plan.load) {
if (isPlInternalCustomerName(plan.load.customerName)) return true
} else if (isPlInternalCustomerName(plan.supplierName)) {
return true
}
const deliveries = plan.deliveries || []
if (deliveries.length) {
return deliveries.some(item => isPlInternalCustomerName(item.customerName))
}
return (
isPlInternalCustomerName(plan.customerName) ||
isPlInternalCustomerName(plan.destinationName)
)
}
@@ -0,0 +1,245 @@
import type { Plan, PlanDelivery, PlanLoad, PlanProduct, PlanStopProduct } from '@/api/pl'
import { formatDate } from '@/utils/format'
import {
planContractSideLines,
type PlanContractDisplayLine
} from '@/utils/pl-plan-contract-display'
import { mapLegacyPlanToStops } from '@/utils/pl-plan-stop-form'
export type PlPlanListDisplayRowKind = 'load' | 'delivery'
export type PlPlanListColumnScope = 'plan' | 'stop'
export interface PlPlanListDisplayRow {
rowKey: string
plan: Plan
kind: PlPlanListDisplayRowKind
stop: PlanLoad | PlanDelivery
deliverySeq?: number
/** 当前页内计划序号(0-based),用于整单斑马纹 */
planListIndex: number
isPlanFirstRow: boolean
planRowSpan: number
isPlanLastRow: boolean
}
const PLAN_FIXED_MERGE_COL_COUNT = 3
export function resolvePlanStopsForList(plan: Plan): {
load: PlanLoad
deliveries: PlanDelivery[]
} {
if (plan.load && (plan.deliveries?.length ?? 0) > 0) {
return {
load: plan.load,
deliveries: plan.deliveries || []
}
}
const legacy = mapLegacyPlanToStops(plan)
return {
load: legacy.load as PlanLoad,
deliveries: legacy.deliveries as PlanDelivery[]
}
}
export function flattenPlanListRows(plans: Plan[]): PlPlanListDisplayRow[] {
const rows: PlPlanListDisplayRow[] = []
plans.forEach((plan, planListIndex) => {
const { load, deliveries } = resolvePlanStopsForList(plan)
const planRowSpan = 1 + deliveries.length
rows.push({
rowKey: `${plan.id}-load`,
plan,
kind: 'load',
stop: load,
planListIndex,
isPlanFirstRow: true,
planRowSpan,
isPlanLastRow: deliveries.length === 0
})
deliveries.forEach((delivery, index) => {
rows.push({
rowKey: `${plan.id}-delivery-${delivery.seq ?? index + 1}`,
plan,
kind: 'delivery',
stop: delivery,
deliverySeq: delivery.seq ?? index + 1,
planListIndex,
isPlanFirstRow: false,
planRowSpan,
isPlanLastRow: index === deliveries.length - 1
})
})
})
return rows
}
export function getPlanListDisplayIndex(
displayRows: PlPlanListDisplayRow[],
displayRowIndex: number,
page: number,
pageSize: number
): number {
const row = displayRows[displayRowIndex]
if (!row?.isPlanFirstRow) return 0
let planIndex = 0
for (let i = 0; i < displayRowIndex; i++) {
if (displayRows[i]?.isPlanFirstRow) planIndex++
}
return (page - 1) * pageSize + planIndex + 1
}
export function isPlPlanListPlanMergeColumn(
columnIndex: number,
visiblePlanProps: string[]
): boolean {
if (columnIndex < PLAN_FIXED_MERGE_COL_COUNT) return true
const planColIndex = columnIndex - PLAN_FIXED_MERGE_COL_COUNT
return planColIndex >= 0 && planColIndex < visiblePlanProps.length
}
export function buildPlPlanListSpanMethod(
visiblePlanProps: string[]
): (args: {
row: PlPlanListDisplayRow
column: { type?: string; property?: string }
rowIndex: number
columnIndex: number
}) => { rowspan: number; colspan: number } | undefined {
const planMergeCount = PLAN_FIXED_MERGE_COL_COUNT + visiblePlanProps.length
return ({ row, columnIndex }) => {
if (columnIndex >= planMergeCount) return undefined
if (row.isPlanFirstRow) {
return { rowspan: row.planRowSpan, colspan: 1 }
}
return { rowspan: 0, colspan: 0 }
}
}
export function getPlPlanListRowClassName({ row }: { row: PlPlanListDisplayRow }): string {
const classes = [row.kind === 'load' ? 'pl-plan-list-row--load' : 'pl-plan-list-row--delivery']
if (row.planListIndex % 2 === 1) classes.push('pl-plan-list-row--plan-striped')
if (row.isPlanLastRow) classes.push('pl-plan-list-row--plan-end')
return classes.join(' ')
}
export function getStopKindLabel(row: PlPlanListDisplayRow): string {
if (row.kind === 'load') return '装货'
return `送货 #${row.deliverySeq ?? 1}`
}
export function formatStopDibangLabel(stop: PlanLoad | PlanDelivery): string {
if (!stop.generateDibang) return ''
const direction = (stop.dibangDirection || '').trim()
if (direction === 'outbound') return '出库'
if (direction === 'inbound') return '入库'
if (direction === 'none') return '无'
return '是'
}
export function getStopDibangTagType(
stop: PlanLoad | PlanDelivery
): 'success' | 'info' | 'warning' | undefined {
if (!stop.generateDibang) return undefined
const direction = (stop.dibangDirection || '').trim()
if (direction === 'outbound') return 'success'
if (direction === 'inbound') return 'success'
return 'info'
}
export function getStopAddress(row: PlPlanListDisplayRow): string {
return (row.stop.address || '').trim() || '—'
}
export function getStopTime(row: PlPlanListDisplayRow): string {
const raw =
row.kind === 'load' ? (row.stop as PlanLoad).loadTime : (row.stop as PlanDelivery).deliveryTime
const text = (raw || '').trim()
if (!text) return '—'
return formatDate(text) || text.slice(0, 10) || '—'
}
export function getStopCustomerName(row: PlPlanListDisplayRow): string {
return (row.stop.customerName || '').trim() || '—'
}
/** 装货行展示供方;送货行留空 */
export function getStopSupplierNameDisplay(row: PlPlanListDisplayRow): string {
if (row.kind !== 'load') return '—'
return (row.plan.supplierName || row.stop.customerName || '').trim() || '—'
}
/** 送货行展示需方;装货行留空 */
export function getStopDemanderNameDisplay(row: PlPlanListDisplayRow): string {
if (row.kind !== 'delivery') return '—'
return (row.stop.customerName || row.plan.customerName || '').trim() || '—'
}
export function getStopContractLines(row: PlPlanListDisplayRow): PlanContractDisplayLine[] {
if (row.kind === 'load') {
return planContractSideLines('供', row.stop.contracts, row.stop.customerName)
}
return planContractSideLines('需', row.stop.contracts, row.stop.customerName)
}
export function getStopProducts(row: PlPlanListDisplayRow): PlanStopProduct[] {
return row.stop.products || []
}
/** 段内危化证展示:按 planProductId 关联计划产品行 */
export function resolveStopCertProducts(
plan: Plan,
stopProducts: PlanStopProduct[]
): PlanProduct[] {
const planProducts = plan.products || []
const resolved: PlanProduct[] = []
for (const stopProduct of stopProducts) {
const match =
planProducts.find(
(item) =>
stopProduct.planProductId &&
item.id &&
String(item.id) === String(stopProduct.planProductId)
) || planProducts.find((item) => String(item.productId) === String(stopProduct.productId))
if (match) {
resolved.push(match)
continue
}
resolved.push({
id: stopProduct.planProductId || stopProduct.id || stopProduct.productId,
planId: plan.id,
productId: stopProduct.productId,
productNo: stopProduct.productNo,
productName: stopProduct.productName,
model: stopProduct.model,
unit: stopProduct.unit || '吨',
packageSpec: stopProduct.packageSpec,
deliveryMethod: stopProduct.deliveryMethod,
quantity: stopProduct.planQty,
isExplosive: 0,
isToxic: 0
})
}
return resolved
}
export function sumStopProductsField(
stopProducts: PlanStopProduct[],
field: 'planQty' | 'inboundQty' | 'outboundQty'
): number {
return stopProducts.reduce((sum, item) => sum + (Number(item[field]) || 0), 0)
}
export function sumStopActualQty(plan: Plan, stopProducts: PlanStopProduct[]): number {
const planProducts = plan.products || []
return stopProducts.reduce((sum, stopProduct) => {
const match =
planProducts.find(
(item) =>
stopProduct.planProductId &&
item.id &&
String(item.id) === String(stopProduct.planProductId)
) || planProducts.find((item) => String(item.productId) === String(stopProduct.productId))
return sum + (Number(match?.actualQty) || 0)
}, 0)
}
+701
View File
@@ -0,0 +1,701 @@
import type { Plan, PlanContractLink, PlanDelivery, PlanLoad, PlanStopProduct } from '@/api/pl'
import type { PlanFormProductRow } from '@/composables/usePlContractToPlan'
import type { BrandOption } from '@/utils/crm-product-brand'
import { formatBrandConstraintLabel } from '@/utils/crm-product-brand'
import { isPlInternalCustomerName } from '@/utils/pl-internal-customer'
export interface PlanStopProductFormRow {
id?: string
planProductId?: string
productId: string
productNo: string
productName: string
/** 展示用:主数据标准名 */
standardProductName?: string
/** 展示用:别名 */
aliasName?: string
aliasOptions?: string[]
brandMode?: string
brandId?: string
brandName?: string
brandOptions?: BrandOption[]
model?: string
unit: string
packageSpec?: string
deliveryMethod?: string
planQty?: number
/** 装货段归档:入库数量即实际数量 */
inboundQty?: number
inboundPrice?: number
inboundAmount?: number
/** 送货段归档:出库数量即实际数量 */
outboundQty?: number
outboundPrice?: number
outboundAmount?: number
remark?: string
isExplosive?: number
isToxic?: number
}
/** 段次查看/归档:装货段取 inbound,送货段取 outbound */
export function resolveStopActualQty(
row: PlanStopProductFormRow,
stopType: 'load' | 'delivery'
): number | undefined {
const raw = stopType === 'load' ? row.inboundQty : row.outboundQty
return raw != null ? Number(raw) : undefined
}
export function resolveStopUnitPrice(
row: PlanStopProductFormRow,
stopType: 'load' | 'delivery'
): number | undefined {
const raw = stopType === 'load' ? row.inboundPrice : row.outboundPrice
return raw != null ? Number(raw) : undefined
}
export function resolveStopArchiveAmount(
row: PlanStopProductFormRow,
stopType: 'load' | 'delivery'
): number | undefined {
const stored = stopType === 'load' ? row.inboundAmount : row.outboundAmount
if (stored != null) {
const n = Number(stored)
return Number.isFinite(n) ? n : undefined
}
const qty = resolveStopActualQty(row, stopType)
const price = resolveStopUnitPrice(row, stopType)
if (qty == null || price == null) return undefined
const amount = qty * price
return Number.isFinite(amount) ? amount : undefined
}
export function formatStopArchiveAmount(
row: PlanStopProductFormRow,
stopType: 'load' | 'delivery'
): string {
const amount = resolveStopArchiveAmount(row, stopType)
return amount != null ? amount.toFixed(2) : '—'
}
export interface PlanLoadForm {
customerId: string
customerName: string
address: string
lng: number | null
lat: number | null
loadTime: string
contact: string
notes: string
generateDibang: number
dibangDirection: string
contracts: PlanContractLink[]
products: PlanStopProductFormRow[]
}
export interface PlanDeliveryForm {
seq: number
customerId: string
customerName: string
address: string
lng: number | null
lat: number | null
deliveryTime: string
contact: string
notes: string
generateDibang: number
dibangDirection: string
contracts: PlanContractLink[]
products: PlanStopProductFormRow[]
}
/** 地磅默认勾选以装货客户是否为内部客户(山峡/云栋)为准,与送货段客户无关。 */
export function resolveStopDibangDefaults(
loadCustomerName?: string | null,
stopType: 'load' | 'delivery' = 'load'
): { generateDibang: number; dibangDirection: string } {
if (!isPlInternalCustomerName(loadCustomerName)) {
return { generateDibang: 0, dibangDirection: 'none' }
}
if (stopType === 'load') {
return { generateDibang: 1, dibangDirection: 'outbound' }
}
return { generateDibang: 1, dibangDirection: 'inbound' }
}
export function syncDeliveryDibangFromLoad(
deliveries: PlanDeliveryForm[],
loadCustomerName?: string | null
): PlanDeliveryForm[] {
const defaults = resolveStopDibangDefaults(loadCustomerName, 'delivery')
return deliveries.map((row) => ({
...row,
generateDibang: defaults.generateDibang,
dibangDirection: defaults.dibangDirection
}))
}
export function createStopProductsFromPlanProducts(
products: PlanFormProductRow[],
qtyField: 'quantity' | 'actualQty' = 'quantity'
): PlanStopProductFormRow[] {
return products.map((item) => ({
planProductId: item.id,
productId: item.productId,
productNo: item.productNo,
productName: item.productName,
standardProductName: item.standardProductName || item.productName,
aliasName: item.aliasName,
aliasOptions: item.aliasOptions ? [...item.aliasOptions] : undefined,
brandMode: item.brandMode,
brandId: item.brandId,
brandName: item.brandName,
brandOptions: item.brandOptions ? [...item.brandOptions] : undefined,
model: item.model,
unit: item.unit || '吨',
packageSpec: item.packageSpec,
deliveryMethod: item.deliveryMethod,
planQty: Number(item[qtyField] ?? item.quantity ?? 0),
remark: item.remark,
isExplosive: item.isExplosive,
isToxic: item.isToxic
}))
}
function matchPrevStopProduct(
stopProducts: PlanStopProductFormRow[],
planRow: PlanFormProductRow,
used: Set<number>
): PlanStopProductFormRow | undefined {
if (planRow.id) {
const byPlanProductId = stopProducts.findIndex(
(row, index) =>
!used.has(index) && row.planProductId && String(row.planProductId) === String(planRow.id)
)
if (byPlanProductId >= 0) {
used.add(byPlanProductId)
return stopProducts[byPlanProductId]
}
}
const byProductId = stopProducts.findIndex(
(row, index) => !used.has(index) && String(row.productId) === String(planRow.productId)
)
if (byProductId >= 0) {
used.add(byProductId)
return stopProducts[byProductId]
}
return undefined
}
/**
* 按计划产品重建段产品列表:增删对齐计划层;共享字段取自计划;
* 可选保留段内已改的数量 / 备注。
*/
export function reconcileStopProductsWithPlan(
stopProducts: PlanStopProductFormRow[],
planProducts: PlanFormProductRow[],
options?: {
preserveQty?: boolean
preserveRemark?: boolean
}
): PlanStopProductFormRow[] {
const preserveQty = options?.preserveQty !== false
const preserveRemark = options?.preserveRemark !== false
const used = new Set<number>()
const nextRows = createStopProductsFromPlanProducts(planProducts)
return nextRows.map((row, index) => {
const planRow = planProducts[index]
const prev = planRow ? matchPrevStopProduct(stopProducts, planRow, used) : undefined
if (!prev) return row
return {
...row,
planQty: preserveQty ? (prev.planQty ?? row.planQty) : row.planQty,
remark: preserveRemark ? (prev.remark ?? row.remark) : row.remark
}
})
}
/** 用计划产品行补齐装货/送货产品表展示字段(别名、品牌等) */
export function sumStopProductsPlanQty(products: PlanStopProductFormRow[]): number {
return products.reduce((sum, row) => sum + Number(row.planQty ?? 0), 0)
}
/** 段内紧凑表「产品」列副行:别名 · 品牌 · 规格 · 单位 */
export function formatStopProductMetaSubline(
row: PlanStopProductFormRow,
unitLabel?: string
): string {
const parts: string[] = []
if (row.aliasName) parts.push(`别名:${row.aliasName}`)
const brand = formatBrandConstraintLabel(row.brandMode, row.brandName)
if (brand && brand !== '—') parts.push(`品牌:${brand}`)
if (row.model) parts.push(row.model)
const unit = unitLabel || row.unit
if (unit) parts.push(unit)
return parts.join(' · ')
}
export function enrichStopProductsDisplayFromPlan(
stopProducts: PlanStopProductFormRow[],
planProducts: Array<{
id?: string
productId: string
productNo?: string
productName?: string
standardProductName?: string
aliasName?: string
aliasOptions?: string[]
brandMode?: string
brandId?: string
brandName?: string
brandOptions?: BrandOption[]
model?: string
unit?: string
packageSpec?: string
deliveryMethod?: string
quantity?: number
isExplosive?: number
isToxic?: number
}>
): PlanStopProductFormRow[] {
return stopProducts.map((row) => {
const match =
planProducts.find(
(p) => row.planProductId && p.id && String(p.id) === String(row.planProductId)
) || planProducts.find((p) => String(p.productId) === String(row.productId))
if (!match) return row
const planQty =
row.planQty != null && Number.isFinite(Number(row.planQty))
? Number(row.planQty)
: match.quantity != null
? Number(match.quantity)
: row.planQty
return {
...row,
productNo: row.productNo || match.productNo || row.productNo,
standardProductName: match.standardProductName || match.productName || row.productName,
aliasName: match.aliasName ?? row.aliasName,
aliasOptions: match.aliasOptions?.length ? [...match.aliasOptions] : row.aliasOptions,
brandMode: match.brandMode ?? row.brandMode,
brandId: match.brandId ?? row.brandId,
brandName: match.brandName ?? row.brandName,
brandOptions: match.brandOptions?.length ? [...match.brandOptions] : row.brandOptions,
model: row.model || match.model,
unit: row.unit || match.unit || '吨',
packageSpec: row.packageSpec || match.packageSpec,
deliveryMethod: row.deliveryMethod || match.deliveryMethod,
planQty,
isExplosive: match.isExplosive ?? row.isExplosive,
isToxic: match.isToxic ?? row.isToxic
}
})
}
/** 查看态:段产品为空时回退计划产品,再补齐展示字段(与编辑弹窗 bootstrap 一致) */
export function resolveStopProductsForView(
stopProducts: PlanStopProductFormRow[] | undefined | null,
planProducts: PlanFormProductRow[]
): PlanStopProductFormRow[] {
const rows = stopProducts?.length
? stopProducts
: planProducts.length
? createStopProductsFromPlanProducts(planProducts)
: []
return enrichStopProductsDisplayFromPlan(rows, planProducts)
}
export function createEmptyLoadForm(): PlanLoadForm {
return {
customerId: '',
customerName: '',
address: '',
lng: null,
lat: null,
loadTime: '',
contact: '',
notes: '',
generateDibang: 0,
dibangDirection: 'none',
contracts: [],
products: []
}
}
export function createEmptyDeliveryForm(
seq = 1,
loadCustomerName?: string | null
): PlanDeliveryForm {
const defaults = resolveStopDibangDefaults(loadCustomerName, 'delivery')
return {
seq,
customerId: '',
customerName: '',
address: '',
lng: null,
lat: null,
deliveryTime: '',
contact: '',
notes: '',
generateDibang: defaults.generateDibang,
dibangDirection: defaults.dibangDirection,
contracts: [],
products: []
}
}
function mapStopProductFromApi(row: PlanStopProduct): PlanStopProductFormRow {
return {
id: row.id,
planProductId: row.planProductId,
productId: row.productId,
productNo: row.productNo,
productName: row.productName,
model: row.model,
unit: row.unit || '吨',
packageSpec: row.packageSpec,
deliveryMethod: row.deliveryMethod,
planQty: row.planQty != null ? Number(row.planQty) : undefined,
inboundQty: row.inboundQty != null ? Number(row.inboundQty) : undefined,
inboundPrice: row.inboundPrice != null ? Number(row.inboundPrice) : undefined,
inboundAmount: row.inboundAmount != null ? Number(row.inboundAmount) : undefined,
outboundQty: row.outboundQty != null ? Number(row.outboundQty) : undefined,
outboundPrice: row.outboundPrice != null ? Number(row.outboundPrice) : undefined,
outboundAmount: row.outboundAmount != null ? Number(row.outboundAmount) : undefined,
remark: row.remark
}
}
/** 查看态装货地址:优先段表 address,回退计划 pickupAddress */
export function resolveLoadViewAddress(
plan?: { pickupAddress?: string | null } | null,
load?: { address?: string | null } | null
): string {
const fromStop = (load?.address || '').trim()
if (fromStop) return fromStop
return (plan?.pickupAddress || '').trim()
}
/** 查看态送货地址:优先段表 address,首段可回退计划 destinationAddress */
export function resolveDeliveryViewAddress(
plan?: { destinationAddress?: string | null } | null,
delivery?: { address?: string | null } | null,
index = 0
): string {
const fromStop = (delivery?.address || '').trim()
if (fromStop) return fromStop
if (index === 0) {
return (plan?.destinationAddress || '').trim()
}
return ''
}
export function mapLoadFromApi(load?: PlanLoad | null): PlanLoadForm {
if (!load) return createEmptyLoadForm()
const defaults = resolveStopDibangDefaults(load.customerName, 'load')
return {
customerId: load.customerId || '',
customerName: load.customerName || '',
address: load.address || '',
lng: load.lng != null ? Number(load.lng) : null,
lat: load.lat != null ? Number(load.lat) : null,
loadTime: (load.loadTime || '').slice(0, 10),
contact: load.contact || '',
notes: load.notes || '',
generateDibang: load.generateDibang ?? defaults.generateDibang,
dibangDirection: load.dibangDirection || defaults.dibangDirection,
contracts: (load.contracts || []).map((item) => ({
contractId: String(item.contractId || ''),
contractNo: item.contractNo
})),
products: (load.products || []).map(mapStopProductFromApi)
}
}
export function mapDeliveriesFromApi(
deliveries?: PlanDelivery[],
loadCustomerName?: string | null
): PlanDeliveryForm[] {
if (!deliveries?.length) return [createEmptyDeliveryForm(1)]
return deliveries.map((row, index) => {
const defaults = resolveStopDibangDefaults(loadCustomerName, 'delivery')
return {
seq: row.seq ?? index + 1,
customerId: row.customerId || '',
customerName: row.customerName || '',
address: row.address || '',
lng: row.lng != null ? Number(row.lng) : null,
lat: row.lat != null ? Number(row.lat) : null,
deliveryTime: (row.deliveryTime || '').slice(0, 10),
contact: row.contact || '',
notes: row.notes || '',
generateDibang: row.generateDibang ?? defaults.generateDibang,
dibangDirection: row.dibangDirection || defaults.dibangDirection,
contracts: (row.contracts || []).map((item) => ({
contractId: String(item.contractId || ''),
contractNo: item.contractNo
})),
products: (row.products || []).map(mapStopProductFromApi)
}
})
}
/** 旧数据无 load/deliveries 时从供方/需方字段回填 */
export function mapLegacyPlanToStops(plan: Plan): {
load: PlanLoadForm
deliveries: PlanDeliveryForm[]
} {
const loadDefaults = resolveStopDibangDefaults(plan.supplierName, 'load')
const deliveryDefaults = resolveStopDibangDefaults(plan.supplierName, 'delivery')
const stopProducts = createStopProductsFromPlanProducts(
(plan.products || []).map((item, index) => ({
id: item.id,
productId: String(item.productId || index),
productNo: item.productNo || '',
productName: item.productName || '',
model: item.model,
unit: item.unit || '吨',
quantity: Number(item.quantity ?? item.actualQty ?? 0),
packageSpec: item.packageSpec,
deliveryMethod: item.deliveryMethod,
remark: item.remark
}))
)
return {
load: {
customerId: plan.supplierId || '',
customerName: plan.supplierName || '',
address: plan.pickupAddress || '',
lng: plan.pickupLng != null ? Number(plan.pickupLng) : null,
lat: plan.pickupLat != null ? Number(plan.pickupLat) : null,
loadTime: (plan.pickupTime || '').slice(0, 10),
contact: plan.pickupContact || '',
notes: plan.pickupNotes || '',
generateDibang: loadDefaults.generateDibang,
dibangDirection: loadDefaults.dibangDirection,
contracts: (plan.supplierContracts || []).map((item) => ({
contractId: String(item.contractId || ''),
contractNo: item.contractNo
})),
products: stopProducts.length ? stopProducts.map((row) => ({ ...row })) : []
},
deliveries: [
{
seq: 1,
customerId: plan.customerId || '',
customerName: plan.customerName || '',
address: plan.destinationAddress || '',
lng: plan.destinationLng != null ? Number(plan.destinationLng) : null,
lat: plan.destinationLat != null ? Number(plan.destinationLat) : null,
deliveryTime: (plan.deliveryTime || '').slice(0, 10),
contact: plan.deliveryContact || '',
notes: plan.destinationNotes || '',
generateDibang: deliveryDefaults.generateDibang,
dibangDirection: deliveryDefaults.dibangDirection,
contracts: (plan.demanderContracts || []).map((item) => ({
contractId: String(item.contractId || ''),
contractNo: item.contractNo
})),
products: stopProducts.length ? stopProducts.map((row) => ({ ...row })) : []
}
]
}
}
export function syncLoadProductsFromPlan(
load: PlanLoadForm,
planProducts: PlanFormProductRow[],
preserveQty = true
) {
load.products = reconcileStopProductsWithPlan(load.products, planProducts, {
preserveQty,
preserveRemark: preserveQty
})
}
export function syncDeliveryProductsFromPlan(
delivery: PlanDeliveryForm,
planProducts: PlanFormProductRow[],
preserveQty = false
) {
delivery.products = reconcileStopProductsWithPlan(delivery.products, planProducts, {
preserveQty,
preserveRemark: preserveQty
})
}
function matchPrevDeliveryProduct(
stopProducts: PlanStopProductFormRow[],
loadRow: PlanStopProductFormRow,
used: Set<number>
): PlanStopProductFormRow | undefined {
if (loadRow.planProductId) {
const byPlanProductId = stopProducts.findIndex(
(row, index) =>
!used.has(index) &&
row.planProductId &&
String(row.planProductId) === String(loadRow.planProductId)
)
if (byPlanProductId >= 0) {
used.add(byPlanProductId)
return stopProducts[byPlanProductId]
}
}
const byProductId = stopProducts.findIndex(
(row, index) => !used.has(index) && String(row.productId) === String(loadRow.productId)
)
if (byProductId >= 0) {
used.add(byProductId)
return stopProducts[byProductId]
}
return undefined
}
function cloneStopProductSharedFields(
loadRow: PlanStopProductFormRow,
planQty: number,
remark?: string
): PlanStopProductFormRow {
return {
...loadRow,
aliasOptions: loadRow.aliasOptions ? [...loadRow.aliasOptions] : undefined,
brandOptions: loadRow.brandOptions ? [...loadRow.brandOptions] : undefined,
planQty,
remark: remark ?? loadRow.remark
}
}
/**
* 从装货产品同步到各送货段(产品清单 ⊆ 装货)。
* C1:仅 1 个送货段时数量跟装货;≥2 段时保留已有段数量,新增行/新增段默认 0。
*/
export function syncDeliveriesProductsFromLoad(
deliveries: PlanDeliveryForm[],
loadProducts: PlanStopProductFormRow[],
options?: { forceQtyFromLoad?: boolean }
): { deliveries: PlanDeliveryForm[]; needSplitHint: boolean } {
const multi = deliveries.length >= 2
const forceQty = options?.forceQtyFromLoad === true
const next = deliveries.map((delivery) => {
const used = new Set<number>()
const products = loadProducts.map((loadRow) => {
const prev = matchPrevDeliveryProduct(delivery.products || [], loadRow, used)
let planQty: number
if (forceQty || !multi) {
planQty = Number(loadRow.planQty ?? 0)
} else if (prev) {
planQty = Number(prev.planQty ?? 0)
} else {
planQty = 0
}
return cloneStopProductSharedFields(loadRow, planQty, prev?.remark)
})
return { ...delivery, products }
})
return { deliveries: next, needSplitHint: multi }
}
/** 装货产品 → 计划层产品行(保存 pl_plan_product / 运费计算) */
export function mapLoadProductsToPlanFormRows(
loadProducts: PlanStopProductFormRow[]
): PlanFormProductRow[] {
return loadProducts.map((row) => ({
id: row.planProductId,
productId: row.productId,
productNo: row.productNo,
productName: row.productName,
standardProductName: row.standardProductName || row.productName,
aliasName: row.aliasName,
aliasOptions: row.aliasOptions ? [...row.aliasOptions] : undefined,
brandMode: row.brandMode,
brandId: row.brandId,
brandName: row.brandName,
brandOptions: row.brandOptions ? [...row.brandOptions] : undefined,
model: row.model,
unit: row.unit || '吨',
quantity: Number(row.planQty ?? 0),
actualQty: 0,
packageSpec: row.packageSpec,
deliveryMethod: row.deliveryMethod,
remark: row.remark,
isExplosive: row.isExplosive,
isToxic: row.isToxic
}))
}
/** 合并装货映射行与既有计划产品元数据(危化证、税额等) */
export function mergePlanProductsFromLoadMirror(
fromLoad: PlanFormProductRow[],
previous: PlanFormProductRow[]
): PlanFormProductRow[] {
return fromLoad.map((row) => {
const prev =
(row.id && previous.find((item) => item.id && String(item.id) === String(row.id))) ||
previous.find((item) => String(item.productId) === String(row.productId))
if (!prev) return row
return {
...row,
id: prev.id || row.id,
categoryName: row.categoryName || prev.categoryName,
standardProductName: row.standardProductName || prev.standardProductName || row.productName,
aliasOptions: row.aliasOptions?.length ? row.aliasOptions : prev.aliasOptions,
brandOptions: row.brandOptions?.length ? row.brandOptions : prev.brandOptions,
taxRate: prev.taxRate,
taxPrice: prev.taxPrice ?? row.taxPrice,
taxAmount: prev.taxAmount,
taxSubtotal: prev.taxSubtotal,
purchaseCertUrl: prev.purchaseCertUrl,
transportCertUrl: prev.transportCertUrl,
legalUseCertUrl: prev.legalUseCertUrl
}
})
}
export function buildStopProductPayload(row: PlanStopProductFormRow) {
return {
planProductId: row.planProductId || undefined,
productId: row.productId,
productNo: row.productNo,
productName: row.productName,
model: row.model,
unit: row.unit,
packageSpec: row.packageSpec,
deliveryMethod: row.deliveryMethod,
planQty: row.planQty ?? 0,
remark: row.remark
}
}
export function buildLoadPayload(load: PlanLoadForm) {
return {
customerId: load.customerId,
customerName: load.customerName,
address: load.address || undefined,
lng: load.lng,
lat: load.lat,
loadTime: load.loadTime || undefined,
contact: load.contact || undefined,
notes: load.notes || undefined,
generateDibang: load.generateDibang,
dibangDirection: load.dibangDirection,
contracts: load.contracts,
products: load.products.map(buildStopProductPayload)
}
}
export function buildDeliveriesPayload(deliveries: PlanDeliveryForm[]) {
return deliveries.map((row, index) => ({
seq: row.seq || index + 1,
customerId: row.customerId,
customerName: row.customerName,
address: row.address || undefined,
lng: row.lng,
lat: row.lat,
deliveryTime: row.deliveryTime || undefined,
contact: row.contact || undefined,
notes: row.notes || undefined,
generateDibang: row.generateDibang,
dibangDirection: row.dibangDirection,
contracts: row.contracts,
products: row.products.map(buildStopProductPayload)
}))
}
@@ -0,0 +1,20 @@
import type { Plan, TransportInventoryLink } from '@/api/pl'
export function formatTransportInventoryLinkLabel(
record: TransportInventoryLink,
plan?: Plan | null
): string {
const product = (record.productName || '').trim() || '—'
let stopLabel = '地磅单'
if (record.stopType === 'load') {
stopLabel = '装货'
} else if (record.stopType === 'delivery') {
const deliveries = plan?.deliveries || []
const match = record.stopId
? deliveries.find((item) => item.id && String(item.id) === String(record.stopId))
: undefined
stopLabel = match?.seq ? `送货 #${match.seq}` : '送货'
}
const batch = (record.batchNo || '').trim()
return batch ? `${stopLabel} · ${product} · ${batch}` : `${stopLabel} · ${product}`
}