import { auditColumns, createCrudOption, dataSourceOptions, phoneRule, selectRule, textRule, waybillStatusOptions, withSearchPlaceholders, } from './common'; const transportTypeDict = { dicUrl: '/blade-system/dict-biz/dictionary?code=transport_type', props: { label: 'dictValue', value: 'dictKey', }, }; const isEmpty = value => value === undefined || value === null || value === ''; const normalizeNumericDisplayValue = value => (Number(value) === -1 ? '' : value); const parseJsonArray = value => { if (Array.isArray(value)) return value; if (typeof value !== 'string' || !value.trim()) return []; try { const result = JSON.parse(value); if (Array.isArray(result)) return result; if (Array.isArray(result.records)) return result.records; if (Array.isArray(result.rows)) return result.rows; return []; } catch (error) { return []; } }; const parseJsonObject = value => { if (!value) return {}; if (typeof value === 'object' && !Array.isArray(value)) return value; if (typeof value !== 'string' || !value.trim()) return {}; try { const result = JSON.parse(value); return result && typeof result === 'object' && !Array.isArray(result) ? result : {}; } catch (error) { return {}; } }; const parseProcessNodes = value => { const result = parseJsonArray(value); if (result.length) return result; if (typeof value === 'string' && value.trim()) { try { const parsed = JSON.parse(value); if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { return Array.isArray(parsed.nodes) ? parsed.nodes : []; } } catch (error) { return []; } } if (!value || typeof value !== 'object' || Array.isArray(value)) return []; return Array.isArray(value.nodes) ? value.nodes : []; }; const getProcessConfigNodes = row => { const sources = [ row.processJson, row.processConfigJson, row.projectProcessJson, row.processConfig, row.projectProcessConfig, ]; return sources.flatMap(source => { const nodes = parseProcessNodes(source); if (nodes.length || !source || typeof source !== 'object' || Array.isArray(source)) { return nodes; } const configuredNodes = parseProcessNodes(source.nodeConfigJson); const includedNodes = String(source.includedNodes || '') .split(',') .map(item => item.trim()) .filter(Boolean); return configuredNodes.filter( node => !includedNodes.length || includedNodes.includes(node.name) || includedNodes.includes(node.key) ); }); }; const isAcceptProcessNode = node => node.key === 'accept' || node.name === '接单'; const requiresDriverAcceptConfirmation = row => getProcessConfigNodes(row).some( node => isAcceptProcessNode(node) && node.enabled !== false && node.confirmMode === 'yes' && node.confirmDriver === true ); const isDriverRejectedStatus = value => { if (value === null || value === undefined || typeof value === 'boolean') return false; return [ '-1', '2', 'reject', 'rejected', 'refuse', 'refused', 'decline', 'declined', 'deny', 'denied', '拒绝', '已拒绝', '拒绝接单', '已拒绝接单', ].includes(String(value).trim().toLowerCase()); }; const hasDriverRejectRecord = row => { const rejectRecordValues = [ row.driverRejectTime, row.driverRejectedTime, row.driverRefuseTime, row.acceptRejectTime, row.driverRejectReason, row.driverRefuseReason, row.acceptRejectReason, ]; if (rejectRecordValues.some(value => !isEmpty(value))) return true; const driverStatusProps = [ 'driverAcceptStatus', 'driverAccepted', 'driverAccept', 'accepted', 'isAccepted', 'acceptStatus', 'driverResponseStatus', 'driverOrderStatus', 'driverAcceptResult', 'acceptResult', ]; if (driverStatusProps.some(prop => isDriverRejectedStatus(row[prop]))) return true; const nodeStatusProps = [ ...driverStatusProps, 'confirmStatus', 'executionStatus', 'nodeStatus', 'status', 'result', ]; return getProcessConfigNodes(row) .filter(isAcceptProcessNode) .some(node => nodeStatusProps.some(prop => isDriverRejectedStatus(node[prop]))); }; const isDriverRejectedWaybill = row => requiresDriverAcceptConfirmation(row) && hasDriverRejectRecord(row); const hasDriverAcceptRecord = row => { const recordValues = [ row.driverAcceptTime, row.driverAcceptedTime, row.acceptTime, row.driverAcceptId, row.driverAcceptedBy, row.acceptUserId, row.acceptUserName, ]; if (recordValues.some(value => value !== null && value !== undefined && value !== '')) { return true; } const statusValues = [ row.driverAcceptStatus, row.driverAccepted, row.driverAccept, row.accepted, row.isAccepted, row.acceptStatus, ]; return statusValues.some(value => { if (value === true) return true; if (value === false || value === null || value === undefined || value === '') return false; return ['1', 'true', 'accepted', 'confirmed', 'success', '已接单', '已确认'].includes( String(value).toLowerCase() ); }); }; const isInProgressStatus = (row, defaultText) => ['processing', 'running', 'in_progress', 'inProgress'].includes( String(row.businessStatus || row.status || row.waybillStatus || '') ) || String(defaultText || '').includes('进行中') || String(row.businessStatusName || row.statusName || '').includes('进行中'); const getFirstValue = (row, props) => { const prop = props.find(item => !isEmpty(row[item])); return prop ? row[prop] : ''; }; const getGoodsRows = row => parseJsonArray(row.goodsList || row.goodsRows || row.goodsJson); const getBillingRows = row => { const freightRows = parseJsonArray(row.freightList || row.freightRows || row.freightJson); if (freightRows.length) return freightRows; return parseJsonArray(row.billingPlanJson).flatMap(item => Array.isArray(item.rules) ? item.rules : [] ); }; const joinText = list => list.filter(item => !isEmpty(item)).join('/'); const formatGoodsInfo = row => { const text = getFirstValue(row, ['goodsInfo', 'cargoInfo']); if (text) return text; return getGoodsRows(row) .map(item => { const name = getFirstValue(item, ['cargoName', 'goodsName', 'name']); const type = getFirstValue(item, ['cargoType', 'goodsType', 'typeName']); const quantity = joinText([ normalizeNumericDisplayValue( getFirstValue(item, ['quantity', 'cargoQuantity', 'goodsQuantity']) ), getFirstValue(item, ['quantityUnit', 'cargoUnit', 'unit']), ]); return joinText([name, type, quantity]); }) .filter(Boolean) .join('; '); }; const formatGoodsField = (row, props) => { const value = getFirstValue(row, props); if (value) return value; const goods = getGoodsRows(row).find(item => getFirstValue(item, props)); return goods ? getFirstValue(goods, props) : ''; }; const formatUnitPrice = row => { const value = normalizeNumericDisplayValue(getFirstValue(row, ['unitPrice', 'price'])); const unit = getFirstValue(row, ['priceUnit', 'billingUnit', 'unit']); if (!isEmpty(value)) return unit ? `${value}(${unit})` : value; const billing = getBillingRows(row).find( item => !isEmpty(normalizeNumericDisplayValue(item.unitPrice)) ); if (!billing) return ''; const billingUnit = getFirstValue(billing, ['priceUnit', 'billingUnit', 'unit']); const billingPrice = normalizeNumericDisplayValue(billing.unitPrice); return billingUnit ? `${billingPrice}(${billingUnit})` : billingPrice; }; const formatAmount = (row, props) => getFirstValue(row, props); const sumAmount = list => list.reduce((sum, item) => { const value = getFirstValue(item, ['freightAmount', 'amount', 'totalAmount']); return isEmpty(value) ? sum : sum + Number(value || 0); }, 0); const calculateFreightTotal = rows => { if (!rows.length) return ''; let hasAmountField = false; const total = rows.reduce((sum, item) => { const amount = getFirstValue(item, ['freightAmount', 'amount']); if (!isEmpty(amount)) { hasAmountField = true; return sum + Number(amount || 0); } if (isEmpty(item?.unitPrice) || isEmpty(item?.quantity)) return sum; hasAmountField = true; return sum + Number(item.unitPrice || 0) * Number(item.quantity || 0); }, 0); if (!hasAmountField) return ''; return Number.isFinite(total) ? Number(total.toFixed(2)).toString() : ''; }; const formatFreight = row => { const value = formatAmount(row, ['freight', 'freightAmount', 'transportFee']); if (!isEmpty(value)) return value; const freight = parseJsonObject(row.freightJson); const freightValue = getFirstValue(freight, ['freightAmount', 'transportFee']); if (!isEmpty(freightValue)) return freightValue; const total = sumAmount(Array.isArray(freight.freightItems) ? freight.freightItems : []); return total ? total : ''; }; const formatOtherFeeTotal = row => { const value = formatAmount(row, ['otherFeeTotal', 'otherAmount', 'otherFeeAmount']); return Number(value) === -1 ? '' : value; }; const formatFreightTotal = row => { const freight = parseJsonObject(row.freightJson); const freightItems = Array.isArray(freight.freightItems) ? freight.freightItems : []; const goodsRows = getGoodsRows(row); const calculated = calculateFreightTotal(freightItems.length ? freightItems : goodsRows); const otherFee = getFirstValue(freight, ['otherFreightAmount', 'otherFeeTotal']); const otherFeeTotal = !isEmpty(otherFee) ? otherFee : formatOtherFeeTotal(row); if (!isEmpty(calculated)) { const total = Number(calculated || 0) + Number(otherFeeTotal || 0); return Number.isFinite(total) ? Number(total.toFixed(2)).toString() : calculated; } const value = getFirstValue(freight, ['totalFreightAmount', 'totalFreight', 'totalAmount']); if (!isEmpty(value)) return value; return formatAmount(row, ['freightTotal', 'totalFreight', 'totalAmount']); }; const auditColumn = prop => ({ ...auditColumns.find(item => item.prop === prop) }); export const config = { title: '运单管理', permission: 'waybill_manage', importUrl: '/blade-transport/waybill-manage/import-waybill-manage', defaultForm: { carrierType: '承运商', transportType: 'road', quantityUnit: '吨', priceUnit: '元/吨', }, enableAllDept: false, enableProjectSelect: true, projectQueryParams: { approvalStatuses: 'approved,change_approved', }, contractQueryParams: { contractCategory: '客户合同', }, templateCreateTarget: 'waybill-manage', enableContractSelect: true, enableShippingPlanSelect: true, enableShippingInfoForm: true, enableAttachmentTable: true, attachmentTitle: '附件', fixedTransportAddressType: true, enableTaskInfoForm: true, enableWaybillFooterFreightSummary: true, enableDraftSave: true, saveAsDraft: true, skipDraftValidation: true, draftStatusProp: 'businessStatus', draftStatus: 'draft', draftSaveText: '暂存', batchDelete: false, actionButtonLikeSearch: true, createText: '新建运单', importText: '导入运单', enableWaybillImport: true, roadLoading: true, roadLoadingText: '公路配载', searchRangeMap: { startDateRange: ['startDateStart', 'startDateEnd'], endDateRange: ['endDateStart', 'endDateEnd'], createTimeRange: ['createTimeStart', 'createTimeEnd'], }, actions: ['copy', 'cancel', 'reassign', 'complete', 'batchComplete'], detailButton: true, statusProp: 'businessStatus', statusTextProp: 'businessStatusName', formatStatus(row, prop, defaultText) { if ( prop === 'businessStatus' && requiresDriverAcceptConfirmation(row) && isInProgressStatus(row, defaultText) && !hasDriverAcceptRecord(row) ) { return '待执行'; } if ( prop === 'businessStatus' && row.businessStatus === 'pending' && !requiresDriverAcceptConfirmation(row) ) { return '进行中'; } return defaultText; }, canReassign(row) { return isDriverRejectedWaybill(row); }, canEdit(row) { return !isDriverRejectedWaybill(row); }, deleteStatus: ['draft'], editStatus: ['draft', 'pending'], }; export const option = { dialogCustomClass: 'waybill-manage-dialog', ...createCrudOption( withSearchPlaceholders([ { label: '', prop: 'basicInfoTitle', formslot: true, span: 24, order: 900, hide: true, labelWidth: 0, }, { label: '运单号', prop: 'waybillNo', search: true, searchOrder: 23, minWidth: 150, addDisplay: false, editDisabled: true, }, { label: '配载单号', prop: 'loadingNo', search: true, searchOrder: 5, span: 6, order: 840, minWidth: 140, addDisplay: false, editDisplay: false, }, { label: '总单号', prop: 'masterNo', search: true, searchLabel: '多联总单', searchOrder: 6, span: 6, order: 830, minWidth: 140, addDisplay: false, editDisplay: false, }, { label: '项目名称', prop: 'projectName', formslot: true, search: true, searchLabel: '项目', searchOrder: 22, span: 6, order: 890, minWidth: 150, rules: textRule('项目', 100, true), }, { label: '客户', prop: 'customerName', search: true, searchLabel: '客户名称', searchOrder: 21, minWidth: 150, addDisplay: false, editDisplay: false, viewDisplay: false, display: false, }, { label: '车牌号/航班号/船号/班列号', prop: 'vehicleNo', search: true, searchLabel: '车/船/航班/班列', searchOrder: 13, minWidth: 210, display: false, }, { label: '司机', prop: 'driverName', search: true, searchLabel: '司机名称', searchOrder: 14, minWidth: 120, display: false, }, { label: '联系方式', prop: 'driverPhone', formatter: row => getFirstValue(row, ['driverPhone', 'driverMobile', 'driverTel']), minWidth: 140, addDisplay: false, editDisplay: false, viewDisplay: false, display: false, }, { label: '运输方式', prop: 'transportType', type: 'select', search: true, searchOrder: 20, span: 6, order: 850, ...transportTypeDict, minWidth: 130, rules: selectRule('运输方式'), }, { label: '过程节点', prop: 'currentProcessNode', minWidth: 140, addDisplay: false, editDisplay: false, viewDisplay: false, display: false, }, { label: '承运类型', prop: 'carrierType', formatter: row => getFirstValue(row, ['carrierTypeName', 'carrierType']), minWidth: 120, addDisplay: false, editDisplay: false, viewDisplay: false, display: false, }, { label: '承运商', prop: 'carrierName', search: true, searchLabel: '承运商名称', searchOrder: 15, minWidth: 150, display: false, }, { label: '货物信息', prop: 'goodsInfo', formatter: row => formatGoodsInfo(row), minWidth: 260, overHidden: true, addDisplay: false, editDisplay: false, viewDisplay: false, }, { label: '发货地址', prop: 'departureAddress', formslot: true, search: true, searchOrder: 17, span: 24, order: 790, minWidth: 220, rules: textRule('发货地址', 255, true), }, { label: '到货地址', prop: 'arrivalAddress', formslot: true, search: true, searchLabel: '收货地址', searchOrder: 16, span: 24, order: 780, minWidth: 220, rules: textRule('收货地址', 255, true), }, { label: '发货联系人', prop: 'departureContact', minWidth: 120, addDisplay: false, editDisplay: false, viewDisplay: false, }, { label: '收货联系人', prop: 'arrivalContact', minWidth: 120, addDisplay: false, editDisplay: false, viewDisplay: false, }, { label: '单价(计价单位)', prop: 'unitPrice', formatter: row => formatUnitPrice(row), minWidth: 140, addDisplay: false, editDisplay: false, viewDisplay: false, }, { label: '运费', prop: 'freight', formatter: row => formatFreight(row), minWidth: 120, addDisplay: false, editDisplay: false, viewDisplay: false, }, { label: '其他费用合计', prop: 'otherFeeTotal', formatter: row => formatOtherFeeTotal(row), minWidth: 140, addDisplay: false, editDisplay: false, viewDisplay: false, }, { label: '运费合计', prop: 'freightTotal', formatter: row => formatFreightTotal(row), minWidth: 120, addDisplay: false, editDisplay: false, viewDisplay: false, }, { label: '客户合同', prop: 'contractName', formslot: true, span: 6, order: 880, minWidth: 160, rules: textRule('客户合同', 100, true), }, { label: '计划名称', prop: 'planName', formslot: true, search: true, searchOrder: 7, span: 6, order: 870, minWidth: 150, }, { label: '货物类型', prop: 'cargoType', search: true, searchOrder: 18, formatter: row => formatGoodsField(row, ['cargoType', 'goodsType', 'typeName']), minWidth: 130, display: false, rules: textRule('货物类型', 100, true), }, { label: '货物名称', prop: 'cargoName', search: true, searchOrder: 19, formatter: row => formatGoodsField(row, ['cargoName', 'goodsName', 'name']), minWidth: 150, display: false, rules: textRule('货物名称', 100, true), }, { label: '实际发货时间', prop: 'startDate', sortable: true, type: 'datetime', format: 'YYYY-MM-DD HH:mm:ss', valueFormat: 'YYYY-MM-DD HH:mm:ss', minWidth: 170, addDisplay: false, editDisplay: false, }, { label: '实际完成时间', prop: 'endDate', sortable: true, type: 'datetime', format: 'YYYY-MM-DD HH:mm:ss', valueFormat: 'YYYY-MM-DD HH:mm:ss', minWidth: 170, addDisplay: false, editDisplay: false, }, { label: '原始单号', prop: 'originalNo', search: true, searchOrder: 12, span: 6, order: 860, minWidth: 140, }, { label: '运单批次号', prop: 'batchNo', search: true, searchLabel: '运输批次', searchOrder: 3, minWidth: 140, addDisplay: false, editDisplay: false, viewDisplay: false, display: false, }, { label: '关联单号', prop: 'relationNo', search: true, searchOrder: 2, span: 6, order: 820, minWidth: 140, hide: false, addDisplay: true, editDisplay: true, viewDisplay: true, }, { label: '预计发货日期', prop: 'estimatedStartTime', sortable: true, type: 'date', format: 'YYYY-MM-DD', valueFormat: 'YYYY-MM-DD', formatter: row => getFirstValue(row, ['estimatedStartTime', 'planStartDate']), minWidth: 170, addDisplay: false, editDisplay: false, viewDisplay: false, display: false, }, { label: '预计完成日期', prop: 'estimatedEndTime', sortable: true, type: 'date', format: 'YYYY-MM-DD', valueFormat: 'YYYY-MM-DD', formatter: row => getFirstValue(row, ['estimatedEndTime', 'planEndDate']), minWidth: 170, addDisplay: false, editDisplay: false, viewDisplay: false, display: false, }, { label: '备注', prop: 'remark', type: 'textarea', minRows: 2, span: 24, order: 810, minWidth: 180, overHidden: true, maxlength: 200, showWordLimit: true, rules: textRule('备注', 200), }, { label: '数据来源', prop: 'dataSource', type: 'select', search: true, searchOrder: 10, dicData: dataSourceOptions, minWidth: 120, addDisplay: false, editDisplay: false, viewDisplay: false, display: false, }, { ...auditColumn('createTime'), }, { ...auditColumn('updateTime'), }, { label: '状态', prop: 'businessStatus', type: 'select', slot: true, search: true, searchLabel: '状态', searchOrder: 11, dicData: waybillStatusOptions, minWidth: 82, fixed: 'right', addDisplay: false, editDisplay: false, }, { label: '', prop: 'shippingInfoTitle', formslot: true, span: 24, order: 800, hide: true, labelWidth: 0, }, { label: '', prop: 'taskInfoTitle', formslot: true, span: 24, order: 770, hide: true, labelWidth: 0, }, { label: '', prop: 'taskInfoForm', formslot: true, span: 24, order: 760, hide: true, labelWidth: '0px', className: 'business-crud-page__full-form-item', }, { label: '实际发货时间', prop: 'startDateRange', type: 'datetime', format: 'YYYY-MM-DD HH:mm:ss', valueFormat: 'YYYY-MM-DD HH:mm:ss', search: true, searchRange: true, searchOrder: 9, hide: true, addDisplay: false, editDisplay: false, viewDisplay: false, display: false, }, { label: '实际完成时间', prop: 'endDateRange', type: 'datetime', format: 'YYYY-MM-DD HH:mm:ss', valueFormat: 'YYYY-MM-DD HH:mm:ss', search: true, searchRange: true, searchOrder: 8, hide: true, addDisplay: false, editDisplay: false, viewDisplay: false, display: false, }, { label: '创建时间', prop: 'createTimeRange', type: 'datetime', format: 'YYYY-MM-DD HH:mm:ss', valueFormat: 'YYYY-MM-DD HH:mm:ss', search: true, searchRange: true, searchOrder: 4, hide: true, addDisplay: false, editDisplay: false, viewDisplay: false, }, { label: '货物信息', prop: 'goodsJson', type: 'textarea', minRows: 2, span: 24, hide: true, display: false, addDisplay: false, editDisplay: false, viewDisplay: false, }, { label: '承运信息', prop: 'carrierJson', type: 'textarea', minRows: 2, span: 24, hide: true, display: false, addDisplay: false, editDisplay: false, viewDisplay: false, }, { label: '过程配置', prop: 'processJson', type: 'textarea', minRows: 2, span: 24, hide: true, display: false, addDisplay: false, editDisplay: false, viewDisplay: false, }, { label: '运费信息', prop: 'freightJson', type: 'textarea', minRows: 2, span: 24, hide: true, display: false, addDisplay: false, editDisplay: false, viewDisplay: false, }, { label: '', prop: 'attachmentTitle', formslot: true, span: 24, order: 750, hide: true, labelWidth: 0, }, { label: '', prop: 'attachmentsJson', type: 'textarea', formslot: true, span: 24, order: 740, hide: true, labelWidth: '0px', className: 'business-crud-page__full-form-item', }, { label: '所属组织', prop: 'deptName', hide: true, minWidth: 150, addDisplay: false, editDisplay: false, }, { label: '发货地', prop: 'departureName', hide: true, display: false, minWidth: 140, rules: textRule('发货地', 100, true), }, { label: '收货地', prop: 'arrivalName', hide: true, display: false, minWidth: 140, rules: textRule('收货地', 100, true), }, { label: '发货联系方式', prop: 'departurePhone', hide: true, display: false, rules: [phoneRule('发货联系方式'), ...textRule('发货联系方式', 50)], }, { label: '收货联系方式', prop: 'arrivalPhone', hide: true, display: false, rules: [phoneRule('收货联系方式'), ...textRule('收货联系方式', 50)], }, { label: '项目ID', prop: 'projectId', hide: true, display: false, }, { label: '客户合同ID', prop: 'contractId', hide: true, display: false, }, { label: '计划名称ID', prop: 'planId', hide: true, display: false, }, { label: '任务录入模式', prop: 'taskEntryMode', hide: true, display: false, value: 'simple', }, { label: '任务备注', prop: 'taskRemark', hide: true, display: false, }, { label: '任务信息', prop: 'taskInfoJson', hide: true, display: false, }, ]) ), // label 固定宽度(容纳 6 汉字),覆盖 createCrudOption 默认 labelWidth:'auto' labelWidth: 100, dialogWidth: '96%', dialogTop: '4vh', menuWidth: 280, };