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>
534 lines
17 KiB
TypeScript
534 lines
17 KiB
TypeScript
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, '(押)')
|
||
}
|