cba2ad3b3b
2、调整运单
3719 lines
131 KiB
Vue
3719 lines
131 KiB
Vue
<template>
|
||
<basic-container class="loading-manage-page">
|
||
<div v-if="!isStandaloneFormPage" 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="!isStandaloneFormPage" 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="!isStandaloneFormPage"
|
||
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="openLoadingDialog('view', 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="action.type === 'delete' ? 'danger' : 'primary'"
|
||
@click="handleRowAction(action.type, row)"
|
||
>{{ action.label }}</el-link
|
||
>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<div v-if="!isStandaloneFormPage" 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>
|
||
|
||
<component
|
||
:is="isStandaloneFormPage ? 'div' : 'el-dialog'"
|
||
:model-value="isStandaloneFormPage ? undefined : dialogVisible"
|
||
:title="isStandaloneFormPage ? undefined : dialogTitle"
|
||
:append-to-body="isStandaloneFormPage ? undefined : true"
|
||
:destroy-on-close="isStandaloneFormPage ? undefined : true"
|
||
:width="isStandaloneFormPage ? undefined : '96%'"
|
||
:top="isStandaloneFormPage ? undefined : '3vh'"
|
||
:class="[
|
||
'loading-manage-dialog',
|
||
{ 'loading-manage-form-dialog': isStandaloneFormPage },
|
||
{ 'loading-manage-form-page': isStandaloneFormPage },
|
||
]"
|
||
:modal="isStandaloneFormPage ? undefined : true"
|
||
:show-close="isStandaloneFormPage ? undefined : true"
|
||
:close-on-click-modal="isStandaloneFormPage ? undefined : true"
|
||
:close-on-press-escape="isStandaloneFormPage ? undefined : true"
|
||
@update:model-value="value => !isStandaloneFormPage && (dialogVisible = value)"
|
||
@closed="resetLoadingDialog"
|
||
>
|
||
<div v-if="isStandaloneFormPage" class="archive-page-form__title">{{ 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"
|
||
>
|
||
<section-card title="筛选待配载运单">
|
||
<template #extra>
|
||
<el-link v-if="!dialogReadonly" type="primary" @click="openCandidateSearch"
|
||
>筛选</el-link
|
||
>
|
||
</template>
|
||
<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>
|
||
</section-card>
|
||
|
||
<div title="配载信息" class="loading-manage-dialog__load-info-card">
|
||
<section-card title="待配载运单">
|
||
<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 }">
|
||
{{ formatCandidateWaybillValue(row, 'goodsInfo') }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="departureAddress"
|
||
label="发货地址"
|
||
min-width="220"
|
||
show-overflow-tooltip
|
||
>
|
||
<template #default="{ row }">
|
||
{{ formatCandidateAddress(row, 'departure') }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="arrivalAddress"
|
||
label="收货地址"
|
||
min-width="220"
|
||
show-overflow-tooltip
|
||
>
|
||
<template #default="{ row }">
|
||
{{ formatCandidateAddress(row, 'arrival') }}
|
||
</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"
|
||
show-overflow-tooltip
|
||
>
|
||
<template #default="{ row }">
|
||
{{ formatWaybillAddress(row, 'departure') }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="arrivalAddress"
|
||
label="到货地"
|
||
min-width="220"
|
||
show-overflow-tooltip
|
||
>
|
||
<template #default="{ row }">
|
||
{{ formatWaybillAddress(row, 'arrival') }}
|
||
</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 v-if="isStandaloneFormPage" 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>
|
||
<template v-if="!isStandaloneFormPage" #footer>
|
||
<div class="loading-manage-dialog__footer">
|
||
<template v-if="dialogReadonly">
|
||
<el-button @click="closeLoadingDialog">关闭</el-button>
|
||
</template>
|
||
<template v-else>
|
||
<el-button @click="clearLoadingDialog">清空</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>
|
||
</template>
|
||
</component>
|
||
<el-dialog
|
||
v-model="routeChangeVisible"
|
||
title="变更运输路线"
|
||
append-to-body
|
||
destroy-on-close
|
||
width="78%"
|
||
class="loading-route-change-dialog"
|
||
>
|
||
<el-tabs 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: [],
|
||
dialogVisible: false,
|
||
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 '请选择';
|
||
},
|
||
dialogTitle() {
|
||
const titleMap = {
|
||
add: '新建配载',
|
||
edit: '编辑配载单',
|
||
view: '配载单详情',
|
||
copy: '复制配载单',
|
||
reassign: '重新派单',
|
||
};
|
||
return titleMap[this.dialogMode] || '配载管理';
|
||
},
|
||
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,
|
||
}
|
||
);
|
||
},
|
||
isStandaloneFormPage() {
|
||
return (
|
||
this.$route.path === '/business/loading-manage/form' &&
|
||
['add', 'edit'].includes(this.$route.query.mode)
|
||
);
|
||
},
|
||
formPageTitle() {
|
||
return (
|
||
this.$route.query.name || `${this.$route.query.mode === 'edit' ? '编辑' : '新增'}配载管理`
|
||
);
|
||
},
|
||
},
|
||
mounted() {
|
||
this.loadTransportTypeOptions();
|
||
this.loadTable();
|
||
if (this.isStandaloneFormPage) {
|
||
this.syncStandaloneTagTitle();
|
||
this.initStandaloneFormPage();
|
||
}
|
||
const detailId = this.$route.query.detailId;
|
||
if (detailId) this.openLoadingDialog('view', { id: detailId });
|
||
},
|
||
watch: {
|
||
$route(to, from) {
|
||
if (this.isStandaloneFormPage) {
|
||
this.syncStandaloneTagTitle();
|
||
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();
|
||
}
|
||
} else if (from?.query?.mode && this.dialogVisible) {
|
||
this.closeLoadingDialog();
|
||
}
|
||
},
|
||
'$route.query.detailId'(detailId) {
|
||
if (detailId) this.openLoadingDialog('view', { id: detailId });
|
||
},
|
||
},
|
||
methods: {
|
||
syncStandaloneTagTitle() {
|
||
if (!this.isStandaloneFormPage) return;
|
||
const title = this.$route.query.mode === 'edit' ? '编辑配载管理' : '新增配载管理';
|
||
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);
|
||
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;
|
||
},
|
||
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 value = row?.[`${type}Address`] || row?.[`${type}Name`] || '';
|
||
return this.formatProvinceCityDistrict(value) || '-';
|
||
},
|
||
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: 'copy', label: '复制' },
|
||
{ type: 'delete', label: '删除' },
|
||
];
|
||
}
|
||
if (status === 'pending') {
|
||
return [
|
||
{ type: 'view', label: '详情' },
|
||
{ type: 'reassign', label: '重新派单' },
|
||
{ type: 'edit', label: '编辑' },
|
||
{ type: 'copy', label: '复制' },
|
||
{ type: 'cancel', label: '取消' },
|
||
];
|
||
}
|
||
if (status === 'running') {
|
||
return [
|
||
{ type: 'view', label: '详情' },
|
||
{ type: 'copy', label: '复制' },
|
||
{ type: 'complete', label: '完成' },
|
||
];
|
||
}
|
||
if (status === 'completed' || status === 'cancelled') {
|
||
return [
|
||
{ type: 'view', label: '详情' },
|
||
{ type: 'copy', label: '复制' },
|
||
];
|
||
}
|
||
return [{ type: 'view', label: '详情' }];
|
||
},
|
||
handleRowAction(type, row) {
|
||
const actionMap = {
|
||
view: () => this.openLoadingDialog('view', row),
|
||
edit: () => this.openLoadingDialog('edit', row),
|
||
reassign: () => this.openLoadingDialog('reassign', row),
|
||
copy: () => this.copyLoading(row),
|
||
delete: () => this.deleteLoading(row),
|
||
cancel: () => this.cancelLoading(row),
|
||
complete: () => this.completeLoading(row),
|
||
};
|
||
actionMap[type]?.();
|
||
},
|
||
async initStandaloneFormPage() {
|
||
if (!this.isStandaloneFormPage) return;
|
||
const mode = this.$route.query.mode === 'edit' ? 'edit' : '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'].includes(mode) && !this.isStandaloneFormPage) {
|
||
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.dialogVisible = true;
|
||
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.isStandaloneFormPage) {
|
||
this.$router.$avueRouter?.closeTag?.();
|
||
this.$router.push({ path: '/business/loading-manage', query: {} });
|
||
return;
|
||
}
|
||
this.dialogVisible = false;
|
||
},
|
||
resetLoadingDialog() {
|
||
this.resetDialogData();
|
||
this.dialogSaving = '';
|
||
this.dialogLoading = false;
|
||
},
|
||
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 = [];
|
||
},
|
||
clearLoadingDialog() {
|
||
this.resetDialogData();
|
||
if (this.dialogMode === 'add') this.candidateQuery.transportType = 'road';
|
||
this.loadCandidateWaybills();
|
||
},
|
||
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 || '';
|
||
},
|
||
openCandidateSearch() {
|
||
this.candidateSearchVisible = true;
|
||
this.loadCandidateWaybills();
|
||
},
|
||
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.formatWaybillAddress(row, 'departure'), `装${rowIndex + 1}`);
|
||
});
|
||
rows.forEach((row, rowIndex) => {
|
||
const arrival = row.arrivalAddress || row.arrivalName;
|
||
appendNode(arrival, this.formatWaybillAddress(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 = '';
|
||
}
|
||
},
|
||
copyLoading(row) {
|
||
this.$router.push({
|
||
path: '/business/loading-manage/form',
|
||
query: {
|
||
mode: 'add',
|
||
copyId: row.id,
|
||
name: '新增配载管理',
|
||
},
|
||
});
|
||
},
|
||
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(() => {});
|
||
},
|
||
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: 4px 0;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.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;
|
||
gap: 8px;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
:global(.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>
|