Files
shanxia2.0/web-admin/src/utils/pl-display.ts
T
zlwuandCursor 10c3c5470c chore: sync local PL/CRM/BX and migration work to new Gogs host
Bundle pending backend/web-admin feature updates, data-migration scripts,
and related docs so the V1.5 branch on the Tencent Cloud Gogs matches the
current workspace (excluding local secrets).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-01 09:19:21 +08:00

534 lines
17 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.
export function getPlVehicleStatusTagType(status: string) {
const map: Record<string, string> = {
available: 'success',
maintaining: 'warning',
scrapped: 'danger'
}
return map[status] || 'info'
}
export function getPlVehicleStatusLabel(status: string) {
const map: Record<string, string> = {
available: '可用',
maintaining: '维修中',
scrapped: '报废'
}
return map[status] || status
}
export function getPlVehicleTypeLabel(type: string) {
return type === 'self' ? '自有' : type === 'carrier' ? '承运商' : type
}
export function getPlVehicleTypeTagType(type: string) {
return type === 'self' ? 'success' : 'warning'
}
type VehicleTrailerLookup = ReadonlyMap<string, string> | Record<string, string | undefined | null>
function resolveTrailerFromLookup(plate: string, lookup?: VehicleTrailerLookup) {
if (!lookup || !plate) return ''
if (lookup instanceof Map) return lookup.get(plate)?.trim() || ''
return lookup[plate]?.trim() || ''
}
/** 按车牌构建挂车号查找表(仅 licensePlate 的业务列表用) */
export function buildVehicleTrailerLookup(
vehicles: Array<{ licensePlate?: string | null; trailerNo?: string | null }> = []
) {
const map = new Map<string, string>()
for (const vehicle of vehicles) {
const plate = vehicle.licensePlate?.trim()
const trailer = vehicle.trailerNo?.trim()
if (plate && trailer) map.set(plate, trailer)
}
return map
}
/** 列表/摘要:车牌号挂车号(无分隔符) */
export function formatPlVehiclePlateDisplay(
licensePlate?: string | null,
trailerNo?: string | null,
trailerLookup?: VehicleTrailerLookup
) {
const plate = licensePlate?.trim() || ''
const trailer = trailerNo?.trim() || resolveTrailerFromLookup(plate, trailerLookup)
if (!plate && !trailer) return '—'
if (plate && trailer) return `${plate}${trailer}`
return plate || trailer
}
export function getPlCarrierStatusLabel(status: string) {
return status === '1' ? '合作中' : '已终止'
}
export function getPlCarrierStatusTagType(status: string) {
return status === '1' ? 'success' : 'danger'
}
export function getPlCarrierAuditStatusLabel(status?: string) {
const map: Record<string, string> = {
pending: '待审核',
approved: '已通过',
rejected: '已驳回'
}
return status ? map[status] || status : '待审核'
}
export function getPlCarrierAuditStatusTagType(status?: string) {
const map: Record<string, string> = {
pending: 'warning',
approved: 'success',
rejected: 'danger'
}
return map[status || ''] || 'info'
}
export function getPlanProductSpec(product: { model?: string; packageSpec?: string }) {
return product.model || product.packageSpec || '—'
}
export function getPlanProductQuantity(product: {
quantity?: number | string | null
actualQty?: number | string | null
inboundEstimateQty?: number | string | null
outboundEstimateQty?: number | string | null
}) {
const actual = product.actualQty
// 实际数量为空或为 0 时,展示合同/计划数量
if (actual !== undefined && actual !== null && actual !== '' && Number(actual) !== 0) {
return String(actual)
}
const qty = product.quantity ?? product.inboundEstimateQty ?? product.outboundEstimateQty
if (qty === undefined || qty === null || qty === '') return '—'
return String(qty)
}
export function getPlanProductPlannedQuantity(product: {
quantity?: number | string | null
}) {
const qty = product.quantity
if (qty === undefined || qty === null || qty === '') return '—'
return String(qty)
}
export function formatPlanProductListText(
product: {
productName?: string
model?: string
packageSpec?: string
unit?: string
quantity?: number | string | null
actualQty?: number | string | null
inboundEstimateQty?: number | string | null
outboundEstimateQty?: number | string | null
},
getUnitLabel?: (unit: string) => string,
options?: { usePlannedQuantity?: boolean }
) {
const name = product.productName || '—'
const spec = getPlanProductSpec(product)
const qty = options?.usePlannedQuantity
? getPlanProductPlannedQuantity(product)
: getPlanProductQuantity(product)
const unitRaw = product.unit || ''
const unit = unitRaw ? (getUnitLabel ? getUnitLabel(unitRaw) || unitRaw : unitRaw) : ''
const specPart = spec && spec !== '—' ? `(${spec})` : ''
let qtyUnit = '—'
if (qty !== '—' && unit) {
qtyUnit = `${qty}${unit}`
} else if (qty !== '—') {
qtyUnit = qty
} else if (unit) {
qtyUnit = unit
}
return `${name}${specPart},${qtyUnit}`
}
/** 入库单/出库单列表产品一行:名称 + 品牌/型号规格/单位/包装规格/送货方式 */
export function formatIoOrderProductListText(item: {
productName?: string | null
brandSummary?: string | null
brandName?: string | null
specification?: string | null
unit?: string | null
unitLabel?: string | null
packageSpec?: string | null
deliveryMethod?: string | null
}) {
const name = (item.productName || '').trim() || '—'
const bits: string[] = []
const brand = (item.brandSummary || item.brandName || '').trim()
if (brand) bits.push(`品牌 ${brand}`)
const spec = (item.specification || '').trim()
if (spec) bits.push(`型号/规格 ${spec}`)
const unit = (item.unitLabel || item.unit || '').trim()
if (unit) bits.push(`单位 ${unit}`)
const packageSpec = (item.packageSpec || '').trim()
if (packageSpec) bits.push(`包装规格 ${packageSpec}`)
const delivery = (item.deliveryMethod || '').trim()
if (delivery) {
const deliveryLabels: Record<string, string> = {
self_pick: '山峡自提',
one_ticket: '一票制',
customer_pick: '客户自提'
}
bits.push(`送货方式 ${deliveryLabels[delivery] || delivery}`)
}
return bits.length ? `${name},${bits.join(',')}` : name
}
/** 品牌数量摘要,如 `君正 16 / 红四方 12` */
export function formatBrandQtySummary(
rows?: Array<{ brandName?: string | null; qty?: number | string | null }> | null
): string {
if (!rows?.length) return ''
const parts = rows
.map(row => {
const qty = Number(row.qty ?? 0)
if (!Number.isFinite(qty) || qty <= 0) return ''
const name = (row.brandName || '').trim() || '未命名品牌'
return `${name} ${qty}`
})
.filter(Boolean)
return parts.join(' / ')
}
/** 现场入出库明细品牌摘要(优先 brandQtys,其次 brandSummary) */
export function resolveSiteBrandSummary(item: {
brandQtys?: Array<{ brandName?: string | null; qty?: number | string | null }> | null
brandSummary?: string | null
brandName?: string | null
}) {
const fromQtys = formatBrandQtySummary(item.brandQtys)
if (fromQtys) return fromQtys
const summary = (item.brandSummary || item.brandName || '').trim()
return summary || '—'
}
/**
* 出入库「品牌数量」列:优先现场 brandQtys;
* 其次归档拼出的「品牌 数量」摘要(排除纯品牌名)。
*/
export function resolveIoBrandQtyDisplay(item: {
brandQtys?: Array<{ brandName?: string | null; qty?: number | string | null }> | null
brandSummary?: string | null
brandName?: string | null
}) {
const fromQtys = formatBrandQtySummary(item.brandQtys)
if (fromQtys) return fromQtys
const summary = (item.brandSummary || '').trim()
if (!summary) return '—'
const brandName = (item.brandName || '').trim()
if (brandName && summary === brandName) return '—'
// 归档摘要形如「君正 12 / 红四方 13」
if (/\s\d/.test(summary)) return summary
return '—'
}
/** 计划品牌约束展示:指定品牌名 / 不限品牌 */
export function resolvePlanBrandDisplayLabel(options: {
brandMode?: string | null
brandName?: string | null
}) {
const mode = (options.brandMode || '').trim()
const name = (options.brandName || '').trim()
if (mode === 'required') return name || '指定品牌'
if (mode === 'any') return name || '不限品牌'
return name || ''
}
/** 司机/现场公开页产品一行展示(含别名、品牌、运输计划产品备注) */
export function formatPublicTransportProductLine(product: {
productName?: string
aliasName?: string | null
model?: string
packageSpec?: string
unit?: string
quantity?: number | string | null
remark?: string
brandMode?: string | null
brandName?: string | null
}) {
const name = product.productName?.trim() || '—'
const parts: string[] = []
const alias = product.aliasName?.trim()
if (alias) parts.push(`别名 ${alias}`)
const brand = resolvePlanBrandDisplayLabel(product)
if (brand) parts.push(`品牌 ${brand}`)
const model = product.model?.trim()
const packageSpec = product.packageSpec?.trim()
if (model) parts.push(model)
if (packageSpec) parts.push(packageSpec)
const qty = getPlanProductPlannedQuantity(product)
const unitRaw = product.unit || '吨'
parts.push(qty !== '—' ? `${qty} ${unitRaw}` : unitRaw)
const remark = product.remark?.trim()
parts.push(`备注:${remark || '—'}`)
return `${name}(${parts.join(',')})`
}
export function getPlPlanCompanyLabel(company?: string) {
const map: Record<string, string> = { shanxia: '安庆山峡', yundong: '安徽云栋' }
return company ? map[company] || company : '—'
}
export function getPlPlanTypeTagType(type?: string) {
const map: Record<string, string> = { direct: 'primary', inbound: 'success', outbound: 'warning' }
return map[type || ''] || 'info'
}
export function getPlPlanTypeLabel(type?: string) {
const map: Record<string, string> = { direct: '直销单', inbound: '入库单', outbound: '出库单' }
return type ? map[type] || type : '—'
}
/** 打印单据下拉项:入库单 / 送(提)货单 / 分装单 */
export type PlPrintSlipCommand = 'inbound' | 'delivery' | 'subpack'
/**
* 按运输计划单据类型返回可打印的单据命令。
* - inbound:仅入库单
* - outbound:送(提)货单、分装单
* - direct / 其他:不可打印
*/
export function getPrintableSlipCommands(planType?: string): PlPrintSlipCommand[] {
if (planType === 'inbound') return ['inbound']
if (planType === 'outbound') return ['delivery', 'subpack']
return []
}
export function canPrintSlipCommand(planType: string | undefined, command: PlPrintSlipCommand) {
return getPrintableSlipCommands(planType).includes(command)
}
export type PlPlanTypeTab = 'all' | 'inbound' | 'outbound' | 'direct'
export const PL_PLAN_TYPE_TABS: { label: string; name: PlPlanTypeTab }[] = [
{ label: '全部', name: 'all' },
{ label: '入库单', name: 'inbound' },
{ label: '出库单', name: 'outbound' },
{ label: '直销单', name: 'direct' }
]
export function plPlanTypeTabToFilter(tab: PlPlanTypeTab): string {
return tab === 'all' ? '' : tab
}
export function plPlanTypeFilterToTab(planType?: string): PlPlanTypeTab {
if (planType === 'inbound' || planType === 'outbound' || planType === 'direct') {
return planType
}
return 'all'
}
/** 运输计划跟进状态选项(筛选) */
export const PL_PLAN_FOLLOW_STATUS_OPTIONS = [
{ label: '等通知', value: 'wait_notice' },
{ label: '待安排', value: 'pending_arrange' },
{ label: '已安排', value: 'arranged' },
{ label: '已完成', value: 'completed' },
{ label: '已取消', value: 'cancelled' }
] as const
export function getPlPlanFollowStatusTagType(status?: string) {
const map: Record<string, string> = {
wait_notice: 'warning',
pending_arrange: 'danger',
arranged: 'success',
completed: 'success',
cancelled: 'info',
// 旧值兼容(迁移前)
draft: 'danger',
processing: 'warning'
}
return map[status || ''] || 'info'
}
export function getPlPlanFollowStatusLabel(status?: string) {
const map: Record<string, string> = {
wait_notice: '等通知',
pending_arrange: '待安排',
arranged: '已安排',
completed: '已完成',
cancelled: '已取消',
draft: '待安排',
processing: '进行中'
}
return status ? map[status] || status : '—'
}
export function getPlPlanArchiveStatusTagType(status?: string) {
return status === 'archived' ? 'info' : 'danger'
}
export function getPlPlanArchiveStatusLabel(status?: string) {
return status === 'archived' ? '已归档' : '未归档'
}
export function getPlPlanCompleteStatusLabel(status?: string) {
return status === 'yes' ? '是' : '否'
}
export function getPlFreightTypeLabel(type?: string) {
const map: Record<string, string> = { by_weight: '按重量', by_trip: '按趟' }
return type ? map[type] || type : '—'
}
export function getPlYesNoLabel(value?: number | boolean | null) {
return value === 1 || value === true ? '是' : '否'
}
export function getPlHasInvoiceLabelFromSettlement(settlement?: {
invoiceNo?: string | null
invoiceDate?: string | null
invoiceAmount?: number | null
invoiceAttachmentUrls?: string | null
} | null) {
if (!settlement) return '—'
const hasInvoice =
Boolean(settlement.invoiceNo?.trim()) ||
Boolean(settlement.invoiceDate) ||
(settlement.invoiceAmount != null && Number(settlement.invoiceAmount) > 0) ||
Boolean(settlement.invoiceAttachmentUrls?.trim())
return hasInvoice ? '是' : '否'
}
/** 解析运输单司机回传附件(JSON 数组或逗号分隔) */
export function parsePlFeedbackImages(raw?: string | string[] | null): string[] {
if (!raw) return []
if (Array.isArray(raw)) return raw.map((item) => String(item).trim()).filter(Boolean)
const text = String(raw).trim()
if (!text) return []
try {
const parsed = JSON.parse(text)
if (Array.isArray(parsed)) {
return parsed.map((item) => String(item).trim()).filter(Boolean)
}
} catch {
// fall through
}
return text
.split(',')
.map((item) => item.trim())
.filter(Boolean)
}
export function getPlRechargeTypeLabel(type: string) {
return type === 'etc' ? 'ETC' : type === 'fuel' ? '加油' : type
}
export function getPlRechargeTypeTagType(type: string) {
return type === 'etc' ? 'primary' : type === 'fuel' ? 'success' : 'info'
}
export function getPlRechargeApplyStatusLabel(status: string) {
const map: Record<string, string> = {
pending: '待发放',
disbursed: '已发放',
cancelled: '已作废'
}
return map[status] || status
}
export function getPlRechargeApplyStatusTagType(status: string) {
const map: Record<string, string> = {
pending: 'warning',
disbursed: 'success',
cancelled: 'info'
}
return map[status] || 'info'
}
export const PL_GAS_STATION_PRESETS = ['中石化', '线上etc-app', '百链']
export const PL_MAINTENANCE_TYPE_PRESETS = ['保养', '维修', '年检']
export function getPlMaintenanceTypeLabel(type: string) {
return type || '—'
}
export function getPlMaintenanceTypeTagType(type: string) {
const map: Record<string, string> = {
保养: 'success',
维修: 'warning',
年检: 'primary'
}
return map[type] || 'info'
}
export function getPlMaintenanceApplyStatusLabel(status: string) {
const map: Record<string, string> = {
pending: '待处理',
processed: '已处理',
cancelled: '已作废'
}
return map[status] || status
}
export function getPlMaintenanceApplyStatusTagType(status: string) {
const map: Record<string, string> = {
pending: 'warning',
processed: 'success',
cancelled: 'info'
}
return map[status] || 'info'
}
export const PL_UREA_UNIT_PRESETS = ['吨桶']
export function getPlUreaApplyStatusLabel(status: string) {
const map: Record<string, string> = {
pending: '待发放',
disbursed: '已发放',
cancelled: '已作废'
}
return map[status] || status
}
export function getPlUreaApplyStatusTagType(status: string) {
const map: Record<string, string> = {
pending: 'warning',
disbursed: 'success',
cancelled: 'info'
}
return map[status] || 'info'
}
const PL_PERSONNEL_ROLE_SHORT_LABELS: Record<string, string> = {
driver: '驾',
escort: '押'
}
/** 人员角色短标签(列表/摘要等紧凑展示) */
export function getPlPersonnelRoleShortLabel(role?: string | null) {
if (!role) return ''
return PL_PERSONNEL_ROLE_SHORT_LABELS[role] || role
}
/** 姓名 + 角色短标签,如 刘道银(驾) */
export function formatPlPersonnelNameWithRole(
name?: string | null,
role?: string | null,
options?: { parentheses?: 'full' | 'half' }
) {
const displayName = name?.trim() || '—'
const roleLabel = getPlPersonnelRoleShortLabel(role)
if (!roleLabel) return displayName
const open = options?.parentheses === 'half' ? '(' : '('
const close = options?.parentheses === 'half' ? ')' : ')'
return `${displayName}${open}${roleLabel}${close}`
}
/** 兼容历史 personnelSummary 快照中的完整角色文案 */
export function normalizePlPersonnelSummaryDisplay(text?: string | null) {
if (!text?.trim()) return text ?? ''
return text
.replace(/\(司机\)/g, '(驾)')
.replace(/\(押运员\)/g, '(押)')
.replace(/(司机)/g, '(驾)')
.replace(/(押运员)/g, '(押)')
}