Files
tms-erp-web/src/views/business/loading-manage.vue
T
gxwebsoft b4e12a671f fix(views): 修复独立页切换时弹窗残留问题
- 独立整页固定使用普通 div 容器,取消动态切换 el-dialog 避免切换时弹窗 DOM 残留
- 删除相关 dialogVisible、dialogTitle、页脚弹窗动态分支代码以简化逻辑
- watch $route 调整,优化独立页初始化流程,避免不必要的关闭弹窗操作
- 新增 closeInnerDialogs 方法,页面卸载或 keep-alive deactivate 时关闭所有二级弹窗
- 对 loading-manage 与 project-apply 两个模块同步改造,统一处理弹窗状态和容器渲染
- 解决 keep-alive 缓存导致旧弹窗一直挂载在 body 上的问题,改善用户切换体验
2026-09-18 10:17:48 +08:00

3888 lines
139 KiB
Vue
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.
<template>
<basic-container class="loading-manage-page">
<div v-if="!isStandalonePage" class="loading-manage-page__search">
<el-form :model="searchForm" label-position="right" label-width="88px">
<div class="loading-manage-page__search-grid">
<el-form-item label="配载单号">
<el-input v-model="searchForm.loadingNo" clearable placeholder="请输入" />
</el-form-item>
<el-form-item label="项目名称">
<el-select
v-model="searchForm.projectName"
clearable
filterable
remote
reserve-keyword
:remote-method="loadProjectOptions"
:loading="projectLoading"
placeholder="请输入"
@visible-change="visible => visible && loadProjectOptions(searchForm.projectName)"
>
<el-option
v-for="item in projectOptions"
:key="item.id || item.projectName"
:label="item.projectName"
:value="item.projectName"
/>
</el-select>
</el-form-item>
<el-form-item label="客户名称">
<el-select
v-model="searchForm.customerName"
clearable
filterable
remote
reserve-keyword
:remote-method="loadCustomerOptions"
:loading="customerLoading"
placeholder="请输入"
@visible-change="visible => visible && loadCustomerOptions(searchForm.customerName)"
>
<el-option
v-for="item in customerOptions"
:key="item.id || item.customerName"
:label="item.customerName"
:value="item.customerName"
/>
</el-select>
</el-form-item>
<el-form-item label="运输方式">
<el-select
v-model="searchForm.transportType"
clearable
filterable
placeholder="请选择"
@visible-change="visible => visible && loadTransportTypeOptions()"
>
<el-option
v-for="item in transportTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<template v-if="searchExpanded">
<el-form-item label="运单号">
<el-input v-model="searchForm.waybillNo" clearable placeholder="请输入" />
</el-form-item>
<el-form-item label="货物类型">
<el-select
v-model="searchForm.cargoType"
clearable
filterable
remote
reserve-keyword
:remote-method="loadCargoTypeOptions"
:loading="cargoTypeLoading"
placeholder="请输入"
@visible-change="visible => visible && loadCargoTypeOptions(searchForm.cargoType)"
>
<el-option
v-for="item in cargoTypeOptions"
:key="item.id || item.cargoName || item.cargoType"
:label="item.cargoName || item.cargoType || item.label"
:value="item.cargoName || item.cargoType || item.label"
/>
</el-select>
</el-form-item>
<el-form-item label="发货地址">
<el-input v-model="searchForm.departureAddress" clearable placeholder="请输入" />
</el-form-item>
<el-form-item label="收货地址">
<el-input v-model="searchForm.arrivalAddress" clearable placeholder="请输入" />
</el-form-item>
<el-form-item label="货物名称">
<el-input v-model="searchForm.cargoName" clearable placeholder="请输入" />
</el-form-item>
<el-form-item label="司机名称">
<el-select
v-model="searchForm.driverName"
clearable
filterable
remote
reserve-keyword
:remote-method="loadDriverOptions"
:loading="driverLoading"
placeholder="请输入"
@visible-change="visible => visible && loadDriverOptions(searchForm.driverName)"
>
<el-option
v-for="item in driverOptions"
:key="item.id || item.driverName || item.name"
:label="item.driverName || item.name"
:value="item.driverName || item.name"
/>
</el-select>
</el-form-item>
<el-form-item label="//航班/班列">
<el-input v-model="searchForm.vehicleNo" clearable placeholder="请输入" />
</el-form-item>
<el-form-item label="原始单号">
<el-input v-model="searchForm.originalNo" clearable placeholder="请输入" />
</el-form-item>
<el-form-item label="承运商名称">
<el-select
v-model="searchForm.carrierName"
clearable
filterable
remote
reserve-keyword
:remote-method="loadCarrierOptions"
:loading="carrierLoading"
placeholder="请输入"
@visible-change="visible => visible && loadCarrierOptions(searchForm.carrierName)"
>
<el-option
v-for="item in carrierOptions"
:key="item.id || item.fullName || item.customerName || item.carrierName"
:label="item.fullName || item.customerName || item.carrierName || item.name"
:value="item.fullName || item.customerName || item.carrierName || item.name"
/>
</el-select>
</el-form-item>
<el-form-item label="数据来源">
<el-select v-model="searchForm.dataSource" clearable placeholder="请选择">
<el-option
v-for="item in dataSourceOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="开始日期">
<el-date-picker
v-model="searchForm.startDateRange"
type="daterange"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
start-placeholder="请选择"
end-placeholder="请选择"
range-separator="-"
/>
</el-form-item>
<el-form-item label="结束日期">
<el-date-picker
v-model="searchForm.endDateRange"
type="daterange"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
start-placeholder="请选择"
end-placeholder="请选择"
range-separator="-"
/>
</el-form-item>
</template>
<el-form-item label="状态">
<el-select v-model="searchForm.businessStatus" clearable placeholder="请选择">
<el-option
v-for="item in statusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="运单批次号">
<el-input v-model="searchForm.batchNo" clearable placeholder="请输入" />
</el-form-item>
<el-form-item label="计划名称">
<el-select
v-model="searchForm.planName"
clearable
filterable
remote
reserve-keyword
:remote-method="loadPlanOptions"
:loading="planLoading"
placeholder="请输入"
@visible-change="visible => visible && loadPlanOptions(searchForm.planName)"
>
<el-option
v-for="item in planOptions"
:key="item.id || item.planName"
:label="item.planName"
:value="item.planName"
/>
</el-select>
</el-form-item>
<div class="loading-manage-page__search-actions">
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleResetSearch">重置</el-button>
<el-link type="primary" @click="searchExpanded = !searchExpanded">{{
searchExpanded ? '收起' : '展开'
}}</el-link>
</div>
</div>
</el-form>
</div>
<div v-if="!isStandalonePage" class="loading-manage-page__toolbar">
<div class="loading-manage-page__toolbar-left">
<el-button type="primary" @click="openLoadingDialog('add')">新建配载</el-button>
<el-button type="primary" plain @click="handleExport">批量导出</el-button>
<el-button type="primary" plain @click="handleBatchComplete">批量完成</el-button>
</div>
<div class="loading-manage-page__toolbar-right">
<el-tooltip content="刷新" placement="top">
<el-button :icon="Refresh" text @click="loadTable" />
</el-tooltip>
<el-tooltip content="表格密度" placement="top">
<el-button :icon="Rank" text @click="toggleTableSize" />
</el-tooltip>
<el-tooltip content="列设置" placement="top">
<el-button :icon="Setting" text @click="handleColumnSetting" />
</el-tooltip>
</div>
</div>
<el-table
v-if="!isStandalonePage"
ref="tableRef"
v-loading="loading"
:data="tableData"
:size="tableSize"
border
row-key="id"
class="loading-manage-page__table"
@selection-change="selectionChange"
>
<el-table-column type="selection" width="52" fixed="left" align="center" />
<el-table-column type="index" label="序号" width="58" fixed="left" align="center" />
<el-table-column label="配载单号" prop="loadingNo" min-width="170">
<template #default="{ row }">
<el-link
type="primary"
class="loading-manage-page__link"
@click="gotoLoadingDetail(row)"
>{{ row.loadingNo }}</el-link
>
</template>
</el-table-column>
<el-table-column label="运单号" prop="loadingSubNos" min-width="220">
<template #default="{ row }">
<span class="loading-manage-page__sub-nos">
<template v-if="splitText(row.loadingSubNos).length">
<el-link
v-for="(waybill, index) in splitText(row.loadingSubNos)"
:key="`${waybill}-${index}`"
type="primary"
class="loading-manage-page__sub-no-link"
@click="openWaybillDetail({ waybillNo: waybill, id: findWaybillId(row, waybill) })"
>
{{ waybill }}
<template v-if="index < splitText(row.loadingSubNos).length - 1">,</template>
</el-link>
</template>
<span v-else>-</span>
</span>
</template>
</el-table-column>
<el-table-column label="车牌号" prop="vehicleNo" min-width="170" show-overflow-tooltip />
<el-table-column label="司机" prop="driverName" min-width="120" show-overflow-tooltip />
<el-table-column label="联系电话" prop="driverPhone" min-width="140" show-overflow-tooltip />
<el-table-column label="承运类型" prop="carrierType" min-width="120" show-overflow-tooltip />
<el-table-column label="发货地" prop="departureAddress" min-width="180">
<template #default="{ row }">
<el-tooltip :content="formatFullRouteAddress(row, 'departure')" placement="top">
<div class="loading-manage-page__address-cell">
{{ formatRouteAddress(row, 'departure') }}
</div>
</el-tooltip>
</template>
</el-table-column>
<el-table-column label="途经地" prop="transitAddress" min-width="190">
<template #default="{ row }">
<el-tooltip :content="formatFullRouteAddress(row, 'transit')" placement="top">
<div class="loading-manage-page__address-cell">
{{ formatRouteAddress(row, 'transit') }}
</div>
</el-tooltip>
</template>
</el-table-column>
<el-table-column label="到货地" prop="arrivalAddress" min-width="180">
<template #default="{ row }">
<el-tooltip :content="formatFullRouteAddress(row, 'arrival')" placement="top">
<div class="loading-manage-page__address-cell">
{{ formatRouteAddress(row, 'arrival') }}
</div>
</el-tooltip>
</template>
</el-table-column>
<el-table-column label="承运商" prop="carrierName" min-width="160" show-overflow-tooltip />
<el-table-column label="运输方式" prop="transportType" min-width="150" show-overflow-tooltip>
<template #default="{ row }">
{{ transportTypeLabel(row.transportType) }}
</template>
</el-table-column>
<el-table-column label="数据来源" prop="dataSource" min-width="130" show-overflow-tooltip />
<el-table-column
label="创建时间"
prop="createTime"
min-width="170"
show-overflow-tooltip
sortable
/>
<el-table-column
label="更新时间"
prop="updateTime"
min-width="170"
show-overflow-tooltip
sortable
/>
<el-table-column
label="过程节点"
prop="currentProcessNode"
min-width="130"
show-overflow-tooltip
/>
<el-table-column label="状态" prop="businessStatus" width="110" fixed="right" align="center">
<template #default="{ row }">
<span :class="['loading-manage-page__status', `is-${row.businessStatus || 'unknown'}`]">
{{ statusText(row) }}
</span>
</template>
</el-table-column>
<el-table-column label="操作" width="320" fixed="right" align="left">
<template #default="{ row }">
<div class="loading-manage-page__actions">
<el-link
v-for="action in rowActions(row)"
:key="action.type"
:type="['delete', 'cancel'].includes(action.type) ? 'danger' : 'primary'"
@click="handleRowAction(action.type, row)"
>{{ action.label }}</el-link
>
</div>
</template>
</el-table-column>
</el-table>
<div v-if="!isStandalonePage" class="loading-manage-page__pagination">
<el-pagination
v-model:current-page="page.currentPage"
v-model:page-size="page.pageSize"
background
:page-sizes="page.pageSizes"
layout="total, sizes, prev, pager, next, jumper"
:total="page.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<!--
配载单的新增/编辑/重新派单/详情全部是独立整页,这里固定渲染普通容器,不再按路由切换 el-dialog。
原因:标签页 keep-alive 会把本页实例缓存下来,若容器随 $route 由 div 变回 el-dialog
被缓存的实例会在离开本页后渲染出 append-to-body 的弹窗并永久停留在 body 上(Vue 的
KeepAlive.deactivate → Teleport.move 不会搬走 append-to-body 的弹窗 DOM),表现为“点左侧菜单又弹出弹窗”。
-->
<div
v-if="isStandalonePage"
class="loading-manage-dialog loading-manage-form-dialog loading-manage-form-page"
>
<div class="archive-page-form__title">{{
isStandaloneDetailPage ? detailPageTitle : formPageTitle
}}</div>
<div v-loading="dialogLoading" class="loading-manage-dialog__body">
<div v-if="dialogReadonly" class="loading-detail">
<section-card title="配载单详情">
<div class="loading-detail__summary">
<div class="loading-detail__heading">配载单详情</div>
<div>{{ dialogForm.loadingNo || '-' }}</div>
<div>
<el-tag :type="detailStatusType" class="status-text">{{
statusText(dialogForm)
}}</el-tag>
</div>
<div>
<el-tag type="warning">{{ transportTypeLabel(dialogForm.transportType) }}</el-tag>
</div>
</div>
<div class="loading-detail__task-row">
<div class="loading-detail__field">
<span class="loading-detail__label">任务信息</span
><span class="loading-detail__value">{{ taskSummary }}</span>
</div>
<div class="loading-detail__field">
<span class="loading-detail__label">附件</span
><span class="loading-detail__value"
><el-link
v-for="file in detailAttachments"
:key="file.url || file.name"
type="primary"
@click="previewAttachment(file)"
>{{ file.name }}</el-link
><span v-if="!detailAttachments.length">-</span></span
>
</div>
</div>
</section-card>
<section-card>
<template #title
><span class="loading-detail__route-title"
>运输路线<el-link type="primary" @click="openRouteChangeDialog"
>变更运输路线</el-link
></span
></template
>
<div v-if="routeNodes.length" class="loading-detail__steps">
<div
v-for="(node, index) in routeNodes"
:key="node.key || index"
class="loading-detail__step"
>
<div class="loading-detail__step-line">
<span
:class="[
'loading-detail__step-dot',
index === 0 ? 'start' : index === routeNodes.length - 1 ? 'end' : 'middle',
]"
>{{ index === 0 ? '起' : index === routeNodes.length - 1 ? '终' : '经' }}</span
>
</div>
<div class="loading-detail__step-address">
{{ node.displayAddress || formatAddressText(node.address) }}
</div>
<div class="loading-detail__step-tags">
<el-tag v-for="tag in node.tags || []" :key="tag" size="small">{{ tag }}</el-tag>
</div>
</div>
</div>
<el-empty v-else description="暂无路线" :image-size="60" />
</section-card>
<section-card title="货物信息"
><el-table :data="cargoRows" border height="220"
><el-table-column type="index" label="序号" width="70" /><el-table-column
prop="projectName"
label="项目名称"
min-width="140" /><el-table-column
prop="cargoName"
label="货物名称"
min-width="140" /><el-table-column
prop="cargoType"
label="货物类型"
min-width="120" /><el-table-column
prop="packageType"
label="包装"
min-width="100" /><el-table-column
prop="weight"
label="重量"
min-width="110" /><el-table-column
prop="volume"
label="体积"
min-width="110" /><el-table-column
prop="quantity"
label="数量"
min-width="90" /><el-table-column
prop="materialCode"
label="物料编码"
min-width="120" /><el-table-column
prop="equipmentCode"
label="设备编码"
min-width="120" /><el-table-column
prop="brand"
label="品牌"
min-width="120" /><el-table-column
prop="specificationModel"
label="规格型号"
min-width="140" /><el-table-column
prop="remark"
label="备注"
min-width="160" /><el-table-column
prop="unitPrice"
label="货物单价"
min-width="140"
><template #default="{ row }">{{
Number(row.unitPrice) === -1 ? '' : row.unitPrice
}}</template></el-table-column
><el-table-column prop="planDate" label="计划日期" min-width="130" /></el-table
></section-card>
<section-card title="关联运单信息"
><el-table :data="selectedWaybillRows" border
><el-table-column type="index" label="序号" width="70" /><el-table-column
prop="waybillNo"
label="运单号"
min-width="150"
><template #default="{ row }"
><el-link type="primary">{{ row.waybillNo || '-' }}</el-link></template
></el-table-column
><el-table-column label="配载单号" min-width="150"
><template #default>{{ dialogForm.loadingNo || '-' }}</template></el-table-column
><el-table-column
prop="projectName"
label="项目名称"
min-width="150" /><el-table-column
prop="customerName"
label="客户"
min-width="140" /><el-table-column
prop="departureAddress"
label="发货地址"
min-width="220"
show-overflow-tooltip /><el-table-column
prop="arrivalAddress"
label="到货地址"
min-width="220"
show-overflow-tooltip /></el-table
></section-card>
<div class="loading-detail__bottom-grid">
<section-card title="运输节点"
><template #extra
><div class="loading-detail__process-actions">
<el-select
v-model="processProjectId"
filterable
placeholder="切换项目"
@change="handleProcessProjectChange"
><el-option
v-for="item in processProjectOptions"
:key="item.id"
:label="item.projectName"
:value="item.id" /></el-select
><el-button plain @click="handleProcessAction('supplement')">补录执行</el-button
><el-button plain @click="handleProcessAction('batch')">批量补录</el-button>
</div></template
><el-timeline v-if="processNodes.length"
><el-timeline-item
v-for="(item, index) in processNodes"
:key="item.id || index"
:timestamp="item.time || ''"
>{{ item.name || item.nodeName || item.label }}</el-timeline-item
></el-timeline
><el-empty v-else description="暂无运输节点" :image-size="50" /></section-card
><section-card title="物流轨迹"
><template #extra
><div class="loading-detail__track-actions">
<el-button plain @click="handleTrackAction('playback')">轨迹回放</el-button
><el-button plain @click="handleTrackAction('locate')">实时定位</el-button>
</div></template
>
<div ref="detailAmap" class="loading-detail__map"></div
></section-card>
</div>
</div>
<el-form
v-else
ref="loadingFormRef"
:model="dialogForm"
:disabled="dialogReadonly"
label-position="right"
label-width="auto"
class="loading-manage-dialog__form archive-form"
>
<div title="配载信息" class="loading-manage-dialog__load-info-card">
<section-card>
<div class="loading-manage-dialog__candidate-search">
<el-row :gutter="16">
<el-col :span="6">
<el-form-item label="运单号">
<el-input v-model="candidateQuery.waybillNo" clearable placeholder="请输入" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="项目名称">
<el-select
v-model="candidateQuery.projectName"
clearable
filterable
remote
reserve-keyword
:remote-method="loadProjectOptions"
:loading="projectLoading"
placeholder="请输入"
@visible-change="
visible => visible && loadProjectOptions(candidateQuery.projectName)
"
>
<el-option
v-for="item in projectOptions"
:key="item.id || item.projectName"
:label="item.projectName"
:value="item.projectName"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="发货地址">
<el-input
v-model="candidateQuery.departureAddress"
clearable
placeholder="请输入"
/>
</el-form-item>
</el-col>
<template v-if="candidateSearchVisible">
<el-col :span="6">
<el-form-item label="货物名称">
<el-input
v-model="candidateQuery.cargoName"
clearable
placeholder="请输入"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="收货地址">
<el-input
v-model="candidateQuery.arrivalAddress"
clearable
placeholder="请输入"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="客户名称">
<el-select
v-model="candidateQuery.customerName"
clearable
filterable
remote
reserve-keyword
:remote-method="loadCustomerOptions"
:loading="customerLoading"
placeholder="请输入"
@visible-change="
visible => visible && loadCustomerOptions(candidateQuery.customerName)
"
>
<el-option
v-for="item in customerOptions"
:key="item.id || item.customerName"
:label="item.customerName"
:value="item.customerName"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="货物类型">
<el-select
v-model="candidateQuery.cargoType"
clearable
filterable
remote
reserve-keyword
:remote-method="loadCargoTypeOptions"
:loading="cargoTypeLoading"
placeholder="请输入"
@visible-change="
visible => visible && loadCargoTypeOptions(candidateQuery.cargoType)
"
>
<el-option
v-for="item in cargoTypeOptions"
:key="item.id || item.cargoName || item.cargoType"
:label="item.cargoName || item.cargoType || item.label"
:value="item.cargoName || item.cargoType || item.label"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="原始单号">
<el-input
v-model="candidateQuery.originalNo"
clearable
placeholder="请输入"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="//航班/班列">
<el-input
v-model="candidateQuery.vehicleNo"
clearable
placeholder="请输入"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="承运商名称">
<el-select
v-model="candidateQuery.carrierName"
clearable
filterable
remote
reserve-keyword
:remote-method="loadCarrierOptions"
:loading="carrierLoading"
placeholder="请输入"
@visible-change="
visible => visible && loadCarrierOptions(candidateQuery.carrierName)
"
>
<el-option
v-for="item in carrierOptions"
:key="item.id || item.fullName || item.customerName || item.carrierName"
:label="
item.fullName || item.customerName || item.carrierName || item.name
"
:value="
item.fullName || item.customerName || item.carrierName || item.name
"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="司机">
<el-select
v-model="candidateQuery.driverName"
clearable
filterable
remote
reserve-keyword
:remote-method="loadDriverOptions"
:loading="driverLoading"
placeholder="请输入"
@visible-change="
visible => visible && loadDriverOptions(candidateQuery.driverName)
"
>
<el-option
v-for="item in driverOptions"
:key="item.id || item.driverName || item.name"
:label="item.driverName || item.name"
:value="item.driverName || item.name"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="运输方式">
<el-select
v-model="candidateQuery.transportType"
clearable
filterable
placeholder="请选择"
:disabled="dialogMode === 'add'"
@visible-change="visible => visible && loadTransportTypeOptions()"
>
<el-option
v-for="item in transportTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
</template>
<el-col
:span="candidateSearchVisible ? 24 : 6"
class="loading-manage-dialog__candidate-actions"
>
<el-button type="primary" @click="handleCandidateSearch">查询</el-button>
<el-button @click="handleCandidateReset">重置</el-button>
<el-link
type="primary"
@click="candidateSearchVisible = !candidateSearchVisible"
>{{ candidateSearchVisible ? '收起' : '展开' }}</el-link
>
</el-col>
</el-row>
</div>
<el-table
ref="candidateTableRef"
v-loading="candidateLoading"
:data="candidateRows"
border
height="287"
row-key="id"
@selection-change="candidateSelectionChange"
>
<el-table-column type="selection" width="52" fixed="left" align="center" />
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column
prop="waybillNo"
label="运单号"
min-width="150"
show-overflow-tooltip
>
<template #default="{ row }">
<el-link v-if="row.waybillNo" type="primary" @click="openWaybillDetail(row)">
{{ row.waybillNo }}
</el-link>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column
prop="projectName"
label="项目名称"
min-width="150"
show-overflow-tooltip
/>
<el-table-column
prop="transportType"
label="运输方式"
min-width="130"
show-overflow-tooltip
>
<template #default="{ row }">
{{ transportTypeLabel(row.transportType) }}
</template>
</el-table-column>
<el-table-column
prop="goodsInfo"
label="货物信息"
min-width="260"
show-overflow-tooltip
>
<template #default="{ row }">
{{ formatCandidateGoodsInfo(row) }}
</template>
</el-table-column>
<el-table-column
prop="departureAddress"
label="发货地址"
min-width="220"
>
<template #default="{ row }">
<el-tooltip
v-if="formatCandidateAddressTooltip(row, 'departure')"
:content="formatCandidateAddressTooltip(row, 'departure')"
placement="top"
>
<div class="loading-manage-page__address-cell">
{{ formatCandidateAddress(row, 'departure') }}
</div>
</el-tooltip>
<span v-else>{{ formatCandidateAddress(row, 'departure') }}</span>
</template>
</el-table-column>
<el-table-column
prop="arrivalAddress"
label="收货地址"
min-width="220"
>
<template #default="{ row }">
<el-tooltip
v-if="formatCandidateAddressTooltip(row, 'arrival')"
:content="formatCandidateAddressTooltip(row, 'arrival')"
placement="top"
>
<div class="loading-manage-page__address-cell">
{{ formatCandidateAddress(row, 'arrival') }}
</div>
</el-tooltip>
<span v-else>{{ formatCandidateAddress(row, 'arrival') }}</span>
</template>
</el-table-column>
<el-table-column
prop="contractName"
label="客户合同"
min-width="160"
show-overflow-tooltip
/>
<el-table-column
prop="planName"
label="计划名称"
min-width="150"
show-overflow-tooltip
/>
<el-table-column
prop="originalNo"
label="原始单号"
min-width="140"
show-overflow-tooltip
/>
<el-table-column
prop="relationNo"
label="关联单号"
min-width="140"
show-overflow-tooltip
/>
<el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip />
<el-table-column
prop="startDate"
label="实际发货时间"
min-width="170"
show-overflow-tooltip
>
<template #default="{ row }">
{{ formatCandidateDate(row.startDate) }}
</template>
</el-table-column>
<el-table-column
prop="endDate"
label="实际完成时间"
min-width="170"
show-overflow-tooltip
>
<template #default="{ row }">
{{ formatCandidateDate(row.endDate) }}
</template>
</el-table-column>
<el-table-column
prop="createTime"
label="创建时间"
min-width="170"
show-overflow-tooltip
>
<template #default="{ row }">
{{ formatCandidateDate(row.createTime) }}
</template>
</el-table-column>
<el-table-column
prop="updateTime"
label="更新时间"
min-width="170"
show-overflow-tooltip
>
<template #default="{ row }">
{{ formatCandidateDate(row.updateTime) }}
</template>
</el-table-column>
<el-table-column
prop="businessStatus"
label="状态"
width="110"
fixed="right"
align="center"
>
<template #default="{ row }">
<span
:class="[
'loading-manage-page__status',
`is-${row.businessStatus || 'unknown'}`,
]"
>
{{ formatCandidateWaybillValue(row, 'businessStatus') }}
</span>
</template>
</el-table-column>
</el-table>
<div class="loading-manage-dialog__table-toolbar">
<el-button
type="primary"
:disabled="!candidateSelection.length || candidateAdding"
:loading="candidateAdding"
@click="addSelectedWaybills"
>添加配载</el-button
>
<el-pagination
v-model:current-page="candidatePage.currentPage"
v-model:page-size="candidatePage.pageSize"
:page-sizes="candidatePage.pageSizes"
layout="total, prev, pager, next, sizes"
:total="candidatePage.total"
@size-change="handleCandidateSizeChange"
@current-change="handleCandidateCurrentChange"
/>
</div>
</section-card>
<div class="loading-manage-dialog__waybill-route-layout">
<section-card title="待配载列表" style="min-height: 257px">
<el-table
:data="selectedWaybillRows"
border
class="loading-manage-dialog__selected-table"
>
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column prop="waybillNo" label="运单号" min-width="150">
<template #default="{ row }">
<el-link
v-if="row.waybillNo"
type="primary"
@click.stop="openWaybillDetail(row)"
>
{{ row.waybillNo }}
</el-link>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="projectName" label="项目名称" min-width="160" />
<el-table-column
prop="departureAddress"
label="发货地"
min-width="220"
>
<template #default="{ row }">
<el-tooltip
v-if="formatCandidateAddressTooltip(row, 'departure')"
:content="formatCandidateAddressTooltip(row, 'departure')"
placement="top"
>
<div class="loading-manage-page__address-cell">
{{ formatCandidateAddress(row, 'departure') }}
</div>
</el-tooltip>
<span v-else>{{ formatCandidateAddress(row, 'departure') }}</span>
</template>
</el-table-column>
<el-table-column
prop="arrivalAddress"
label="到货地"
min-width="220"
>
<template #default="{ row }">
<el-tooltip
v-if="formatCandidateAddressTooltip(row, 'arrival')"
:content="formatCandidateAddressTooltip(row, 'arrival')"
placement="top"
>
<div class="loading-manage-page__address-cell">
{{ formatCandidateAddress(row, 'arrival') }}
</div>
</el-tooltip>
<span v-else>{{ formatCandidateAddress(row, 'arrival') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="110" fixed="right">
<template #default="{ $index }">
<el-link
v-if="!dialogReadonly"
type="danger"
@click="removeSelectedWaybill($index)"
>删除</el-link
>
</template>
</el-table-column>
</el-table>
</section-card>
<section-card title="线路编辑">
<div class="loading-manage-dialog__route-list" v-if="routeNodes.length">
<div
v-for="(node, index) in routeNodes"
:key="node.key"
class="loading-manage-dialog__route-item"
draggable="true"
@dragstart="handleRouteDragStart(index)"
@dragover.prevent
@drop="handleRouteDrop(index)"
>
<el-icon class="loading-manage-dialog__route-handle"><Rank /></el-icon>
<span
:class="[
'loading-manage-dialog__route-type',
`loading-manage-dialog__route-type--${routeTypeClass(index)}`,
]"
>{{ routeTypeText(index) }}</span
>
<span class="loading-manage-dialog__route-address">{{
node.displayAddress || formatAddressText(node.address)
}}</span>
<span class="loading-manage-dialog__route-tags">
<el-tag v-for="tag in node.tags" :key="tag" size="small">{{ tag }}</el-tag>
</span>
</div>
</div>
<el-empty v-else description="添加待配载运单后自动生成路线" :image-size="72" />
</section-card>
</div>
<section-card title="货物信息" class="loading-manage-dialog__cargo-section">
<el-table :data="cargoRows" border class="loading-manage-dialog__cargo-table">
<el-table-column type="index" label="序号" width="70" align="center" fixed="left" />
<el-table-column prop="projectName" label="项目名称" min-width="150" />
<el-table-column prop="cargoName" label="货物名称" min-width="140" />
<el-table-column prop="cargoType" label="货物类型" min-width="140" />
<el-table-column prop="packageType" label="包装" min-width="100" />
<el-table-column prop="quantity" label="数量" min-width="100" />
<el-table-column prop="quantityUnit" label="单位" min-width="100" />
<el-table-column prop="materialCode" label="物料编码" min-width="130" />
<el-table-column prop="equipmentCode" label="设备编码" min-width="130" />
<el-table-column prop="brand" label="品牌" min-width="120" />
<el-table-column prop="specificationModel" label="规格型号" min-width="140" />
<el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip />
<el-table-column prop="unitPrice" label="货物单价" min-width="150">
<template #default="{ row }">{{
Number(row.unitPrice) === -1 ? '' : row.unitPrice
}}</template>
</el-table-column>
<el-table-column prop="planDate" label="计划日期" min-width="130" sortable />
</el-table>
</section-card>
<section-card title="任务信息">
<div class="loading-manage-dialog__carrier-tabs">
<el-segmented
v-model="dialogForm.carrierType"
:options="carrierTypeOptions"
:disabled="dialogReadonly"
@change="handleCarrierTypeChange"
/>
</div>
<div class="loading-manage-dialog__grid loading-manage-dialog__task-grid">
<el-form-item :required="dialogForm.carrierType === '承运商'" label="承运商">
<el-select
:model-value="
dialogForm.carrierType === '承运商'
? dialogForm.carrierContractId
: dialogForm.carrierName
"
clearable
filterable
:disabled="dialogReadonly || !selectedWaybillRows.length"
:loading="taskCarrierLoading"
:placeholder="taskCarrierPlaceholder"
@change="handleTaskCarrierChange"
@visible-change="visible => visible && loadTaskCarrierOptions()"
>
<el-option
v-for="item in taskCarrierOptions"
:key="item.id || item.carrierName"
:label="item.carrierName || item.partyB"
:value="
dialogForm.carrierType === '承运商'
? item.id
: item.carrierName || item.partyB
"
/>
</el-select>
</el-form-item>
<el-form-item
label="司机"
:required="dialogForm.carrierType === '自运'"
>
<el-autocomplete
v-model="dialogForm.driverName"
clearable
:debounce="300"
:fetch-suggestions="fetchDriverSuggestions"
:loading="driverLoading"
placeholder="请输入"
@select="item => handleDriverSuggestionSelect('dialog', item)"
/>
</el-form-item>
<el-form-item
label="手机号"
:required="dialogForm.carrierType === '自运'"
>
<el-input v-model="dialogForm.driverPhone" clearable placeholder="请输入" />
</el-form-item>
<el-form-item label="车牌号" required>
<el-input v-model="dialogForm.vehicleNo" clearable placeholder="请输入" />
</el-form-item>
<el-form-item v-if="dialogForm.carrierType !== '承运商'" label="挂车车牌号">
<el-input v-model="dialogForm.trailerVehicleNo" clearable placeholder="请输入" />
</el-form-item>
<el-form-item v-if="dialogForm.carrierType !== '承运商'" label="押运人">
<el-autocomplete
v-model="dialogForm.escortName"
clearable
:debounce="300"
:fetch-suggestions="fetchEscortSuggestions"
:loading="escortLoading"
placeholder="请输入"
@select="item => handleEscortSuggestionSelect('dialog', item)"
/>
</el-form-item>
<el-form-item v-if="dialogForm.carrierType !== '承运商'" label="押运人手机号">
<el-input v-model="dialogForm.escortPhone" clearable placeholder="请输入" />
</el-form-item>
<el-form-item label="里程km">
<el-input
v-model="dialogForm.mileage"
clearable
placeholder="请输入"
@input="handleMileageInput"
/>
</el-form-item>
<el-form-item label="预计发货日期">
<el-date-picker
v-model="dialogForm.estimatedStartDate"
type="date"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
placeholder="请选择"
/>
</el-form-item>
<el-form-item label="预计完成日期">
<el-date-picker
v-model="dialogForm.estimatedEndDate"
type="date"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
placeholder="请选择"
/>
</el-form-item>
<el-form-item label="备注" class="loading-manage-dialog__span-2">
<el-input
v-model="dialogForm.taskRemark"
type="textarea"
:rows="2"
maxlength="500"
show-word-limit
placeholder="请输入500字以内"
/>
</el-form-item>
</div>
</section-card>
</div>
</el-form>
</div>
<div class="loading-manage-dialog__footer">
<template v-if="dialogReadonly">
<el-button @click="closeLoadingDialog">关闭</el-button>
</template>
<template v-else>
<el-button @click="closeLoadingDialog">关闭</el-button>
<el-button type="primary" plain :loading="dialogSaving === 'draft'" @click="saveDraft">
暂存
</el-button>
<el-button type="primary" :loading="dialogSaving === 'submit'" @click="submitLoading">
{{ dialogMode === 'reassign' ? '确认派单' : '确认' }}
</el-button>
</template>
</div>
</div>
<el-dialog
v-model="routeChangeVisible"
title="变更运输路线"
append-to-body
destroy-on-close
width="78%"
class="loading-route-change-dialog"
>
<el-tabs type="border-card" v-model="routeChangeTab">
<el-tab-pane label="变更路线" name="change">
<section-card title="关联子运单地址">
<el-table :data="routeChangeRows" border>
<el-table-column type="index" label="序号" width="70" />
<el-table-column prop="waybillNo" label="运单号" min-width="150"
><template #default="{ row }"
><el-link type="primary">{{ row.waybillNo || '-' }}</el-link></template
></el-table-column
>
<el-table-column prop="projectName" label="项目名称" min-width="160" />
<el-table-column label="发货地" min-width="310"
><template #default="{ row }"
><div class="loading-route-change-dialog__original">
原:{{ row.originalDepartureAddress || '-' }}
</div>
<el-input
v-model="row.departureAddress"
placeholder="请选择或输入地址"
@input="updateRouteChangeNodes"
><template #suffix
><el-icon
class="loading-route-change-dialog__location"
@click="openCommonAddressDialog(row, 'departure')"
><Location /></el-icon></template></el-input></template
></el-table-column>
<el-table-column label="到货地" min-width="310"
><template #default="{ row }"
><div class="loading-route-change-dialog__original">
原:{{ row.originalArrivalAddress || '-' }}
</div>
<el-input
v-model="row.arrivalAddress"
placeholder="请选择或输入地址"
@input="updateRouteChangeNodes"
><template #suffix
><el-icon
class="loading-route-change-dialog__location"
@click="openCommonAddressDialog(row, 'arrival')"
><Location /></el-icon></template></el-input></template
></el-table-column>
</el-table>
</section-card>
<section-card title="路线编辑"
><template #extra
><span class="loading-route-change-dialog__hint">拖拽调整顺序</span></template
>
<div v-if="routeChangeNodes.length" class="loading-route-change-dialog__route-list">
<div
v-for="(node, index) in routeChangeNodes"
:key="node.key || index"
class="loading-route-change-dialog__route-item"
draggable="true"
@dragstart="handleRouteChangeDragStart(index)"
@dragover.prevent
@drop="handleRouteChangeDrop(index)"
>
<span
:class="[
'loading-route-change-dialog__route-type',
`loading-route-change-dialog__route-type--${routeChangeTypeClass(index)}`,
]"
>{{ routeChangeTypeText(index) }}</span
><span>{{ node.address }}</span
><span class="loading-route-change-dialog__tags"
><el-tag v-for="tag in node.tags || []" :key="tag" size="small">{{
tag
}}</el-tag></span
><el-icon><Rank /></el-icon>
</div>
</div>
<el-empty v-else description="暂无路线" :image-size="60"
/></section-card>
<el-form label-width="auto"
><el-form-item label="变更备注"
><el-input
v-model="routeChangeRemark"
type="textarea"
:rows="2"
maxlength="200"
show-word-limit
placeholder="请输入" /></el-form-item
></el-form>
</el-tab-pane>
<el-tab-pane label="变更记录" name="records"
><el-table :data="routeChangeRecords" border
><el-table-column prop="changeTime" label="变更时间" min-width="170" /><el-table-column
prop="changeUser"
label="变更人"
min-width="120" /><el-table-column prop="content" label="变更内容" min-width="360"
><template #default="{ row }"
><div class="loading-route-change-dialog__record-content">
{{ row.content || '-' }}
</div></template
></el-table-column
><el-table-column
prop="remark"
label="备注"
min-width="200"
show-overflow-tooltip /></el-table
><el-empty v-if="!routeChangeRecords.length" description="暂无变更记录" :image-size="60"
/></el-tab-pane>
</el-tabs>
<template #footer
><el-button @click="routeChangeVisible = false">关闭</el-button
><el-button v-if="routeChangeTab === 'change'" type="primary" @click="confirmRouteChange"
>确认变更</el-button
></template
>
</el-dialog>
<el-dialog
v-model="commonAddressVisible"
title="选择常用地址"
append-to-body
width="900px"
class="loading-common-address-dialog"
>
<div class="loading-common-address-dialog__toolbar">
<el-input
v-model="commonAddressQuery.addressName"
clearable
placeholder="请输入地址名称"
@keyup.enter="loadCommonAddresses"
/><el-button type="primary" @click="loadCommonAddresses">查询</el-button>
</div>
<el-table
v-loading="commonAddressLoading"
:data="commonAddressRows"
border
@row-dblclick="selectCommonAddress"
><el-table-column prop="addressName" label="地址名称" min-width="180" /><el-table-column
prop="regionName"
label="区域"
min-width="180"
/><el-table-column prop="detailAddress" label="详细地址" min-width="260" /><el-table-column
label="操作"
width="90"
><template #default="{ row }"
><el-link type="primary" @click="selectCommonAddress(row)">选择</el-link></template
></el-table-column
></el-table
>
<template #footer
><el-pagination
v-model:current-page="commonAddressPage.currentPage"
v-model:page-size="commonAddressPage.pageSize"
layout="total, prev, pager, next"
:total="commonAddressPage.total"
@current-change="loadCommonAddresses"
/><el-button @click="commonAddressVisible = false">关闭</el-button></template
>
</el-dialog>
</basic-container>
</template>
<script>
import request from '@/axios';
import { exportBlob } from '@/api/common';
import { downloadXls } from '@/utils/util';
import * as loadingApi from '@/api/business/loading-manage';
import { getList as getProjectList } from '@/api/business/project-apply';
import { getList as getPlanList } from '@/api/business/transport-plan';
import {
getList as getWaybillList,
getDetail as getWaybillDetail,
} from '@/api/business/waybill-manage';
import { getList as getContractList } from '@/api/business/contract-manage';
import { getParentOptions as getCargoTypeOptions } from '@/api/base/cargo-type';
import { getList as getCustomerList } from '@/api/vehicle/customer-archive';
import { getList as getDriverList } from '@/api/transportCapacity/driver';
import { getList as getProcessConfigList } from '@/api/business/process-config';
import { getList as getCommonAddressList } from '@/api/base/common-address';
import {
carrierTypeOptions,
loadingManageConfig,
loadingStatusMap,
loadingStatusOptions,
} from '@/option/business/loading-manage';
import { option as waybillOption } from '@/option/business/waybill-manage';
import { ElMessage, ElMessageBox } from 'element-plus';
import { Location, Rank, Refresh, Setting } from '@element-plus/icons-vue';
import dayjs from 'dayjs';
import { isMobile } from '@/utils/validate';
const defaultTransportType = '';
const defaultCandidateTransportType = '';
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
const loadingCreateWaybillsStorageKey = 'loading-manage-create-waybills';
let detailAmapLoader;
const createSearchForm = () => ({
loadingNo: '',
projectName: '',
customerName: '',
transportType: defaultTransportType,
waybillNo: '',
cargoType: '',
departureAddress: '',
arrivalAddress: '',
cargoName: '',
driverName: '',
vehicleNo: '',
originalNo: '',
carrierName: '',
dataSource: '',
startDateRange: [],
endDateRange: [],
businessStatus: '',
planName: '',
batchNo: '',
});
const createCandidateSearchForm = () => ({
waybillNo: '',
projectName: '',
departureAddress: '',
cargoName: '',
arrivalAddress: '',
customerName: '',
cargoType: '',
originalNo: '',
vehicleNo: '',
carrierName: '',
driverName: '',
transportType: defaultCandidateTransportType,
});
const createDialogForm = () => ({
id: '',
loadingNo: '',
loadingSubNos: '',
waybillIdsJson: '',
projectName: '',
customerName: '',
transportType: '',
cargoType: '',
cargoName: '',
vehicleNo: '',
trailerVehicleNo: '',
driverName: '',
driverPhone: '',
carrierType: '承运商',
carrierName: '',
carrierContractId: '',
escortName: '',
escortPhone: '',
departureAddress: '',
transitAddress: '',
arrivalAddress: '',
originalNo: '',
dataSource: '手工创建',
startDate: '',
endDate: '',
planName: '',
batchNo: '',
currentProcessNode: '接单',
mileage: '',
estimatedStartDate: '',
estimatedEndDate: '',
taskRemark: '',
goodsJson: '',
routeJson: '',
taskInfoJson: '',
businessStatus: 'draft',
});
// 后端以 -1 表示未填写里程,表单中应保持为空,避免给用户造成已填写的错觉。
const normalizeLoadingMileage = value => {
if (value === undefined || value === null || value === '' || Number(value) === -1) return '';
return value;
};
const createCopiedDialogForm = (detail = {}) => {
const form = createDialogForm();
Object.keys(form).forEach(prop => {
if (Object.prototype.hasOwnProperty.call(detail, prop)) form[prop] = detail[prop];
});
return {
...form,
id: '',
loadingNo: '',
startDate: '',
endDate: '',
dataSource: '手工创建',
currentProcessNode: '接单',
mileage: normalizeLoadingMileage(form.mileage),
taskInfoJson: '',
businessStatus: 'draft',
};
};
const unwrapRecords = res => {
const data = res?.data || res;
if (Array.isArray(data)) return data;
if (Array.isArray(data?.records)) return data.records;
if (Array.isArray(data?.data)) return data.data;
if (Array.isArray(data?.data?.records)) return data.data.records;
return [];
};
const unwrapPage = res => {
const data = res?.data?.data || res?.data || res;
return {
records: data?.records || [],
total: Number(data?.total || 0),
};
};
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 parseIdJsonArray = value => {
if (Array.isArray(value)) return value.map(id => String(id));
if (typeof value !== 'string' || !value.trim()) return [];
try {
const normalizedValue = value.replace(/(^\s*\[\s*|,\s*)(-?\d{16,})(\s*(?=,|\]))/g, '$1"$2"$3');
const result = JSON.parse(normalizedValue);
return Array.isArray(result) ? result.map(id => String(id)) : [];
} catch (error) {
return [];
}
};
const uniqueText = (rows, prop) =>
[
...new Set(
rows
.map(row => row?.[prop])
.filter(value => value !== undefined && value !== null && value !== '')
),
].join('、');
export default {
components: {
Rank,
},
data() {
return {
Rank,
Location,
Refresh,
Setting,
loading: false,
tableSize: 'default',
tableData: [],
page: {
currentPage: 1,
pageSize: 10,
total: 0,
pageSizes: [10, 20, 50, 100],
},
searchExpanded: false,
searchForm: createSearchForm(),
selectionRows: [],
dialogMode: 'add',
dialogLoading: false,
dialogSaving: '',
dialogForm: createDialogForm(),
candidateSearchVisible: false,
candidateQuery: createCandidateSearchForm(),
candidateLoading: false,
candidateAdding: false,
candidateRows: [],
candidateSelection: [],
candidatePage: {
currentPage: 1,
pageSize: 10,
total: 0,
pageSizes: [10, 20, 50, 100],
},
selectedWaybillRows: [],
routeNodes: [],
cargoRows: [],
routeDragIndex: -1,
transportTypeOptions: [],
projectOptions: [],
customerOptions: [],
cargoTypeOptions: [],
driverOptions: [],
carrierOptions: [],
taskCarrierOptions: [],
planOptions: [],
projectLoading: false,
customerLoading: false,
cargoTypeLoading: false,
driverLoading: false,
escortLoading: false,
carrierLoading: false,
taskCarrierLoading: false,
planLoading: false,
taskCarrierRequestId: 0,
carrierTypeOptions,
statusOptions: loadingStatusOptions,
dataSourceOptions: loadingManageConfig.dataSourceOptions,
config: loadingManageConfig,
processNodes: [],
processProjectId: '',
processProjectOptions: [],
routeChangeVisible: false,
routeChangeTab: 'change',
routeChangeRows: [],
routeChangeNodes: [],
routeChangeRemark: '',
routeChangeRecords: [],
routeChangeDragIndex: -1,
commonAddressVisible: false,
commonAddressLoading: false,
commonAddressRows: [],
commonAddressQuery: { addressName: '' },
commonAddressPage: { currentPage: 1, pageSize: 10, total: 0 },
commonAddressTarget: null,
detailAmap: null,
};
},
computed: {
dialogReadonly() {
return this.dialogMode === 'view';
},
taskCarrierPlaceholder() {
if (!this.selectedWaybillRows.length) return '请先选择运单';
if (!this.taskCarrierLoading && !this.taskCarrierOptions.length) return '暂无可用承运商合同';
return '请选择';
},
detailStatusType() {
const typeMap = {
pending: 'success',
running: 'success',
completed: 'info',
cancelled: 'info',
draft: 'warning',
};
return typeMap[this.dialogForm.businessStatus] || 'info';
},
taskSummary() {
return (
[this.dialogForm.carrierName, this.dialogForm.driverName, this.dialogForm.vehicleNo]
.filter(Boolean)
.join(' / ') || '-'
);
},
detailAttachments() {
return parseJsonArray(this.dialogForm.attachmentsJson || this.dialogForm.attachmentJson).map(
item =>
typeof item === 'string'
? { name: item.split('/').pop(), url: item }
: {
name: item.name || item.fileName || '附件',
url: item.url || item.link || item.fileUrl,
}
);
},
// 独立表单页:/business/loading-manage/form?mode=add|edit|reassign
isStandaloneFormPage() {
return (
this.$route.path === '/business/loading-manage/form' &&
['add', 'edit', 'reassign'].includes(this.$route.query.mode)
);
},
// 独立详情页:/business/loading-manage/detail?id=xxx
isStandaloneDetailPage() {
return this.$route.path === '/business/loading-manage/detail';
},
// 列表容器隐藏搜索栏 / 工具栏 / 表格 / 分页的统一开关
isStandalonePage() {
return this.isStandaloneFormPage || this.isStandaloneDetailPage;
},
formPageDefaultTitle() {
const titleMap = {
add: '新增配载管理',
edit: '编辑配载管理',
reassign: '重新派单',
};
return titleMap[this.$route.query.mode] || '新增配载管理';
},
formPageTitle() {
return this.$route.query.name || this.formPageDefaultTitle;
},
detailPageTitle() {
return this.$route.query.name || '配载单详情';
},
},
mounted() {
this.loadTransportTypeOptions();
this.loadTable();
if (this.isStandalonePage) {
this.syncStandaloneTagTitle();
if (this.isStandaloneDetailPage) this.initStandaloneDetailPage();
else this.initStandaloneFormPage();
}
// 兼容旧链接:/business/loading-manage?detailId=xxx 直接跳独立详情页
const detailId = this.$route.query.detailId;
if (detailId) this.gotoLoadingDetail({ id: detailId });
},
// 标签切走(实例被 keep-alive 缓存)或销毁时,先收起页内子弹窗
deactivated() {
this.closeInnerDialogs();
},
beforeUnmount() {
this.closeInnerDialogs();
},
watch: {
$route(to, from) {
if (!this.isStandalonePage) return;
this.syncStandaloneTagTitle();
if (this.isStandaloneDetailPage) {
// 记录变化或上一个页面不是详情态(例如从表单页/列表页进来)时重新拉取详情
const idChanged = String(to.query.id || '') !== String(from?.query?.id || '');
if (idChanged || this.dialogMode !== 'view') this.initStandaloneDetailPage();
return;
}
if (
to.query.mode !== from?.query?.mode ||
String(to.query.id || '') !== String(from?.query.id || '') ||
String(to.query.copyId || '') !== String(from?.query.copyId || '')
) {
this.initStandaloneFormPage();
}
},
'$route.query.detailId'(detailId) {
if (detailId) this.gotoLoadingDetail({ id: detailId });
},
},
methods: {
syncStandaloneTagTitle() {
if (!this.isStandalonePage) return;
const title = this.isStandaloneDetailPage ? this.detailPageTitle : this.formPageDefaultTitle;
if (this.$route.query.name === title) return;
this.$store.commit('SET_TAG', {
fullPath: this.$route.fullPath,
query: { ...this.$route.query, name: title },
});
},
buildQueryParams(form) {
const params = { ...form };
const [startDateStart, startDateEnd] = params.startDateRange || [];
const [endDateStart, endDateEnd] = params.endDateRange || [];
delete params.startDateRange;
delete params.endDateRange;
return {
...params,
startDateStart,
startDateEnd,
endDateStart,
endDateEnd,
};
},
async loadTable() {
this.loading = true;
try {
const res = await loadingApi.getList(
this.page.currentPage,
this.page.pageSize,
this.buildQueryParams(this.searchForm)
);
const page = unwrapPage(res);
this.tableData = page.records;
this.page.total = page.total;
} finally {
this.loading = false;
}
},
handleSearch() {
this.page.currentPage = 1;
this.loadTable();
},
handleResetSearch() {
this.searchForm = createSearchForm();
this.page.currentPage = 1;
this.loadTable();
},
handleSizeChange(size) {
this.page.currentPage = 1;
this.page.pageSize = size;
this.loadTable();
},
handleCurrentChange(current) {
this.page.currentPage = current;
this.loadTable();
},
selectionChange(rows) {
this.selectionRows = rows || [];
},
toggleTableSize() {
const order = ['default', 'small', 'large'];
const index = order.indexOf(this.tableSize);
this.tableSize = order[(index + 1) % order.length];
},
handleColumnSetting() {
ElMessage.info('列设置按原型保留入口,当前默认展示全部列表字段');
},
statusText(row) {
return row.businessStatusName || loadingStatusMap[row.businessStatus] || '未知';
},
formatCandidateWaybillValue(row, prop) {
if (!row) return '-';
if (prop === 'businessStatus') return this.statusText(row);
if (prop === 'transportType') return this.transportTypeLabel(row.transportType);
if (prop === 'goodsInfo') return this.formatCandidateGoodsInfo(row);
const column = waybillOption.column.find(item => item.prop === prop);
if (column?.formatter) {
const value = column.formatter(row, {}, row[prop]);
return value === undefined || value === null || value === '' ? '-' : value;
}
const value = row[prop];
return value === undefined || value === null || value === '' ? '-' : value;
},
formatCandidateGoodsInfo(row = {}) {
const goodsRows =
[row.goodsJson, row.goodsList, row.goodsRows, row.cargoList, row.cargoRows, row.goods]
.map(source => {
if (source === undefined || source === null || source === '') return [];
if (Array.isArray(source)) return source;
if (typeof source === 'object') return [source];
return parseJsonArray(source);
})
.filter(source => source.length)
.sort((left, right) => right.length - left.length)[0] || [];
if (!goodsRows.length) return String(row.goodsInfo || row.cargoInfo || '') || '-';
const groups = goodsRows.reduce((result, item = {}) => {
const unit = String(
item.quantityUnit ||
item.goodsQuantityUnit ||
item.cargoUnit ||
item.unit ||
row.quantityUnit ||
row.goodsQuantityUnit ||
row.cargoUnit ||
row.unit ||
''
).trim();
const key = unit || '__empty__';
if (!result[key]) {
result[key] = {
typeName:
item.secondCargoTypeName ||
item.secondCargoType ||
item.cargoType ||
item.goodsType ||
item.typeName ||
'货物',
count: 0,
quantity: 0,
unit,
};
}
result[key].count += 1;
result[key].quantity += this.parseGoodsQuantity(
item.quantity ?? item.cargoQuantity ?? item.goodsQuantity
);
return result;
}, {});
return (
Object.values(groups)
.map(group => {
const quantity = this.formatGoodsQuantity(group.quantity);
return `${group.typeName}等${group.count}种货物 | ${quantity}${
group.unit ? ` ${group.unit}` : ''
}`;
})
.join(' ') || '-'
);
},
parseGoodsQuantity(value) {
if (value === undefined || value === null || value === '') return 0;
const text = String(value).replace(/,/g, '');
const match = text.match(/-?\d+(\.\d+)?/);
if (!match) return 0;
const number = Number(match[0]);
return Number.isFinite(number) ? number : 0;
},
formatGoodsQuantity(value) {
const number = Number(value || 0);
if (!Number.isFinite(number)) return '0';
const fixed = Math.round((number + Number.EPSILON) * 1000) / 1000;
return Number.isInteger(fixed) ? String(fixed) : String(fixed).replace(/\.?0+$/, '');
},
formatCandidateDate(value) {
if (!value) return '-';
const date = dayjs(value);
return date.isValid() ? date.format('YYYY-MM-DD HH:mm:ss') : value;
},
formatCandidateAddress(row, type) {
const prefix = type === 'departure' ? 'departure' : 'arrival';
const name = String(row?.[`${prefix}Name`] || '').trim();
const address = String(row?.[`${prefix}Address`] || '').trim();
if (!this.isRoadTransportType(row?.transportType)) {
return name || address || '-';
}
const region = this.mergeRouteAddressParts(
this.routeProvinceName(row, prefix),
row[`${prefix}CityName`] || row[`${prefix}City`],
row[`${prefix}DistrictName`] || row[`${prefix}District`]
);
const source = this.mergeRouteAddressParts(region, '', address || name);
const parts = this.parseWaybillAddress(source);
return (
[parts.province, parts.city, parts.district]
.map(value => String(value || '').trim())
.filter(Boolean)
.join('') ||
this.formatProvinceCityDistrict(source) ||
name ||
address ||
'-'
);
},
formatCandidateAddressTooltip(row, type) {
if (!this.isRoadTransportType(row?.transportType)) return '';
const prefix = type === 'departure' ? 'departure' : 'arrival';
const address = String(row?.[`${prefix}Address`] || '').trim();
const name = String(row?.[`${prefix}Name`] || '').trim();
const region = this.mergeRouteAddressParts(
this.routeProvinceName(row, prefix),
row[`${prefix}CityName`] || row[`${prefix}City`],
row[`${prefix}DistrictName`] || row[`${prefix}District`]
);
const detailAddress =
row[`${prefix}DetailAddress`] || row[`${prefix}AddressDetail`] || address;
return this.mergeRouteAddressParts(region, '', detailAddress || name) || '';
},
isRoadTransportType(type) {
const value = String(type || '')
.trim()
.toLowerCase();
return (
value === 'road' ||
value.includes('公路') ||
value.includes('道路') ||
value.includes('highway')
);
},
splitText(value) {
if (!value) return [];
return String(value)
.split(/[\n,]+/)
.map(item => item.trim())
.filter(Boolean);
},
findWaybillId(row, waybillNo) {
const rows = this.splitText(row.loadingSubNos);
const index = rows.findIndex(item => item === waybillNo);
const linkedRows = parseJsonArray(row.waybillList || row.waybillRows || row.waybills);
if (linkedRows[index]?.id || linkedRows[index]?.waybillId) {
return linkedRows[index].id || linkedRows[index].waybillId;
}
const ids = parseIdJsonArray(row.waybillIdsJson);
return ids[index] || '';
},
async openWaybillDetail(row = {}) {
let id = row.id || row.waybillId || '';
if (!id && row.waybillNo) {
try {
const res = await getWaybillList(1, 1, { waybillNo: row.waybillNo });
const page = unwrapPage(res);
id = page.records?.[0]?.id || page[0]?.id || '';
} catch (error) {
id = '';
}
}
if (!id) {
ElMessage.info('当前子运单暂无详情数据');
return;
}
this.$router.push({ path: '/business/waybill-manage/detail', query: { id } });
},
formatRouteAddress(row, type) {
const prefix =
type === 'departure' ? 'departure' : type === 'arrival' ? 'arrival' : 'transit';
if (type !== 'transit') {
const address = row[`${prefix}Address`];
const region = this.routeProvinceName(row, prefix);
const district = row[`${prefix}DistrictName`];
const source = this.mergeRouteAddressParts(region, district, address);
return this.formatProvinceCityDistrict(source) || '-';
}
const values = [
row[`${prefix}RegionName`],
row[`${prefix}DistrictName`],
row[`${prefix}Address`],
]
.filter(Boolean)
.flatMap(value => this.splitText(value))
.map(value => this.formatProvinceCityDistrict(value))
.filter(Boolean);
return [...new Set(values)].join(',') || '-';
},
formatFullRouteAddress(row, type) {
const prefix =
type === 'departure' ? 'departure' : type === 'arrival' ? 'arrival' : 'transit';
if (type !== 'transit') {
const address = row[`${prefix}Address`];
const region = this.routeProvinceName(row, prefix);
const district = row[`${prefix}DistrictName`];
return this.mergeRouteAddressParts(region, district, address) || '-';
}
const values = [row.transitRegionName, row.transitDistrictName, row.transitAddress]
.filter(Boolean)
.flatMap(value => this.splitText(value));
return [...new Set(values)].join('') || '-';
},
formatWaybillAddress(row, type) {
const prefix = type === 'departure' ? 'departure' : 'arrival';
const address = row[`${prefix}Address`] || row[`${prefix}Name`] || '';
const region = this.mergeRouteAddressParts(
this.routeProvinceName(row, prefix),
row[`${prefix}CityName`] || row[`${prefix}City`],
row[`${prefix}DistrictName`] || row[`${prefix}District`]
);
const source = this.mergeRouteAddressParts(region, '', address);
const parts = this.parseWaybillAddress(source);
const detailAddress =
row[`${prefix}DetailAddress`] ||
row[`${prefix}AddressDetail`] ||
parts.detailAddress ||
address;
return (
[parts.province, parts.city, parts.district, detailAddress]
.map(value => String(value || '').trim())
.filter(Boolean)
.join(' / ') || '-'
);
},
parseWaybillAddress(address) {
let rest = String(address || '').trim();
const result = { province: '', city: '', district: '', detailAddress: '' };
if (!rest) return result;
const municipalityMatch = rest.match(/^(北京市|天津市|上海市|重庆市)/);
if (municipalityMatch) {
result.province = municipalityMatch[0];
result.city = municipalityMatch[0];
rest = rest.slice(municipalityMatch[0].length);
} else {
const provinceMatch = rest.match(/^(.+?(?:省|自治区|特别行政区))/);
if (provinceMatch) {
result.province = provinceMatch[0];
rest = rest.slice(provinceMatch[0].length);
}
const cityMatch = rest.match(/^(.+?(?:市|自治州|地区|盟))/);
if (cityMatch) {
result.city = cityMatch[0];
rest = rest.slice(cityMatch[0].length);
}
}
const districtMatch = rest.match(/^(.+?(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗))/);
if (districtMatch) {
result.district = districtMatch[0];
rest = rest.slice(districtMatch[0].length);
}
result.detailAddress = rest;
return result;
},
formatAddressText(address) {
const parts = this.parseWaybillAddress(address);
return (
[parts.province, parts.city, parts.district, parts.detailAddress]
.map(value => String(value || '').trim())
.filter(Boolean)
.join(' / ') ||
String(address || '').trim() ||
'-'
);
},
mergeRouteAddressParts(region, district, address) {
const values = [region, district, address]
.map(value => String(value || '').trim())
.filter(Boolean);
if (!values.length) return '';
return values.reduce((result, value) => {
if (!result) return value;
if (result.includes(value) || value.includes(result))
return result.length >= value.length ? result : value;
return `${result}${value}`;
}, '');
},
routeProvinceName(row, prefix) {
return (
[
row[`${prefix}ProvinceName`],
row[`${prefix}Province`],
row[`${prefix}RegionName`],
row[`${prefix}Region`],
row[`${prefix}Name`],
row[`${prefix}AdministrativeRegionName`],
row[`${prefix}AddressRegionName`],
].find(value => String(value || '').trim()) || ''
);
},
formatProvinceCityDistrict(value) {
const text = String(value || '').trim();
if (!text) return '';
const districtMatch = text.match(/(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗)/);
if (districtMatch) return text.slice(0, districtMatch.index + districtMatch[0].length);
const cityMatch = text.match(/市/);
return cityMatch ? text.slice(0, cityMatch.index + 1) : text;
},
rowActions(row) {
const status = row.businessStatus;
if (status === 'draft') {
return [
{ type: 'edit', label: '编辑' },
{ type: 'delete', label: '删除' },
];
}
if (status === 'pending') {
const actions = [
{ type: 'view', label: '详情' },
{ type: 'start', label: '改为进行中' },
];
if (row.driverRejected) {
actions.push({ type: 'reassign', label: '重新派单' });
}
actions.push({ type: 'edit', label: '编辑' }, { type: 'cancel', label: '取消' });
return actions;
}
if (status === 'running') {
return [
{ type: 'view', label: '详情' },
{ type: 'complete', label: '完成' },
];
}
if (status === 'completed' || status === 'cancelled') {
return [{ type: 'view', label: '详情' }];
}
return [{ type: 'view', label: '详情' }];
},
handleRowAction(type, row) {
const actionMap = {
view: () => this.gotoLoadingDetail(row),
edit: () => this.openLoadingDialog('edit', row),
reassign: () => this.gotoLoadingReassign(row),
delete: () => this.deleteLoading(row),
start: () => this.startLoading(row),
cancel: () => this.cancelLoading(row),
complete: () => this.completeLoading(row),
};
actionMap[type]?.();
},
// 详情改为独立页面(与新增/编辑一致)
gotoLoadingDetail(row = {}) {
const id = row.id || row.loadingId || row.loadingManageId;
if (!id) {
ElMessage.info('当前配载单暂无详情数据');
return;
}
if (this.isStandaloneDetailPage && String(this.$route.query.id || '') === String(id)) return;
this.$router.push({
path: '/business/loading-manage/detail',
query: { id, name: '配载单详情' },
});
},
// 重新派单改为独立页面
gotoLoadingReassign(row = {}) {
if (!row.id) {
ElMessage.warning('配载单不存在,无法重新派单');
return;
}
this.$router.push({
path: '/business/loading-manage/form',
query: { mode: 'reassign', id: row.id, name: '重新派单' },
});
},
async initStandaloneDetailPage() {
if (!this.isStandaloneDetailPage) return;
const id = this.$route.query.id;
if (!id) {
ElMessage.warning('缺少配载单参数,无法查看详情');
this.closeLoadingDialog();
return;
}
await this.openLoadingDialog('view', { id });
},
async initStandaloneFormPage() {
if (!this.isStandaloneFormPage) return;
const queryMode = this.$route.query.mode;
const mode = ['edit', 'reassign'].includes(queryMode) ? queryMode : 'add';
const copyId = mode === 'add' ? this.$route.query.copyId : '';
await this.openLoadingDialog(mode, {
id: copyId || this.$route.query.id,
copy: Boolean(copyId),
});
if (mode === 'add' && !copyId) await this.consumeLoadingCreateWaybills();
},
async consumeLoadingCreateWaybills() {
const raw = sessionStorage.getItem(loadingCreateWaybillsStorageKey);
if (!raw) return;
sessionStorage.removeItem(loadingCreateWaybillsStorageKey);
try {
const payload = JSON.parse(raw);
if (payload?.source !== 'waybill-manage' || !Array.isArray(payload.rows)) return;
await this.addWaybillRows(payload.rows);
} catch (error) {
ElMessage.warning('待配载运单数据读取失败,请重新选择');
}
},
async openLoadingDialog(mode, row) {
// 列表页进入新增/编辑/重新派单:统一走独立页面
if (['add', 'edit', 'reassign'].includes(mode) && !this.isStandaloneFormPage) {
if (mode === 'reassign') {
this.gotoLoadingReassign(row || {});
return;
}
this.$router.push({
path: '/business/loading-manage/form',
query: {
mode,
...(mode === 'edit' && row?.id ? { id: row.id } : {}),
name: mode === 'edit' ? '编辑配载管理' : '新增配载管理',
},
});
return;
}
this.dialogMode = mode;
this.dialogLoading = true;
this.resetDialogData();
if (mode === 'add') this.candidateQuery.transportType = 'road';
try {
if (row?.id) {
const res = await loadingApi.getDetail(row.id);
const detail = res.data?.data || res.data || {};
this.dialogForm = row.copy
? createCopiedDialogForm(detail)
: { ...createDialogForm(), ...detail };
this.dialogForm.mileage = normalizeLoadingMileage(this.dialogForm.mileage);
await this.restoreWaybillRows(
this.dialogForm.waybillIdsJson,
this.dialogForm.loadingSubNos
);
this.syncProcessProjectOptions();
this.restoreRouteAndCargo();
this.restoreRouteChangeRecords();
await this.loadProcessNodes();
this.$nextTick(() => this.initDetailAmap());
await this.loadCandidateWaybills();
} else {
this.dialogForm = createDialogForm();
this.candidateQuery = createCandidateSearchForm();
this.candidateQuery.transportType = 'road';
await this.loadCandidateWaybills();
}
} finally {
this.dialogLoading = false;
}
},
closeLoadingDialog() {
if (!this.isStandalonePage) return;
this.$router.$avueRouter?.closeTag?.();
this.$router.push({ path: '/business/loading-manage', query: {} });
},
resetDialogData() {
this.detailAmap?.destroy?.();
this.detailAmap = null;
this.dialogForm = createDialogForm();
this.candidateQuery = createCandidateSearchForm();
this.candidateRows = [];
this.candidateSelection = [];
this.selectedWaybillRows = [];
this.taskCarrierRequestId += 1;
this.taskCarrierOptions = [];
this.taskCarrierLoading = false;
this.routeNodes = [];
this.cargoRows = [];
this.candidatePage.currentPage = 1;
this.candidatePage.total = 0;
this.candidateSearchVisible = false;
this.processNodes = [];
this.processProjectId = '';
this.processProjectOptions = [];
},
// 离开本页(标签缓存/卸载)时收起页内子弹窗,避免 append-to-body 的弹窗 DOM 残留在 body 上
closeInnerDialogs() {
this.routeChangeVisible = false;
this.commonAddressVisible = false;
this.candidateSearchVisible = false;
},
restoreRouteAndCargo() {
const routeRows = parseJsonArray(this.dialogForm.routeJson);
const cargoRows = parseJsonArray(this.dialogForm.goodsJson);
this.routeNodes = routeRows.length
? routeRows
: this.buildRouteNodes(this.selectedWaybillRows);
this.cargoRows = cargoRows.length ? cargoRows : this.buildCargoRows(this.selectedWaybillRows);
},
restoreRouteChangeRecords() {
try {
const taskInfo = JSON.parse(this.dialogForm.taskInfoJson || '{}');
this.routeChangeRecords = Array.isArray(taskInfo.routeChangeRecords)
? [...taskInfo.routeChangeRecords].sort((a, b) =>
String(b.changeTime).localeCompare(String(a.changeTime))
)
: [];
} catch (error) {
this.routeChangeRecords = [];
}
},
async loadProcessNodes() {
if (!this.processProjectId) {
this.processNodes = [];
return;
}
try {
const projectId = String(this.processProjectId);
const rows = unwrapRecords(
await getProcessConfigList(1, 100, { projectIds: projectId, status: 1 })
);
const config =
rows.find(item => {
const projectIds = String(item.projectIds || '')
.split(',')
.map(value => value.trim())
.filter(Boolean);
return projectIds.includes(projectId) && item.nodeConfigJson;
}) || {};
this.processNodes = this.parseProcessConfigNodes(config);
} catch (error) {
this.processNodes = [];
}
},
syncProcessProjectOptions() {
const projectMap = new Map();
this.selectedWaybillRows.forEach(row => {
const id = row.projectId;
if (!id || projectMap.has(String(id))) return;
projectMap.set(String(id), {
id,
projectName: row.projectName || row.projectNames || String(id),
});
});
if (!projectMap.size && this.dialogForm.projectId) {
projectMap.set(String(this.dialogForm.projectId), {
id: this.dialogForm.projectId,
projectName: this.dialogForm.projectName || String(this.dialogForm.projectId),
});
}
this.processProjectOptions = [...projectMap.values()];
this.processProjectId = this.processProjectOptions[0]?.id || '';
},
parseProcessConfigNodes(config = {}) {
let nodes = [];
try {
const parsed =
typeof config.nodeConfigJson === 'string'
? JSON.parse(config.nodeConfigJson)
: config.nodeConfigJson;
nodes = Array.isArray(parsed) ? parsed : parsed?.nodes || [];
} catch (error) {
nodes = [];
}
const includedNodes = String(config.includedNodes || '')
.split(',')
.map(value => value.trim())
.filter(Boolean);
return nodes.filter(
node =>
node.enabled !== false && (!includedNodes.length || includedNodes.includes(node.name))
);
},
async handleProcessProjectChange() {
await this.loadProcessNodes();
},
previewAttachment(file) {
if (!file.url) {
ElMessage.info('暂无可预览的附件地址');
return;
}
window.open(file.url, '_blank', 'noopener,noreferrer');
},
loadDetailAmap() {
if (window.AMap?.Map) return Promise.resolve();
if (!detailAmapLoader) {
detailAmapLoader = new Promise((resolve, reject) => {
window._AMapSecurityConfig = { securityJsCode: AMAP_SECURITY_CODE };
const script = document.createElement('script');
script.src = `https://webapi.amap.com/maps?v=2.0&key=${AMAP_KEY}`;
script.async = true;
script.onload = resolve;
script.onerror = () => reject(new Error('高德地图组件加载失败'));
document.body.appendChild(script);
});
}
return detailAmapLoader;
},
initDetailAmap() {
if (!this.$refs.detailAmap) return;
this.loadDetailAmap()
.then(() => {
if (!this.detailAmap) {
this.detailAmap = new window.AMap.Map(this.$refs.detailAmap, {
center: [116.40769, 39.89945],
zoom: 11,
});
window.AMap.plugin(['AMap.ToolBar'], () =>
this.detailAmap?.addControl(new window.AMap.ToolBar())
);
} else {
this.detailAmap.resize?.();
}
})
.catch(() => ElMessage.warning('高德地图加载失败'));
},
handleTrackAction(action) {
ElMessage.info(action === 'playback' ? '暂无可回放的物流轨迹' : '暂无车辆实时定位数据');
},
handleProcessAction(action) {
ElMessage.info(action === 'supplement' ? '补录执行功能暂未接入' : '批量补录功能暂未接入');
},
openRouteChangeDialog() {
this.routeChangeTab = 'change';
this.routeChangeRemark = '';
this.routeChangeRows = this.selectedWaybillRows.map(row => ({
...row,
originalDepartureAddress: row.departureAddress || row.departureName || '',
originalArrivalAddress: row.arrivalAddress || row.arrivalName || '',
departureAddress: row.departureAddress || row.departureName || '',
arrivalAddress: row.arrivalAddress || row.arrivalName || '',
}));
this.routeChangeNodes = this.buildRouteNodes(this.routeChangeRows);
this.routeChangeVisible = true;
},
routeChangeTypeText(index) {
if (index === 0) return '起';
if (index === this.routeChangeNodes.length - 1) return '终';
return '经';
},
routeChangeTypeClass(index) {
if (index === 0) return 'start';
if (index === this.routeChangeNodes.length - 1) return 'end';
return 'middle';
},
handleRouteChangeDragStart(index) {
this.routeChangeDragIndex = index;
},
handleRouteChangeDrop(index) {
if (this.routeChangeDragIndex < 0 || this.routeChangeDragIndex === index) return;
const nodes = [...this.routeChangeNodes];
const [node] = nodes.splice(this.routeChangeDragIndex, 1);
nodes.splice(index, 0, node);
this.routeChangeNodes = nodes;
this.routeChangeDragIndex = -1;
},
updateRouteChangeNodes() {
this.routeChangeNodes = this.buildRouteNodes(this.routeChangeRows);
},
openCommonAddressDialog(row, field) {
this.commonAddressTarget = { row, field };
this.commonAddressQuery = { addressName: '' };
this.commonAddressPage.currentPage = 1;
this.commonAddressVisible = true;
this.loadCommonAddresses();
},
async loadCommonAddresses() {
this.commonAddressLoading = true;
try {
const page = unwrapPage(
await getCommonAddressList(
this.commonAddressPage.currentPage,
this.commonAddressPage.pageSize,
{ ...this.commonAddressQuery, allDept: 0 }
)
);
this.commonAddressRows = page.records;
this.commonAddressPage.total = page.total;
} finally {
this.commonAddressLoading = false;
}
},
selectCommonAddress(address) {
const target = this.commonAddressTarget;
if (!target) return;
target.row[`${target.field}Address`] =
[address.regionName, address.detailAddress || address.addressName]
.filter(Boolean)
.join('') ||
address.addressName ||
'';
this.updateRouteChangeNodes();
this.commonAddressVisible = false;
},
async confirmRouteChange() {
const changeRows = this.routeChangeRows.filter(
row =>
row.departureAddress !== row.originalDepartureAddress ||
row.arrivalAddress !== row.originalArrivalAddress
);
if (!changeRows.length && !this.routeChangeRemark) {
ElMessage.warning('请修改地址或填写变更备注');
return;
}
const content =
changeRows
.map(
row =>
`${row.waybillNo || '运单'}:发货地 ${row.originalDepartureAddress} → ${
row.departureAddress
};到货地 ${row.originalArrivalAddress} → ${row.arrivalAddress}`
)
.join('') || '调整运输路线顺序';
this.routeChangeRecords.unshift({
changeTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
changeUser: '当前用户',
content,
remark: this.routeChangeRemark,
});
let taskInfo = {};
try {
taskInfo = JSON.parse(this.dialogForm.taskInfoJson || '{}');
} catch (error) {
taskInfo = {};
}
this.dialogForm.taskInfoJson = JSON.stringify({
...(typeof taskInfo === 'object' && !Array.isArray(taskInfo) ? taskInfo : {}),
routeChangeRecords: this.routeChangeRecords,
});
this.selectedWaybillRows = this.routeChangeRows.map(
({ originalDepartureAddress, originalArrivalAddress, ...row }) => row
);
this.routeNodes = this.routeChangeNodes;
this.syncRouteAddressFields();
try {
await loadingApi.changeRoute({
id: this.dialogForm.id,
routeJson: JSON.stringify(this.routeNodes),
departureAddress: this.dialogForm.departureAddress,
transitAddress: this.dialogForm.transitAddress,
arrivalAddress: this.dialogForm.arrivalAddress,
taskInfoJson: this.dialogForm.taskInfoJson,
});
this.routeChangeVisible = false;
ElMessage.success('路线变更已保存');
} catch (error) {
this.restoreRouteChangeRecords();
throw error;
}
},
async restoreWaybillRows(idsJson, loadingSubNos) {
const ids = parseIdJsonArray(idsJson).filter(Boolean);
if (ids.length) {
const rows = await Promise.all(
ids.map(id =>
getWaybillDetail(id)
.then(res => res.data?.data || res.data)
.catch(() => null)
)
);
this.selectedWaybillRows = rows.filter(Boolean);
this.rebuildSummaryFromWaybills(false);
await this.loadTaskCarrierOptions();
return;
}
this.selectedWaybillRows = this.splitText(loadingSubNos).map(waybillNo => ({ waybillNo }));
this.rebuildSummaryFromWaybills(false);
await this.loadTaskCarrierOptions();
},
async loadCandidateWaybills() {
this.candidateLoading = true;
try {
const query = {
...this.buildQueryParams(this.candidateQuery),
businessStatus: 'pending',
onlyUnassignedLoading: 1,
...(this.dialogMode === 'add' ? { transportType: 'road' } : {}),
};
const transportType = this.getCandidateTransportTypeQuery(query.transportType);
const res = await getWaybillList(
this.candidatePage.currentPage,
this.candidatePage.pageSize,
{ ...query, transportType }
);
const page = unwrapPage(res);
this.candidateRows = page.records;
this.candidatePage.total = page.total;
} finally {
this.candidateLoading = false;
}
},
getCandidateTransportTypeQuery(value) {
const normalized = String(value || '')
.trim()
.toLowerCase();
if (['road', '公路运输'].includes(normalized)) return 'road,公路运输';
return value || '';
},
handleCandidateSearch() {
this.candidatePage.currentPage = 1;
this.loadCandidateWaybills();
},
handleCandidateReset() {
this.candidateQuery = createCandidateSearchForm();
if (this.dialogMode === 'add') this.candidateQuery.transportType = 'road';
this.candidatePage.currentPage = 1;
this.loadCandidateWaybills();
},
handleCandidateSizeChange(size) {
this.candidatePage.currentPage = 1;
this.candidatePage.pageSize = size;
this.loadCandidateWaybills();
},
handleCandidateCurrentChange(current) {
this.candidatePage.currentPage = current;
this.loadCandidateWaybills();
},
candidateSelectionChange(rows) {
this.candidateSelection = rows || [];
},
async addWaybillRows(rows = []) {
const waybillRows = Array.isArray(rows) ? rows.filter(Boolean) : [];
if (!waybillRows.length) return;
this.candidateAdding = true;
try {
const selectedRows = await Promise.all(
waybillRows.map(async row => {
if (!row.id) return row;
try {
const res = await getWaybillDetail(row.id);
return { ...row, ...(res.data?.data || res.data || {}) };
} catch (error) {
return row;
}
})
);
const exists = new Set(
this.selectedWaybillRows.map(row => String(row.id || row.waybillNo))
);
const nextRows = [...this.selectedWaybillRows];
selectedRows.forEach(row => {
const key = String(row.id || row.waybillNo);
if (!exists.has(key)) {
nextRows.push(row);
exists.add(key);
}
});
this.selectedWaybillRows = nextRows;
this.rebuildSummaryFromWaybills();
await this.loadTaskCarrierOptions();
this.$refs.candidateTableRef?.clearSelection?.();
this.candidateSelection = [];
} finally {
this.candidateAdding = false;
}
},
async addSelectedWaybills() {
await this.addWaybillRows(this.candidateSelection);
},
removeSelectedWaybill(index) {
this.selectedWaybillRows.splice(index, 1);
this.rebuildSummaryFromWaybills();
this.loadTaskCarrierOptions();
},
rebuildSummaryFromWaybills(rebuildRoute = true) {
const rows = this.selectedWaybillRows;
this.dialogForm.loadingSubNos = rows
.map(row => row.waybillNo)
.filter(Boolean)
.join('');
this.dialogForm.waybillIdsJson = JSON.stringify(
rows
.map(row => row.id)
.filter(Boolean)
.map(id => String(id))
);
this.dialogForm.projectName = uniqueText(rows, 'projectName');
this.dialogForm.customerName = uniqueText(rows, 'customerName');
this.dialogForm.originalNo = uniqueText(rows, 'originalNo');
this.dialogForm.cargoType = uniqueText(rows, 'cargoType');
this.dialogForm.cargoName = uniqueText(rows, 'cargoName');
if (!this.dialogForm.transportType) {
this.dialogForm.transportType = rows.find(row => row.transportType)?.transportType || '';
}
if (rebuildRoute) {
this.routeNodes = this.buildRouteNodes(rows);
}
this.cargoRows = this.buildCargoRows(rows);
this.syncRouteAddressFields();
},
buildRouteNodes(rows) {
const nodeMap = new Map();
const appendNode = (address, displayAddress, tag) => {
if (!address) return;
if (!nodeMap.has(address)) nodeMap.set(address, { address, displayAddress, tags: [] });
nodeMap.get(address).tags.push(tag);
};
rows.forEach((row, rowIndex) => {
const departure = row.departureAddress || row.departureName;
appendNode(departure, this.formatCandidateAddress(row, 'departure'), `装${rowIndex + 1}`);
});
rows.forEach((row, rowIndex) => {
const arrival = row.arrivalAddress || row.arrivalName;
appendNode(arrival, this.formatCandidateAddress(row, 'arrival'), `卸${rowIndex + 1}`);
});
return [...nodeMap.values()].map((node, index) => ({
...node,
key: `${node.address}-${index}`,
}));
},
routeTypeText(index) {
if (index === 0) return '起';
if (index === this.routeNodes.length - 1) return '终';
return '经';
},
routeTypeClass(index) {
if (index === 0) return 'start';
if (index === this.routeNodes.length - 1) return 'end';
return 'middle';
},
handleRouteDragStart(index) {
this.routeDragIndex = index;
},
handleRouteDrop(index) {
if (this.routeDragIndex < 0 || this.routeDragIndex === index) return;
const rows = [...this.routeNodes];
const [target] = rows.splice(this.routeDragIndex, 1);
rows.splice(index, 0, target);
this.routeDragIndex = -1;
if (!this.validateRouteEndpoints(rows)) return;
this.routeNodes = rows;
this.syncRouteAddressFields();
},
validateRouteEndpoints(nodes = this.routeNodes) {
const startTags = nodes[0]?.tags || [];
if (startTags.some(tag => String(tag).startsWith('卸'))) {
ElMessage.warning('起点不能设置卸货');
return false;
}
const endTags = nodes[nodes.length - 1]?.tags || [];
if (endTags.some(tag => String(tag).startsWith('装'))) {
ElMessage.warning('终点不能设置装货');
return false;
}
return true;
},
syncRouteAddressFields() {
const nodes = this.routeNodes.filter(item => item.address);
this.dialogForm.departureAddress = nodes[0]?.address || '';
this.dialogForm.arrivalAddress = nodes[nodes.length - 1]?.address || '';
this.dialogForm.transitAddress = nodes
.slice(1, -1)
.map(item => item.address)
.join('');
},
buildCargoRows(rows) {
return rows.flatMap(row => {
const goodsRows = parseJsonArray(row.goodsList || row.goodsRows || row.goodsJson);
if (goodsRows.length) {
return goodsRows.map(item => this.normalizeCargoRow(row, item));
}
return [this.normalizeCargoRow(row, row)];
});
},
normalizeCargoRow(waybill, item = {}) {
return {
projectName: item.projectName || waybill.projectName || '',
cargoName: item.cargoName || item.goodsName || waybill.cargoName || '',
cargoType: item.cargoType || item.goodsType || waybill.cargoType || '',
packageType: item.packageType || item.package || '',
weight: item.weight || item.cargoWeight || '',
volume: item.volume || item.cargoVolume || '',
quantity: item.quantity || item.cargoQuantity || waybill.quantity || '',
quantityUnit:
item.quantityUnit ||
item.goodsQuantityUnit ||
item.cargoUnit ||
item.unit ||
waybill.quantityUnit ||
waybill.goodsQuantityUnit ||
waybill.cargoUnit ||
waybill.unit ||
'',
planDate: item.planDate || waybill.startDate || '',
unitPrice: item.unitPrice || waybill.unitPrice || '',
materialCode: item.materialCode || '',
equipmentCode:
item.equipmentCode ||
item.deviceCode ||
waybill.equipmentCode ||
waybill.deviceCode ||
'',
brand: item.brand || item.brandName || waybill.brand || '',
specificationModel:
item.specificationModel ||
item.specModel ||
item.specification ||
waybill.specificationModel ||
'',
remark: item.remark || '',
};
},
normalizePayload() {
this.syncRouteAddressFields();
this.rebuildSummaryFromWaybills(false);
this.dialogForm.mileage = normalizeLoadingMileage(this.dialogForm.mileage);
return {
...this.dialogForm,
routeJson: JSON.stringify(this.routeNodes),
goodsJson: JSON.stringify(this.cargoRows),
taskInfoJson: JSON.stringify({
carrierType: this.dialogForm.carrierType,
carrierName: this.dialogForm.carrierName,
carrierContractId: this.dialogForm.carrierContractId,
driverName: this.dialogForm.driverName,
driverPhone: this.dialogForm.driverPhone,
vehicleNo: this.dialogForm.vehicleNo,
trailerVehicleNo: this.dialogForm.trailerVehicleNo,
escortName: this.dialogForm.escortName,
escortPhone: this.dialogForm.escortPhone,
mileage: this.dialogForm.mileage,
estimatedStartDate: this.dialogForm.estimatedStartDate,
estimatedEndDate: this.dialogForm.estimatedEndDate,
taskRemark: this.dialogForm.taskRemark,
}),
};
},
validatePayload(draftMode = false) {
if (!this.selectedWaybillRows.length) {
ElMessage.warning('请选择待配载运单');
return false;
}
if (!this.dialogForm.transportType) {
ElMessage.warning('请选择运输方式');
return false;
}
const invalidPhone = [
[this.dialogForm.driverPhone, '手机号'],
[this.dialogForm.escortPhone, '押运人手机号'],
].find(([value]) => String(value || '').trim() && !isMobile(value));
if (invalidPhone) {
ElMessage.warning(`${invalidPhone[1]}格式不正确`);
return false;
}
if (draftMode) {
return true;
}
if (this.selectedWaybillRows.length < 2) {
ElMessage.warning('请至少选择两条待配载运单');
return false;
}
if (!this.validateRouteEndpoints()) {
return false;
}
if (!this.dialogForm.departureAddress || !this.dialogForm.arrivalAddress) {
ElMessage.warning('请确认配载路线');
return false;
}
if (!this.dialogForm.carrierType) {
ElMessage.warning('请选择承运类型');
return false;
}
if (!this.dialogForm.vehicleNo) {
ElMessage.warning('请输入车牌号');
return false;
}
if (this.dialogForm.carrierType === '承运商' && !this.dialogForm.carrierContractId) {
ElMessage.warning('请选择承运商合同');
return false;
}
if (this.dialogForm.carrierType === '自运') {
if (!this.dialogForm.driverName || !this.dialogForm.driverPhone) {
ElMessage.warning('请输入司机和司机手机号');
return false;
}
}
return true;
},
async saveDraft() {
if (!this.validatePayload(true)) return;
this.dialogSaving = 'draft';
try {
const res = await loadingApi.saveDraft(this.normalizePayload());
this.dialogForm = {
...this.dialogForm,
...(res.data?.data || res.data || {}),
};
this.dialogForm.mileage = normalizeLoadingMileage(this.dialogForm.mileage);
ElMessage.success('暂存成功');
this.loadTable();
} finally {
this.dialogSaving = '';
}
},
async submitLoading() {
if (!this.validatePayload(false)) return;
this.dialogSaving = 'submit';
try {
const payload = this.normalizePayload();
if (this.dialogMode === 'reassign') {
await loadingApi.reassign(payload);
ElMessage.success('派单成功');
} else {
await loadingApi.submit(payload);
ElMessage.success('确认成功');
}
this.closeLoadingDialog();
this.loadTable();
} finally {
this.dialogSaving = '';
}
},
deleteLoading(row) {
ElMessageBox.confirm(`确定删除配载单 ${row.loadingNo}`, '提示', { type: 'warning' })
.then(async () => {
const res = await loadingApi.remove(row.id);
const result = res.data?.data || {};
ElMessage.success(`删除成功 ${result.successCount || 0} 条`);
this.loadTable();
})
.catch(() => {});
},
cancelLoading(row) {
ElMessageBox.confirm(`确定取消配载单 ${row.loadingNo}`, '提示', { type: 'warning' })
.then(async () => {
await loadingApi.cancel(row.id);
ElMessage.success('取消成功');
this.loadTable();
})
.catch(() => {});
},
startLoading(row) {
ElMessageBox.confirm(`确定将配载单 ${row.loadingNo} 改为进行中?`, '提示', { type: 'warning' })
.then(async () => {
await loadingApi.start(row.id);
ElMessage.success('已改为进行中');
this.loadTable();
})
.catch(() => {});
},
completeLoading(row) {
ElMessageBox.confirm(`确定完成配载单 ${row.loadingNo}`, '提示', { type: 'warning' })
.then(async () => {
await loadingApi.complete(row.id);
ElMessage.success('完成成功');
this.loadTable();
})
.catch(() => {});
},
handleBatchComplete() {
if (!this.selectionRows.length) {
ElMessage.warning('请选择需要批量完成的配载单');
return;
}
ElMessageBox.confirm('确定对选中的配载单执行批量完成?', '提示', { type: 'warning' })
.then(async () => {
const ids = this.selectionRows.map(row => row.id).join(',');
const res = await loadingApi.batchComplete(ids);
const result = res.data?.data || {};
const summary = `批量完成成功 ${result.successCount || 0} 条,跳过 ${
result.skippedCount || 0
} 条`;
if (result.skippedCount && result.skippedReasons?.length) {
ElMessage.warning(`${summary}${result.skippedReasons.join('')}`);
} else {
ElMessage.success(summary);
}
this.loadTable();
})
.catch(() => {});
},
handleExport() {
const ids = this.selectionRows.map(row => row.id).join(',');
exportBlob(
loadingApi.exportUrl,
{
...this.buildQueryParams(this.searchForm),
ids,
exportColumns: JSON.stringify(loadingManageConfig.exportColumns),
},
{
feedback: true,
}
).then(res => {
downloadXls(res.data, `配载管理${dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`);
});
},
handleCarrierTypeChange() {
this.dialogForm.carrierContractId = '';
this.dialogForm.carrierName = '';
this.dialogForm.carrierId = '';
if (this.dialogForm.carrierType === '承运商') {
this.dialogForm.trailerVehicleNo = '';
this.dialogForm.escortName = '';
this.dialogForm.escortPhone = '';
}
if (this.dialogForm.carrierType === '自运') {
this.dialogForm.carrierContractId = '';
}
this.loadTaskCarrierOptions();
},
handleTaskCarrierChange(contractId) {
const contract = this.taskCarrierOptions.find(item =>
[item.id, item.carrierName, item.partyB].some(
value => String(value || '') === String(contractId || '')
)
);
this.dialogForm.carrierContractId =
this.dialogForm.carrierType === '承运商' && contract?.id ? contract.id : '';
this.dialogForm.carrierName = contract?.carrierName || '';
},
handleDriverChange(value) {
const driver = this.driverOptions.find(item => (item.driverName || item.name) === value);
if (driver && !this.dialogForm.driverPhone) {
this.dialogForm.driverPhone = driver.mobile || driver.driverPhone || driver.phone || '';
}
if (String(driver?.drivingVehicle || '').trim()) {
this.dialogForm.vehicleNo = String(driver.drivingVehicle).trim();
}
},
fetchDriverSuggestions(queryString, callback) {
this.loadDriverOptions(String(queryString || '').trim())
.then(() =>
callback(
this.driverOptions.map(item => ({
...item,
value: item.driverName || item.name || '',
}))
)
)
.catch(() => callback([]));
},
handleDriverSuggestionSelect(target, item = {}) {
const value = item.driverName || item.name || '';
if (target === 'dialog') {
this.dialogForm.driverName = value;
const phone = item.mobile || item.driverPhone || item.phone || '';
if (phone) this.dialogForm.driverPhone = phone;
const drivingVehicle = String(item.drivingVehicle || '').trim();
if (drivingVehicle) this.dialogForm.vehicleNo = drivingVehicle;
}
},
fetchEscortSuggestions(queryString, callback) {
const keyword = String(queryString || '').trim();
this.escortLoading = true;
getDriverList(1, 20, { ...(keyword ? { driverName: keyword } : {}), posts: '押运员' })
.then(res => {
callback(
unwrapRecords(res).map(item => ({
...item,
value: item.driverName || item.name || '',
}))
);
})
.catch(() => callback([]))
.finally(() => {
this.escortLoading = false;
});
},
handleEscortSuggestionSelect(target, item = {}) {
if (target !== 'dialog') return;
this.dialogForm.escortName = item.driverName || item.name || '';
const phone = item.mobile || item.driverPhone || item.phone || '';
if (phone) this.dialogForm.escortPhone = phone;
},
handleMileageInput(value) {
this.dialogForm.mileage = String(value || '').replace(/[^\d.]/g, '');
},
transportTypeLabel(value) {
if (!value) return '-';
return (
this.transportTypeOptions.find(item => String(item.value) === String(value))?.label || value
);
},
async loadTransportTypeOptions() {
if (this.transportTypeOptions.length) return;
const res = await request({
url: '/blade-system/dict-biz/dictionary',
method: 'get',
params: { code: 'transport_type' },
});
const records = unwrapRecords(res);
this.transportTypeOptions = records.map(item => ({
label: item.dictValue || item.label || item.name,
value: item.dictKey || item.value || item.dictValue || item.name,
}));
},
async loadProjectOptions(keyword = '') {
this.projectLoading = true;
try {
this.projectOptions = unwrapRecords(await getProjectList(1, 20, { projectName: keyword }));
} finally {
this.projectLoading = false;
}
},
async loadCustomerOptions(keyword = '') {
this.customerLoading = true;
try {
this.customerOptions = unwrapRecords(
await getCustomerList(1, 20, { customerName: keyword, customerType: '承运商' })
);
} finally {
this.customerLoading = false;
}
},
async loadCargoTypeOptions(keyword = '') {
this.cargoTypeLoading = true;
try {
this.cargoTypeOptions = unwrapRecords(await getCargoTypeOptions(keyword));
} finally {
this.cargoTypeLoading = false;
}
},
async loadDriverOptions(keyword = '') {
this.driverLoading = true;
try {
this.driverOptions = unwrapRecords(
await getDriverList(1, 20, { driverName: keyword, posts: '司机' })
);
} finally {
this.driverLoading = false;
}
},
async loadCarrierOptions(keyword = '') {
this.carrierLoading = true;
try {
this.carrierOptions = unwrapRecords(
await getCustomerList(1, 20, {
customerName: keyword,
customerType: '承运商',
status: 1,
})
);
} finally {
this.carrierLoading = false;
}
},
async loadTaskCarrierOptions() {
const requestId = ++this.taskCarrierRequestId;
if (!this.selectedWaybillRows.length) {
this.taskCarrierOptions = [];
this.taskCarrierLoading = false;
this.dialogForm.carrierContractId = '';
this.dialogForm.carrierName = '';
return [];
}
this.taskCarrierLoading = true;
try {
const projectIds = [
...new Set(this.selectedWaybillRows.map(row => row.projectId).filter(Boolean)),
];
const response = await loadingApi.getCarrierContracts(projectIds);
if (requestId !== this.taskCarrierRequestId) return [];
const contracts = response.data?.data || response.data || [];
if (this.dialogForm.carrierType !== '自运') {
this.taskCarrierOptions = contracts
.map(item => ({
...item,
carrierName:
item.partyB ||
item.partyBName ||
item.contractPartyB ||
item.customerContractPartyB ||
item.carrierName ||
'',
carrierId:
item.partyBId ||
item.partyBUserId ||
item.contractPartyBId ||
item.customerContractPartyBId ||
item.carrierId ||
'',
}))
.filter(item => item.carrierName);
const current =
contracts.find(item => String(item.id) === String(this.dialogForm.carrierContractId)) ||
this.taskCarrierOptions.find(item => item.carrierName === this.dialogForm.carrierName);
if (current) {
this.dialogForm.carrierContractId = current.id;
this.dialogForm.carrierName = current.carrierName;
this.dialogForm.carrierId = current.carrierId || '';
} else {
this.dialogForm.carrierContractId = '';
this.dialogForm.carrierName = '';
}
return this.taskCarrierOptions;
}
if (this.dialogForm.carrierType === '自运' && contracts.length) {
this.taskCarrierOptions = contracts;
const current = contracts.find(item => item.carrierName === this.dialogForm.carrierName);
this.dialogForm.carrierContractId = '';
this.dialogForm.carrierName = current?.carrierName || contracts[0].carrierName || '';
return contracts;
}
if (this.dialogForm.carrierType !== '自运') {
this.taskCarrierOptions = [];
this.dialogForm.carrierContractId = '';
return [];
}
const customerContracts = await Promise.all(
projectIds.map(projectId =>
getContractList(1, 9999, { projectId, contractCategory: '客户合同' })
.then(res => extractRecords(res))
.catch(() => [])
)
);
const partyBNames = [];
const customerContractMap = new Map(
projectIds.map((projectId, index) => [String(projectId), customerContracts[index] || []])
);
this.selectedWaybillRows.forEach(row => {
const direct = row.partyB || row.contractPartyB || row.customerContractPartyB;
const contractsForProject = customerContractMap.get(String(row.projectId)) || [];
const matched = contractsForProject.find(
item => String(item.id) === String(row.contractId)
);
const name = String(direct || matched?.partyB || '').trim();
if (name && !partyBNames.includes(name)) partyBNames.push(name);
});
this.taskCarrierOptions = partyBNames.map(name => ({
id: `customer-contract-${name}`,
carrierName: name,
partyB: name,
}));
this.dialogForm.carrierContractId = '';
this.dialogForm.carrierName = partyBNames[0] || '';
return this.taskCarrierOptions;
} catch (error) {
if (requestId !== this.taskCarrierRequestId) return [];
this.taskCarrierOptions = [];
this.dialogForm.carrierContractId = '';
if (this.dialogForm.carrierType !== '自运') this.dialogForm.carrierName = '';
return [];
} finally {
if (requestId === this.taskCarrierRequestId) this.taskCarrierLoading = false;
}
},
async loadPlanOptions(keyword = '') {
this.planLoading = true;
try {
this.planOptions = unwrapRecords(await getPlanList(1, 20, { planName: keyword }));
} finally {
this.planLoading = false;
}
},
},
};
</script>
<style lang="scss" scoped>
.loading-manage-page {
margin-bottom: 60px;
.loading-manage-page__search {
padding: 12px 12px 4px;
margin-bottom: 20px;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.loading-manage-page__search-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px 24px;
}
.loading-manage-page__search-actions {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 8px;
min-height: 32px;
}
.loading-manage-page__toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin: 12px 0;
}
.loading-manage-page__toolbar-left,
.loading-manage-page__toolbar-right {
display: flex;
align-items: center;
gap: 12px;
}
.loading-manage-page__table {
width: 100%;
}
.loading-manage-page__link {
padding: 0;
min-height: auto;
}
.loading-manage-page__sub-nos {
display: block;
line-height: 20px;
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
}
.loading-manage-page__sub-no-link {
display: inline;
white-space: normal;
line-height: 20px;
text-align: left;
}
.loading-manage-page__address-cell {
line-height: 20px;
white-space: pre-line;
word-break: break-word;
}
.loading-manage-page__status {
color: #303133;
}
.loading-manage-page__status.is-running {
color: #1dbf00;
}
.loading-manage-page__status.is-cancelled {
color: #909399;
}
.loading-manage-page__actions {
display: flex;
flex-wrap: wrap;
gap: 2px 12px;
}
.loading-manage-page__actions .el-button {
margin-left: 0;
padding: 0;
}
.loading-manage-page__pagination {
display: flex;
justify-content: flex-end;
padding: 12px 0 0;
background: #fff;
}
:deep(.el-form-item) {
margin-bottom: 8px;
}
:deep(.el-form-item__label) {
white-space: nowrap;
}
:deep(.el-input),
:deep(.el-select),
:deep(.el-date-editor.el-input),
:deep(.el-date-editor.el-input__wrapper),
:deep(.el-date-editor--daterange) {
width: 100%;
}
:deep(.el-table) {
--el-table-border-color: #eff1f7;
--el-table-row-hover-bg-color: #f5f7fa;
}
:deep(.el-table__body tr:nth-child(even) > td) {
background: #fafafa;
}
}
.loading-manage-dialog {
min-height: 12vh;
.loading-manage-dialog__body {
max-height: 78vh;
overflow-y: auto;
padding-right: 4px;
}
.loading-manage-dialog__grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0 24px;
}
.loading-manage-dialog__span-2 {
grid-column: 1 / -1;
}
.loading-manage-dialog__title-action {
display: flex;
align-items: center;
justify-content: space-between;
}
.loading-manage-dialog__candidate-search {
padding: 0;
margin-bottom: 0;
}
.loading-manage-dialog__waybill-route-layout {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
align-items: start;
//margin-bottom: 12px;
.section-card {
min-height: 257px;
}
}
.loading-manage-dialog__candidate-actions,
.loading-manage-dialog__collapsed-search,
.loading-manage-dialog__table-toolbar,
.loading-manage-dialog__footer {
display: flex;
align-items: center;
justify-content: flex-end;
}
.loading-manage-dialog__table-toolbar {
justify-content: space-between;
margin: 10px 0;
}
.loading-manage-dialog__route-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.loading-manage-dialog__cargo-section {
margin-top: 16px;
}
@media (max-width: 1200px) {
.loading-manage-dialog__grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.loading-manage-dialog__waybill-route-layout {
grid-template-columns: 1fr;
}
}
.loading-manage-dialog__route-item {
display: grid;
grid-template-columns: 28px 36px minmax(0, 1fr) auto;
align-items: center;
gap: 10px;
min-height: 44px;
padding: 8px 12px;
border: 1px solid #eff1f7;
background: #fff;
cursor: move;
}
.loading-manage-dialog__route-handle {
color: #909399;
}
.loading-manage-dialog__route-type {
width: 28px;
height: 28px;
line-height: 28px;
text-align: center;
border-radius: 50%;
color: #fff;
&--start {
background: #409eff;
}
&--middle {
background: #e6a23c;
}
&--end {
background: #67c23a;
}
}
.loading-manage-dialog__route-address {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.loading-manage-dialog__route-tags {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 6px;
}
.loading-manage-dialog__carrier-tabs {
margin-bottom: 12px;
}
.loading-manage-dialog__task-grid {
:deep(.el-form-item__label) {
flex: 0 0 100px;
width: 100px;
}
:deep(.el-form-item__content),
:deep(.el-input),
:deep(.el-select),
:deep(.el-autocomplete),
:deep(.el-date-editor.el-input),
:deep(.el-date-editor.el-input__wrapper) {
min-width: 0;
width: 100%;
}
}
:deep(.dialog-section-title) {
display: flex;
align-items: center;
margin: 16px 0 12px;
font-weight: 600;
color: #303133;
}
:deep(.dialog-section-title)::before {
content: '';
width: 4px;
height: 16px;
margin-right: 8px;
background: #409eff;
}
:deep(.el-form-item) {
margin-bottom: 14px;
}
:deep(.el-form-item__label) {
text-align: right;
white-space: nowrap;
}
:deep(.el-input),
:deep(.el-select),
:deep(.el-date-editor.el-input),
:deep(.el-date-editor.el-input__wrapper) {
width: 100%;
}
:deep(.el-table) {
--el-table-border-color: #eff1f7;
}
}
.loading-detail {
display: flex;
flex-direction: column;
//gap: 8px;
color: #303133;
.loading-detail__summary {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 24px;
padding: 4px 0 16px;
}
.loading-detail__heading {
font-size: 20px;
font-weight: 600;
margin-right: 8px;
}
.loading-detail__route-title {
display: inline-flex;
align-items: center;
gap: 16px;
}
.loading-detail__label {
color: #909399;
margin-right: 10px;
}
.loading-detail__task-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
padding-bottom: 4px;
}
.loading-detail__field {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.loading-detail__field .loading-detail__label {
margin-right: 0;
}
.loading-detail__value {
display: flex;
flex-wrap: wrap;
gap: 8px;
text-align: left;
}
.loading-detail__steps {
display: flex;
align-items: flex-start;
overflow-x: auto;
padding: 8px 4px 16px;
}
.loading-detail__step {
position: relative;
min-width: 190px;
flex: 1;
text-align: center;
}
.loading-detail__step:not(:last-child)::after {
position: absolute;
z-index: 0;
top: 16px;
left: calc(50% + 25px);
width: calc(100% - 50px);
height: 6px;
content: '';
background: #e4e7ed;
}
.loading-detail__step-line {
display: flex;
align-items: center;
justify-content: center;
}
.loading-detail__step-dot {
position: relative;
z-index: 1;
display: inline-flex;
width: 34px;
height: 34px;
align-items: center;
justify-content: center;
color: #fff;
border-radius: 4px;
font-weight: 600;
}
.loading-detail__step-dot.start {
background: #409eff;
}
.loading-detail__step-dot.middle {
background: #67c23a;
}
.loading-detail__step-dot.end {
background: #e6a23c;
}
.loading-detail__step-address {
margin-top: 8px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.loading-detail__step-tags {
display: flex;
justify-content: center;
gap: 4px;
margin-top: 6px;
}
.loading-detail__bottom-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.loading-detail__track-actions {
display: flex;
gap: 8px;
}
.loading-detail__process-actions {
display: flex;
align-items: center;
gap: 8px;
}
.loading-detail__process-actions .el-select {
width: 180px;
}
.loading-detail__map {
width: 100%;
min-height: 260px;
border: 1px solid #eff1f7;
}
}
.loading-route-change-dialog {
.loading-route-change-dialog__original {
color: #a8abb2;
line-height: 20px;
margin-bottom: 4px;
}
.loading-route-change-dialog__location {
cursor: pointer;
color: #909399;
}
.loading-route-change-dialog__hint {
color: #909399;
}
.loading-route-change-dialog__route-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.loading-route-change-dialog__route-item {
display: grid;
grid-template-columns: 34px minmax(0, 1fr) auto 24px;
align-items: center;
gap: 12px;
padding: 10px 12px;
background: #f5f6f7;
cursor: move;
}
.loading-route-change-dialog__route-type {
width: 30px;
height: 30px;
border-radius: 50%;
text-align: center;
line-height: 30px;
color: #fff;
&--start {
background: #409eff;
}
&--middle {
background: #e6a23c;
}
&--end {
background: #67c23a;
}
}
.loading-route-change-dialog__tags {
display: flex;
gap: 4px;
}
.loading-route-change-dialog__record-content {
white-space: normal;
overflow-wrap: anywhere;
line-height: 22px;
}
}
.loading-common-address-dialog__toolbar {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.loading-common-address-dialog__toolbar .el-input {
flex: 1;
}
:global(.loading-manage-form-page) {
width: 100%;
min-height: 100%;
box-sizing: border-box;
background: #f5f6fa;
}
/* 独立页面(新增/编辑/重新派单/详情):内容随 #avue-view 自然滚动,不做内嵌 78vh 滚动盒。
此处权重必须高于 .loading-manage-dialog .loading-manage-dialog__body,故带上 .loading-manage-page 外壳。 */
:global(.loading-manage-page .loading-manage-form-dialog.loading-manage-form-page .loading-manage-dialog__body) {
max-height: none;
overflow: visible;
padding-right: 0;
}
:global(.loading-manage-form-dialog) {
width: 100% !important;
min-height: 100%;
margin: 0 !important;
border-radius: 0;
box-shadow: none;
}
:global(.loading-manage-form-dialog .el-dialog__body) {
max-height: none;
padding: 20px 24px 96px;
background: #f5f6fa;
}
:global(.el-overlay:has(.loading-manage-form-dialog)) {
position: fixed !important;
inset: 92px 0 0 230px !important;
z-index: 100 !important;
overflow: auto;
background: transparent;
}
:global(.el-overlay:has(.loading-manage-form-dialog) .el-overlay-dialog) {
min-height: 100%;
overflow: visible;
}
:global(.loading-manage-form-dialog .el-dialog__header) {
padding: 18px 24px;
margin-right: 0;
background: #fff;
border-bottom: 1px solid #eff1f7;
}
:global(.loading-manage-form-dialog .el-dialog__footer) {
position: fixed;
right: 0;
bottom: 0;
left: 230px;
z-index: 1001;
box-sizing: border-box;
padding: 12px 24px;
background: #fff;
border-top: 1px solid #eff1f7;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
}
:global(.loading-manage-form-page .loading-manage-dialog__footer) {
position: fixed;
right: 0;
bottom: 0;
left: 230px;
z-index: 1001;
box-sizing: border-box;
min-height: 64px;
padding: 12px 24px;
background: #fff;
border-top: 1px solid #eff1f7;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
}
:global(.avue--collapse .el-overlay:has(.loading-manage-form-dialog)) {
left: 60px !important;
}
:global(.avue-layout--horizontal .el-overlay:has(.loading-manage-form-dialog)) {
top: 92px !important;
left: 0 !important;
}
:global(.avue--collapse .loading-manage-form-dialog .el-dialog__footer) {
left: 60px;
}
:global(.avue-layout--horizontal .loading-manage-form-dialog .el-dialog__footer) {
left: 0;
}
:global(.avue--collapse .loading-manage-form-page .loading-manage-dialog__footer) {
left: 60px;
}
:global(.avue-layout--horizontal .loading-manage-form-page .loading-manage-dialog__footer) {
left: 0;
}
@media (max-width: 900px) {
.loading-manage-dialog__grid {
grid-template-columns: 1fr;
}
.loading-detail__task-row,
.loading-detail__bottom-grid {
grid-template-columns: 1fr;
}
}
</style>