5717 lines
207 KiB
Vue
5717 lines
207 KiB
Vue
<template>
|
||
<basic-container class="loading-manage-page">
|
||
<div v-if="!isStandalonePage && searchVisible" class="loading-manage-page__search">
|
||
<el-form :model="searchForm" label-position="right" label-width="88px">
|
||
<div class="loading-manage-page__search-grid">
|
||
<el-form-item label="配载单号">
|
||
<el-input v-model="searchForm.loadingNo" clearable placeholder="请输入" />
|
||
</el-form-item>
|
||
<el-form-item label="项目名称">
|
||
<el-select
|
||
v-model="searchForm.projectName"
|
||
clearable
|
||
filterable
|
||
remote
|
||
reserve-keyword
|
||
:remote-method="loadProjectOptions"
|
||
:loading="projectLoading"
|
||
placeholder="请输入"
|
||
@visible-change="visible => visible && loadProjectOptions(searchForm.projectName)"
|
||
>
|
||
<el-option
|
||
v-for="item in projectOptions"
|
||
:key="item.id || item.projectName"
|
||
:label="item.projectName"
|
||
:value="item.projectName"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="客户名称">
|
||
<el-select
|
||
v-model="searchForm.customerName"
|
||
clearable
|
||
filterable
|
||
remote
|
||
reserve-keyword
|
||
:remote-method="loadCustomerOptions"
|
||
:loading="customerLoading"
|
||
placeholder="请输入"
|
||
@visible-change="visible => visible && loadCustomerOptions(searchForm.customerName)"
|
||
>
|
||
<el-option
|
||
v-for="item in customerOptions"
|
||
:key="item.id || item.customerName"
|
||
:label="item.customerName"
|
||
:value="item.customerName"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="运输方式">
|
||
<el-select
|
||
v-model="searchForm.transportType"
|
||
clearable
|
||
filterable
|
||
placeholder="请选择"
|
||
@visible-change="visible => visible && loadTransportTypeOptions()"
|
||
>
|
||
<el-option
|
||
v-for="item in transportTypeOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
|
||
<template v-if="searchExpanded">
|
||
<el-form-item label="运单号">
|
||
<el-input v-model="searchForm.waybillNo" clearable placeholder="请输入" />
|
||
</el-form-item>
|
||
<el-form-item label="货物类型">
|
||
<el-select
|
||
v-model="searchForm.cargoType"
|
||
clearable
|
||
filterable
|
||
remote
|
||
reserve-keyword
|
||
:remote-method="loadCargoTypeOptions"
|
||
:loading="cargoTypeLoading"
|
||
placeholder="请输入"
|
||
@visible-change="visible => visible && loadCargoTypeOptions(searchForm.cargoType)"
|
||
>
|
||
<el-option
|
||
v-for="item in cargoTypeOptions"
|
||
:key="item.id || item.cargoName || item.cargoType"
|
||
:label="item.cargoName || item.cargoType || item.label"
|
||
:value="item.cargoName || item.cargoType || item.label"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="发货地址">
|
||
<el-input v-model="searchForm.departureAddress" clearable placeholder="请输入" />
|
||
</el-form-item>
|
||
<el-form-item label="收货地址">
|
||
<el-input v-model="searchForm.arrivalAddress" clearable placeholder="请输入" />
|
||
</el-form-item>
|
||
|
||
<el-form-item label="货物名称">
|
||
<el-input v-model="searchForm.cargoName" clearable placeholder="请输入" />
|
||
</el-form-item>
|
||
<el-form-item label="司机名称">
|
||
<el-select
|
||
v-model="searchForm.driverName"
|
||
clearable
|
||
filterable
|
||
remote
|
||
reserve-keyword
|
||
:remote-method="loadDriverOptions"
|
||
:loading="driverLoading"
|
||
placeholder="请输入"
|
||
@visible-change="visible => visible && loadDriverOptions(searchForm.driverName)"
|
||
>
|
||
<el-option
|
||
v-for="item in driverOptions"
|
||
:key="item.id || item.driverName || item.name"
|
||
:label="item.driverName || item.name"
|
||
:value="item.driverName || item.name"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="车牌号/船班号/船次号/班次号">
|
||
<el-input v-model="searchForm.vehicleNo" clearable placeholder="请输入" />
|
||
</el-form-item>
|
||
<el-form-item label="原始单号">
|
||
<el-input v-model="searchForm.originalNo" clearable placeholder="请输入" />
|
||
</el-form-item>
|
||
|
||
<el-form-item label="承运商名称">
|
||
<el-select
|
||
v-model="searchForm.carrierName"
|
||
clearable
|
||
filterable
|
||
remote
|
||
reserve-keyword
|
||
:remote-method="loadCarrierOptions"
|
||
:loading="carrierLoading"
|
||
placeholder="请输入"
|
||
@visible-change="visible => visible && loadCarrierOptions(searchForm.carrierName)"
|
||
>
|
||
<el-option
|
||
v-for="item in carrierOptions"
|
||
:key="item.id || item.fullName || item.customerName || item.carrierName"
|
||
:label="item.fullName || item.customerName || item.carrierName || item.name"
|
||
:value="item.fullName || item.customerName || item.carrierName || item.name"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="数据来源">
|
||
<el-select v-model="searchForm.dataSource" clearable placeholder="请选择">
|
||
<el-option
|
||
v-for="item in dataSourceOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="开始日期">
|
||
<el-date-picker
|
||
v-model="searchForm.startDateRange"
|
||
type="daterange"
|
||
format="YYYY-MM-DD"
|
||
value-format="YYYY-MM-DD"
|
||
start-placeholder="请选择"
|
||
end-placeholder="请选择"
|
||
range-separator="-"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="结束日期">
|
||
<el-date-picker
|
||
v-model="searchForm.endDateRange"
|
||
type="daterange"
|
||
format="YYYY-MM-DD"
|
||
value-format="YYYY-MM-DD"
|
||
start-placeholder="请选择"
|
||
end-placeholder="请选择"
|
||
range-separator="-"
|
||
/>
|
||
</el-form-item>
|
||
</template>
|
||
|
||
<el-form-item label="状态">
|
||
<el-select v-model="searchForm.businessStatus" clearable placeholder="请选择">
|
||
<el-option
|
||
v-for="item in statusOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="运单批次号">
|
||
<el-input v-model="searchForm.batchNo" clearable placeholder="请输入" />
|
||
</el-form-item>
|
||
<el-form-item label="计划名称">
|
||
<el-select
|
||
v-model="searchForm.planName"
|
||
clearable
|
||
filterable
|
||
remote
|
||
reserve-keyword
|
||
:remote-method="loadPlanOptions"
|
||
:loading="planLoading"
|
||
placeholder="请输入"
|
||
@visible-change="visible => visible && loadPlanOptions(searchForm.planName)"
|
||
>
|
||
<el-option
|
||
v-for="item in planOptions"
|
||
:key="item.id || item.planName"
|
||
:label="item.planName"
|
||
:value="item.planName"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<div class="loading-manage-page__search-actions">
|
||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||
<el-button @click="handleResetSearch">重置</el-button>
|
||
<el-link type="primary" @click="searchExpanded = !searchExpanded">{{
|
||
searchExpanded ? '收起' : '展开'
|
||
}}</el-link>
|
||
</div>
|
||
</div>
|
||
</el-form>
|
||
</div>
|
||
|
||
<div v-if="!isStandalonePage" class="loading-manage-page__toolbar">
|
||
<div class="loading-manage-page__toolbar-left">
|
||
<el-button type="primary" @click="openLoadingDialog('add')">新建配载</el-button>
|
||
<el-button type="primary" plain @click="handleExport">批量导出</el-button>
|
||
<el-button type="primary" plain @click="handleBatchComplete">批量完成</el-button>
|
||
</div>
|
||
<table-header-tools
|
||
:columns="columnSettingOptions"
|
||
v-model:visible-column-keys="visibleColumnKeys"
|
||
v-model:table-size="tableSize"
|
||
v-model:search-visible="searchVisible"
|
||
@refresh="loadTable"
|
||
/>
|
||
</div>
|
||
|
||
<el-table
|
||
v-if="!isStandalonePage"
|
||
ref="tableRef"
|
||
v-loading="loading"
|
||
:data="tableData"
|
||
:size="tableSize"
|
||
border
|
||
row-key="id"
|
||
class="loading-manage-page__table"
|
||
@selection-change="selectionChange"
|
||
>
|
||
<el-table-column type="selection" width="52" fixed="left" align="center" />
|
||
<el-table-column type="index" label="序号" width="58" fixed="left" align="center" />
|
||
<el-table-column
|
||
v-if="isColumnVisible('loadingNo')"
|
||
label="配载单号"
|
||
prop="loadingNo"
|
||
min-width="170"
|
||
>
|
||
<template #default="{ row }">
|
||
<el-link
|
||
type="primary"
|
||
class="loading-manage-page__link"
|
||
@click="gotoLoadingDetail(row)"
|
||
>{{ row.loadingNo }}</el-link
|
||
>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
v-if="isColumnVisible('loadingSubNos')"
|
||
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
|
||
v-if="isColumnVisible('vehicleNo')"
|
||
label="车牌号"
|
||
prop="vehicleNo"
|
||
min-width="170"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column
|
||
v-if="isColumnVisible('driverName')"
|
||
label="司机"
|
||
prop="driverName"
|
||
min-width="120"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column
|
||
v-if="isColumnVisible('driverPhone')"
|
||
label="联系电话"
|
||
prop="driverPhone"
|
||
min-width="140"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column
|
||
v-if="isColumnVisible('carrierType')"
|
||
label="承运类型"
|
||
prop="carrierType"
|
||
min-width="120"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column
|
||
v-if="isColumnVisible('departureAddress')"
|
||
label="发货地"
|
||
prop="departureAddress"
|
||
min-width="180"
|
||
>
|
||
<template #default="{ row }">
|
||
<el-tooltip
|
||
v-if="formatFullRouteAddress(row, 'departure')"
|
||
:content="formatFullRouteAddress(row, 'departure')"
|
||
placement="top"
|
||
:show-after="120"
|
||
>
|
||
<div class="loading-manage-page__address-cell">
|
||
{{ formatRouteAddress(row, 'departure') }}
|
||
</div>
|
||
</el-tooltip>
|
||
<div v-else class="loading-manage-page__address-cell">
|
||
{{ formatRouteAddress(row, 'departure') }}
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
v-if="isColumnVisible('transitAddress')"
|
||
label="途经地"
|
||
prop="transitAddress"
|
||
min-width="190"
|
||
>
|
||
<template #default="{ row }">
|
||
<el-tooltip
|
||
v-if="formatFullRouteAddress(row, 'transit')"
|
||
:content="formatFullRouteAddress(row, 'transit')"
|
||
placement="top"
|
||
:show-after="120"
|
||
>
|
||
<div class="loading-manage-page__address-cell">
|
||
{{ formatRouteAddress(row, 'transit') }}
|
||
</div>
|
||
</el-tooltip>
|
||
<div v-else class="loading-manage-page__address-cell">
|
||
{{ formatRouteAddress(row, 'transit') }}
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
v-if="isColumnVisible('arrivalAddress')"
|
||
label="到货地"
|
||
prop="arrivalAddress"
|
||
min-width="180"
|
||
>
|
||
<template #default="{ row }">
|
||
<el-tooltip
|
||
v-if="formatFullRouteAddress(row, 'arrival')"
|
||
:content="formatFullRouteAddress(row, 'arrival')"
|
||
placement="top"
|
||
:show-after="120"
|
||
>
|
||
<div class="loading-manage-page__address-cell">
|
||
{{ formatRouteAddress(row, 'arrival') }}
|
||
</div>
|
||
</el-tooltip>
|
||
<div v-else class="loading-manage-page__address-cell">
|
||
{{ formatRouteAddress(row, 'arrival') }}
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
v-if="isColumnVisible('carrierName')"
|
||
label="承运商"
|
||
prop="carrierName"
|
||
min-width="160"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column
|
||
v-if="isColumnVisible('transportType')"
|
||
label="运输方式"
|
||
prop="transportType"
|
||
min-width="150"
|
||
show-overflow-tooltip
|
||
>
|
||
<template #default="{ row }">
|
||
{{ transportTypeLabel(row.transportType) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
v-if="isColumnVisible('dataSource')"
|
||
label="数据来源"
|
||
prop="dataSource"
|
||
min-width="130"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column
|
||
v-if="isColumnVisible('createUserName')"
|
||
label="创建人"
|
||
prop="createUserName"
|
||
min-width="120"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column
|
||
v-if="isColumnVisible('createTime')"
|
||
label="创建时间"
|
||
prop="createTime"
|
||
min-width="170"
|
||
show-overflow-tooltip
|
||
sortable
|
||
/>
|
||
<el-table-column
|
||
v-if="isColumnVisible('updateTime')"
|
||
label="更新时间"
|
||
prop="updateTime"
|
||
min-width="170"
|
||
show-overflow-tooltip
|
||
sortable
|
||
/>
|
||
<el-table-column
|
||
v-if="isColumnVisible('currentProcessNode')"
|
||
label="过程节点"
|
||
prop="currentProcessNode"
|
||
min-width="130"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column
|
||
v-if="isColumnVisible('businessStatus')"
|
||
label="状态"
|
||
prop="businessStatus"
|
||
width="110"
|
||
fixed="right"
|
||
align="center"
|
||
>
|
||
<template #default="{ row }">
|
||
<span :class="['loading-manage-page__status', `is-${row.businessStatus || 'unknown'}`]">
|
||
{{ statusText(row) }}
|
||
</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="320" fixed="right" align="left">
|
||
<template #default="{ row }">
|
||
<div class="loading-manage-page__actions">
|
||
<el-link
|
||
v-for="action in rowActions(row)"
|
||
:key="action.type"
|
||
:type="['delete', 'cancel'].includes(action.type) ? 'danger' : 'primary'"
|
||
@click="handleRowAction(action.type, row)"
|
||
>{{ action.label }}</el-link
|
||
>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<div v-if="!isStandalonePage" class="loading-manage-page__pagination">
|
||
<el-pagination
|
||
v-model:current-page="page.currentPage"
|
||
v-model:page-size="page.pageSize"
|
||
background
|
||
:page-sizes="page.pageSizes"
|
||
layout="total, sizes, prev, pager, next, jumper"
|
||
:total="page.total"
|
||
@size-change="handleSizeChange"
|
||
@current-change="handleCurrentChange"
|
||
/>
|
||
</div>
|
||
|
||
<!--
|
||
配载单的新增/编辑/重新派单/详情全部是独立整页,这里固定渲染普通容器,不再按路由切换 el-dialog。
|
||
原因:标签页 keep-alive 会把本页实例缓存下来,若容器随 $route 由 div 变回 el-dialog,
|
||
被缓存的实例会在离开本页后渲染出 append-to-body 的弹窗并永久停留在 body 上(Vue 的
|
||
KeepAlive.deactivate → Teleport.move 不会搬走 append-to-body 的弹窗 DOM),表现为“点左侧菜单又弹出弹窗”。
|
||
-->
|
||
<div
|
||
v-if="isStandalonePage"
|
||
class="loading-manage-dialog loading-manage-form-dialog loading-manage-form-page"
|
||
>
|
||
<div v-if="!isStandaloneDetailPage" 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>{{ dialogForm.loadingNo || '-' }}</div>
|
||
<el-tag :type="detailStatusType">{{ statusText(dialogForm) }}</el-tag>
|
||
<el-tag type="primary">{{ transportTypeLabel(dialogForm.transportType) }}</el-tag>
|
||
</div>
|
||
<div class="loading-detail__task-row">
|
||
<div class="loading-detail__task-main">
|
||
<span class="loading-detail__label">任务信息</span>
|
||
<div class="loading-detail__task-grid">
|
||
<div class="loading-detail__field">
|
||
<span class="loading-detail__label">承运商</span>
|
||
<span class="loading-detail__value">{{ detailTaskField('carrierName') }}</span>
|
||
</div>
|
||
<div class="loading-detail__field">
|
||
<span class="loading-detail__label">司机</span>
|
||
<span class="loading-detail__value">{{ detailTaskField('driverName') }}</span>
|
||
</div>
|
||
<div class="loading-detail__field">
|
||
<span class="loading-detail__label">车牌号</span>
|
||
<span class="loading-detail__value">{{ detailTaskField('vehicleNo') }}</span>
|
||
</div>
|
||
<div class="loading-detail__field">
|
||
<span class="loading-detail__label">手机号</span>
|
||
<span class="loading-detail__value">{{ detailTaskField('driverPhone') }}</span>
|
||
</div>
|
||
<div class="loading-detail__field">
|
||
<span class="loading-detail__label">里程</span>
|
||
<span class="loading-detail__value">{{
|
||
detailTaskMileage ? `${detailTaskMileage} km` : '-'
|
||
}}</span>
|
||
</div>
|
||
<div class="loading-detail__field">
|
||
<span class="loading-detail__label">预计发货日期</span>
|
||
<span class="loading-detail__value">{{
|
||
detailTaskDate('estimatedStartDate')
|
||
}}</span>
|
||
</div>
|
||
<div class="loading-detail__field">
|
||
<span class="loading-detail__label">预计完成日期</span>
|
||
<span class="loading-detail__value">{{
|
||
detailTaskDate('estimatedEndDate')
|
||
}}</span>
|
||
</div>
|
||
<template v-if="isSelfCarrierDetail">
|
||
<div class="loading-detail__field">
|
||
<span class="loading-detail__label">挂车车牌号</span>
|
||
<span class="loading-detail__value">{{
|
||
detailTaskField('trailerVehicleNo')
|
||
}}</span>
|
||
</div>
|
||
<div class="loading-detail__field">
|
||
<span class="loading-detail__label">押运人</span>
|
||
<span class="loading-detail__value">{{ detailTaskField('escortName') }}</span>
|
||
</div>
|
||
<div class="loading-detail__field">
|
||
<span class="loading-detail__label">押运人手机号</span>
|
||
<span class="loading-detail__value">{{ detailTaskField('escortPhone') }}</span>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
<div class="loading-detail__field loading-detail__attachment-field">
|
||
<span class="loading-detail__label">附件</span>
|
||
<span class="loading-detail__value"
|
||
><span
|
||
v-for="file in detailAttachments"
|
||
:key="file.url || file.name"
|
||
class="attachment-file-name"
|
||
role="button"
|
||
tabindex="0"
|
||
@click="previewAttachment(file)"
|
||
@keydown.enter="previewAttachment(file)"
|
||
@keydown.space.prevent="previewAttachment(file)"
|
||
>{{ file.name }}</span
|
||
><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">
|
||
<el-tooltip
|
||
v-if="formatDetailRouteTooltip(node)"
|
||
:content="formatDetailRouteTooltip(node)"
|
||
placement="top"
|
||
:show-after="120"
|
||
>
|
||
<span class="loading-detail__step-address-text">{{
|
||
formatDetailRouteShort(node)
|
||
}}</span>
|
||
</el-tooltip>
|
||
<span v-else class="loading-detail__step-address-text">{{
|
||
formatDetailRouteShort(node)
|
||
}}</span>
|
||
</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="quantity"
|
||
label="数量"
|
||
min-width="120"
|
||
><template #default="{ row }">{{
|
||
formatCargoQuantityDisplay(row)
|
||
}}</template></el-table-column
|
||
><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
|
||
v-if="row.waybillNo"
|
||
type="primary"
|
||
@click="openWaybillDetail(row)"
|
||
>{{ row.waybillNo }}</el-link
|
||
><span v-else>-</span></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="processWaybillId"
|
||
filterable
|
||
placeholder="切换运单"
|
||
@change="handleProcessWaybillChange"
|
||
>
|
||
<el-option
|
||
v-for="item in processWaybillOptions"
|
||
:key="item.id"
|
||
:label="item.waybillNo || String(item.id)"
|
||
:value="item.id"
|
||
/>
|
||
</el-select>
|
||
</div>
|
||
</template>
|
||
<el-tabs
|
||
v-model="processDetailTab"
|
||
type="border-card"
|
||
@tab-change="handleProcessDetailTabChange"
|
||
>
|
||
<el-tab-pane label="打卡详情" name="punch">
|
||
<div v-loading="punchRecordsLoading">
|
||
<el-timeline v-if="orderedPunchRecords.length">
|
||
<el-timeline-item
|
||
v-for="(record, index) in orderedPunchRecords"
|
||
:key="`${record.nodeCode || record.id || 'punch'}-${index}`"
|
||
:timestamp="record.punchTime || record.statusName || '未打卡'"
|
||
:type="record.punched ? 'primary' : 'info'"
|
||
:color="record.punched ? '#2b6ce8' : '#c0c4cc'"
|
||
placement="top"
|
||
>
|
||
<div class="loading-detail__punch-record">
|
||
<div class="loading-detail__punch-record-title">
|
||
{{ record.nodeName || record.nodeCode || '打卡' }}
|
||
<el-tag
|
||
:type="record.punched ? 'success' : 'info'"
|
||
size="small"
|
||
effect="plain"
|
||
>{{
|
||
record.statusName || (record.punched ? '已打卡' : '未打卡')
|
||
}}</el-tag
|
||
>
|
||
<el-tag
|
||
v-if="record.exceptionFlag"
|
||
size="small"
|
||
type="danger"
|
||
effect="plain"
|
||
>异常</el-tag
|
||
>
|
||
</div>
|
||
<template v-if="record.punched">
|
||
<div
|
||
v-if="record.address"
|
||
class="loading-detail__punch-record-line"
|
||
>
|
||
地点:{{ record.address }}
|
||
</div>
|
||
<div
|
||
v-if="record.weight || record.volume || record.quantity"
|
||
class="loading-detail__punch-record-line"
|
||
>
|
||
<span v-if="record.weight">重量 {{ record.weight }}吨</span>
|
||
<span v-if="record.volume">体积 {{ record.volume }}方</span>
|
||
<span v-if="record.quantity">数量 {{ record.quantity }}</span>
|
||
</div>
|
||
<div
|
||
v-if="record.remark"
|
||
class="loading-detail__punch-record-line"
|
||
>
|
||
备注:{{ record.remark }}
|
||
</div>
|
||
<punch-record-photos
|
||
v-if="record.photos && record.photos.length"
|
||
:photos="record.photos"
|
||
lazy
|
||
@preview="previewDriverUploadPhoto(record.photos, $event)"
|
||
/>
|
||
</template>
|
||
<div v-else class="loading-detail__punch-record-line">暂未打卡</div>
|
||
</div>
|
||
</el-timeline-item>
|
||
</el-timeline>
|
||
<el-empty
|
||
v-else-if="!punchRecordsLoading"
|
||
description="暂无打卡记录"
|
||
:image-size="50"
|
||
/>
|
||
</div>
|
||
</el-tab-pane>
|
||
<el-tab-pane v-if="hasRelatedVoucher" label="批量补录" name="batchSupplement">
|
||
<voucher-execution-detail
|
||
v-if="isCarrierExecutionVoucher"
|
||
:key="processWaybillId"
|
||
:waybill-id="processWaybillId"
|
||
/>
|
||
<div
|
||
v-else-if="voucherFolderView"
|
||
class="loading-detail__voucher-folder-toolbar"
|
||
>
|
||
<el-button text type="primary" @click="backToVoucherFolders">
|
||
返回文件夹
|
||
</el-button>
|
||
<span>{{ voucherFolderName(voucherFolder) }}</span>
|
||
</div>
|
||
<div
|
||
v-if="!isCarrierExecutionVoucher"
|
||
v-loading="voucherImagesLoading"
|
||
class="loading-detail__voucher-image-grid"
|
||
>
|
||
<div
|
||
v-for="(image, index) in voucherImages"
|
||
:key="
|
||
image.id ||
|
||
image.objectKey ||
|
||
(isVoucherFolder(image)
|
||
? `${image.voucherId || 'folder'}-${image.folderName || image.name || index}`
|
||
: index)
|
||
"
|
||
:class="[
|
||
'loading-detail__voucher-image-item',
|
||
{
|
||
'loading-detail__voucher-image-item--folder': isVoucherFolder(image),
|
||
},
|
||
]"
|
||
:title="
|
||
isVoucherFolder(image)
|
||
? voucherFolderName(image)
|
||
: image.imageName || image.name
|
||
"
|
||
@click="
|
||
isVoucherFolder(image)
|
||
? openVoucherFolder(image)
|
||
: previewVoucherImage(index)
|
||
"
|
||
>
|
||
<el-image
|
||
:src="
|
||
isVoucherFolder(image) ? image.icon || '/img/文件夹.png' : image.url
|
||
"
|
||
:fit="isVoucherFolder(image) ? 'contain' : 'cover'"
|
||
lazy
|
||
/>
|
||
<div
|
||
v-if="isVoucherFolder(image)"
|
||
class="loading-detail__voucher-folder-name"
|
||
>
|
||
{{ voucherFolderName(image) }}
|
||
</div>
|
||
</div>
|
||
<el-empty
|
||
v-if="!voucherImagesLoading && !voucherImages.length"
|
||
description="暂无凭证图片"
|
||
:image-size="50"
|
||
/>
|
||
</div>
|
||
</el-tab-pane>
|
||
<el-tab-pane v-if="driverUploads.length" label="司机上传" name="driverUpload">
|
||
<div
|
||
v-loading="punchRecordsLoading"
|
||
class="loading-detail__driver-upload-grid"
|
||
>
|
||
<div
|
||
v-for="(photo, index) in driverUploads"
|
||
:key="`${photo.url}-${index}`"
|
||
class="loading-detail__driver-upload-item"
|
||
:title="photo.label"
|
||
@click="previewDriverUploadPhoto(driverUploads, index)"
|
||
>
|
||
<el-image :src="photo.url" fit="cover" lazy />
|
||
<div class="loading-detail__driver-upload-label">
|
||
{{ photo.label || '凭证' }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</el-tab-pane>
|
||
</el-tabs>
|
||
</section-card>
|
||
<section-card title="物流轨迹"
|
||
><template #extra
|
||
><div class="loading-detail__track-actions">
|
||
<el-button plain @click="handleTrackAction('playback')">轨迹回放</el-button
|
||
><el-button plain @click="handleTrackAction('locate')">实时定位</el-button>
|
||
</div></template
|
||
>
|
||
<div ref="detailAmap" class="loading-detail__map"></div
|
||
></section-card>
|
||
</div>
|
||
</div>
|
||
<el-form
|
||
v-else
|
||
ref="loadingFormRef"
|
||
:model="dialogForm"
|
||
:disabled="dialogReadonly"
|
||
label-position="right"
|
||
label-width="auto"
|
||
class="loading-manage-dialog__form archive-form"
|
||
>
|
||
<div title="配载信息" class="loading-manage-dialog__load-info-card">
|
||
<section-card>
|
||
<div class="loading-manage-dialog__candidate-search">
|
||
<el-row :gutter="16">
|
||
<el-col :span="6">
|
||
<el-form-item label="运单号">
|
||
<el-input v-model="candidateQuery.waybillNo" clearable placeholder="请输入" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<el-form-item label="项目名称">
|
||
<el-select
|
||
v-model="candidateQuery.projectName"
|
||
clearable
|
||
filterable
|
||
remote
|
||
reserve-keyword
|
||
:remote-method="loadProjectOptions"
|
||
:loading="projectLoading"
|
||
placeholder="请输入"
|
||
@visible-change="
|
||
visible => visible && loadProjectOptions(candidateQuery.projectName)
|
||
"
|
||
>
|
||
<el-option
|
||
v-for="item in projectOptions"
|
||
:key="item.id || item.projectName"
|
||
:label="item.projectName"
|
||
:value="item.projectName"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<el-form-item label="发货地址">
|
||
<el-input
|
||
v-model="candidateQuery.departureAddress"
|
||
clearable
|
||
placeholder="请输入"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<template v-if="candidateSearchVisible">
|
||
<el-col :span="6">
|
||
<el-form-item label="货物名称">
|
||
<el-input
|
||
v-model="candidateQuery.cargoName"
|
||
clearable
|
||
placeholder="请输入"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<el-form-item label="收货地址">
|
||
<el-input
|
||
v-model="candidateQuery.arrivalAddress"
|
||
clearable
|
||
placeholder="请输入"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<el-form-item label="客户名称">
|
||
<el-select
|
||
v-model="candidateQuery.customerName"
|
||
clearable
|
||
filterable
|
||
remote
|
||
reserve-keyword
|
||
:remote-method="loadCustomerOptions"
|
||
:loading="customerLoading"
|
||
placeholder="请输入"
|
||
@visible-change="
|
||
visible => visible && loadCustomerOptions(candidateQuery.customerName)
|
||
"
|
||
>
|
||
<el-option
|
||
v-for="item in customerOptions"
|
||
:key="item.id || item.customerName"
|
||
:label="item.customerName"
|
||
:value="item.customerName"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<el-form-item label="货物类型">
|
||
<el-select
|
||
v-model="candidateQuery.cargoType"
|
||
clearable
|
||
filterable
|
||
remote
|
||
reserve-keyword
|
||
:remote-method="loadCargoTypeOptions"
|
||
:loading="cargoTypeLoading"
|
||
placeholder="请输入"
|
||
@visible-change="
|
||
visible => visible && loadCargoTypeOptions(candidateQuery.cargoType)
|
||
"
|
||
>
|
||
<el-option
|
||
v-for="item in cargoTypeOptions"
|
||
:key="item.id || item.cargoName || item.cargoType"
|
||
:label="item.cargoName || item.cargoType || item.label"
|
||
:value="item.cargoName || item.cargoType || item.label"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<el-form-item label="原始单号">
|
||
<el-input
|
||
v-model="candidateQuery.originalNo"
|
||
clearable
|
||
placeholder="请输入"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<el-form-item label="车牌号/船班号/船次号/班次号">
|
||
<el-input
|
||
v-model="candidateQuery.vehicleNo"
|
||
clearable
|
||
placeholder="请输入"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<el-form-item label="承运商名称">
|
||
<el-select
|
||
v-model="candidateQuery.carrierName"
|
||
clearable
|
||
filterable
|
||
remote
|
||
reserve-keyword
|
||
:remote-method="loadCarrierOptions"
|
||
:loading="carrierLoading"
|
||
placeholder="请输入"
|
||
@visible-change="
|
||
visible => visible && loadCarrierOptions(candidateQuery.carrierName)
|
||
"
|
||
>
|
||
<el-option
|
||
v-for="item in carrierOptions"
|
||
:key="item.id || item.fullName || item.customerName || item.carrierName"
|
||
:label="
|
||
item.fullName || item.customerName || item.carrierName || item.name
|
||
"
|
||
:value="
|
||
item.fullName || item.customerName || item.carrierName || item.name
|
||
"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<el-form-item label="司机">
|
||
<el-select
|
||
v-model="candidateQuery.driverName"
|
||
clearable
|
||
filterable
|
||
remote
|
||
reserve-keyword
|
||
:remote-method="loadDriverOptions"
|
||
:loading="driverLoading"
|
||
placeholder="请输入"
|
||
@visible-change="
|
||
visible => visible && loadDriverOptions(candidateQuery.driverName)
|
||
"
|
||
>
|
||
<el-option
|
||
v-for="item in driverOptions"
|
||
:key="item.id || item.driverName || item.name"
|
||
:label="driverOptionText(item)"
|
||
:value="item.driverName || item.name"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
</template>
|
||
<el-col
|
||
:span="candidateSearchVisible ? 24 : 6"
|
||
class="loading-manage-dialog__candidate-actions"
|
||
>
|
||
<el-button type="primary" @click="handleCandidateSearch">查询</el-button>
|
||
<el-button @click="handleCandidateReset">重置</el-button>
|
||
<el-link
|
||
type="primary"
|
||
@click="candidateSearchVisible = !candidateSearchVisible"
|
||
>{{ candidateSearchVisible ? '收起' : '展开' }}</el-link
|
||
>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
<el-table
|
||
ref="candidateTableRef"
|
||
v-loading="candidateLoading"
|
||
:data="candidateRows"
|
||
border
|
||
height="287"
|
||
row-key="id"
|
||
@selection-change="candidateSelectionChange"
|
||
>
|
||
<el-table-column type="selection" width="52" fixed="left" align="center" />
|
||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||
<el-table-column
|
||
prop="waybillNo"
|
||
label="运单号"
|
||
min-width="150"
|
||
show-overflow-tooltip
|
||
class-name="is-ellipsis-link"
|
||
>
|
||
<template #default="{ row }">
|
||
<el-link v-if="row.waybillNo" type="primary" @click="openWaybillDetail(row)">
|
||
{{ row.waybillNo }}
|
||
</el-link>
|
||
<span v-else>-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="projectName"
|
||
label="项目名称"
|
||
min-width="150"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column
|
||
prop="transportType"
|
||
label="运输方式"
|
||
min-width="130"
|
||
show-overflow-tooltip
|
||
>
|
||
<template #default="{ row }">
|
||
{{ transportTypeLabel(row.transportType) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="goodsInfo"
|
||
label="货物信息"
|
||
min-width="260"
|
||
show-overflow-tooltip
|
||
>
|
||
<template #default="{ row }">
|
||
{{ formatCandidateGoodsInfo(row) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="departureAddress"
|
||
label="发货地址"
|
||
min-width="220"
|
||
>
|
||
<template #default="{ row }">
|
||
<el-tooltip
|
||
v-if="formatCandidateAddressTooltip(row, 'departure')"
|
||
:content="formatCandidateAddressTooltip(row, 'departure')"
|
||
placement="top"
|
||
>
|
||
<div class="loading-manage-page__address-cell">
|
||
{{ formatCandidateAddress(row, 'departure') }}
|
||
</div>
|
||
</el-tooltip>
|
||
<span v-else>{{ formatCandidateAddress(row, 'departure') }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="arrivalAddress"
|
||
label="收货地址"
|
||
min-width="220"
|
||
>
|
||
<template #default="{ row }">
|
||
<el-tooltip
|
||
v-if="formatCandidateAddressTooltip(row, 'arrival')"
|
||
:content="formatCandidateAddressTooltip(row, 'arrival')"
|
||
placement="top"
|
||
>
|
||
<div class="loading-manage-page__address-cell">
|
||
{{ formatCandidateAddress(row, 'arrival') }}
|
||
</div>
|
||
</el-tooltip>
|
||
<span v-else>{{ formatCandidateAddress(row, 'arrival') }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="contractName"
|
||
label="客户合同"
|
||
min-width="160"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column
|
||
prop="carrierContractName"
|
||
label="承运商合同"
|
||
min-width="160"
|
||
show-overflow-tooltip
|
||
>
|
||
<template #default="{ row }">
|
||
{{ row.carrierContractName || '-' }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="planName"
|
||
label="计划名称"
|
||
min-width="150"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column
|
||
prop="originalNo"
|
||
label="原始单号"
|
||
min-width="140"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column
|
||
prop="relationNo"
|
||
label="关联单号"
|
||
min-width="140"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip />
|
||
<el-table-column
|
||
prop="startDate"
|
||
label="实际发货时间"
|
||
min-width="170"
|
||
show-overflow-tooltip
|
||
>
|
||
<template #default="{ row }">
|
||
{{ formatCandidateDate(row.startDate) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="endDate"
|
||
label="实际完成时间"
|
||
min-width="170"
|
||
show-overflow-tooltip
|
||
>
|
||
<template #default="{ row }">
|
||
{{ formatCandidateDate(row.endDate) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="createTime"
|
||
label="创建时间"
|
||
min-width="170"
|
||
show-overflow-tooltip
|
||
>
|
||
<template #default="{ row }">
|
||
{{ formatCandidateDate(row.createTime) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="updateTime"
|
||
label="更新时间"
|
||
min-width="170"
|
||
show-overflow-tooltip
|
||
>
|
||
<template #default="{ row }">
|
||
{{ formatCandidateDate(row.updateTime) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="businessStatus"
|
||
label="状态"
|
||
width="110"
|
||
fixed="right"
|
||
align="center"
|
||
>
|
||
<template #default="{ row }">
|
||
<span
|
||
:class="[
|
||
'loading-manage-page__status',
|
||
`is-${row.businessStatus || 'unknown'}`,
|
||
]"
|
||
>
|
||
{{ formatCandidateWaybillValue(row, 'businessStatus') }}
|
||
</span>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div class="loading-manage-dialog__table-toolbar">
|
||
<el-button
|
||
type="primary"
|
||
:disabled="!candidateSelection.length || candidateAdding"
|
||
:loading="candidateAdding"
|
||
@click="addSelectedWaybills"
|
||
>添加配载</el-button
|
||
>
|
||
<el-pagination
|
||
v-model:current-page="candidatePage.currentPage"
|
||
v-model:page-size="candidatePage.pageSize"
|
||
:page-sizes="candidatePage.pageSizes"
|
||
layout="total, prev, pager, next, sizes"
|
||
:total="candidatePage.total"
|
||
@size-change="handleCandidateSizeChange"
|
||
@current-change="handleCandidateCurrentChange"
|
||
/>
|
||
</div>
|
||
</section-card>
|
||
|
||
<div class="loading-manage-dialog__waybill-route-layout">
|
||
<section-card title="待配载列表" style="min-height: 257px">
|
||
<el-table
|
||
:data="selectedWaybillRows"
|
||
border
|
||
class="loading-manage-dialog__selected-table"
|
||
>
|
||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||
<el-table-column prop="waybillNo" label="运单号" min-width="150">
|
||
<template #default="{ row }">
|
||
<el-link
|
||
v-if="row.waybillNo"
|
||
type="primary"
|
||
@click.stop="openWaybillDetail(row)"
|
||
>
|
||
{{ row.waybillNo }}
|
||
</el-link>
|
||
<span v-else>-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="projectName" label="项目名称" min-width="160" />
|
||
<el-table-column
|
||
prop="departureAddress"
|
||
label="发货地"
|
||
min-width="220"
|
||
>
|
||
<template #default="{ row }">
|
||
<el-tooltip
|
||
v-if="formatCandidateAddressTooltip(row, 'departure')"
|
||
:content="formatCandidateAddressTooltip(row, 'departure')"
|
||
placement="top"
|
||
>
|
||
<div class="loading-manage-page__address-cell">
|
||
{{ formatCandidateAddress(row, 'departure') }}
|
||
</div>
|
||
</el-tooltip>
|
||
<span v-else>{{ formatCandidateAddress(row, 'departure') }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="arrivalAddress"
|
||
label="到货地"
|
||
min-width="220"
|
||
>
|
||
<template #default="{ row }">
|
||
<el-tooltip
|
||
v-if="formatCandidateAddressTooltip(row, 'arrival')"
|
||
:content="formatCandidateAddressTooltip(row, 'arrival')"
|
||
placement="top"
|
||
>
|
||
<div class="loading-manage-page__address-cell">
|
||
{{ formatCandidateAddress(row, 'arrival') }}
|
||
</div>
|
||
</el-tooltip>
|
||
<span v-else>{{ formatCandidateAddress(row, 'arrival') }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="110" fixed="right">
|
||
<template #default="{ row, $index }">
|
||
<el-link
|
||
v-if="!dialogReadonly && !isSelectedWaybillRemoveLocked(row)"
|
||
type="danger"
|
||
@click="removeSelectedWaybill($index)"
|
||
>删除</el-link
|
||
>
|
||
<span
|
||
v-else-if="!dialogReadonly && isSelectedWaybillRemoveLocked(row)"
|
||
class="loading-manage-dialog__locked-tip"
|
||
>已打卡</span
|
||
>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</section-card>
|
||
|
||
<section-card title="线路编辑">
|
||
<template v-if="isRunningEdit" #extra>
|
||
<span class="loading-manage-dialog__route-hint"
|
||
>已打卡地点置灰不可拖拽,新增地址仅可在未打卡节点间调整</span
|
||
>
|
||
</template>
|
||
<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',
|
||
{ 'is-locked': isRouteChangeNodeLocked(node) },
|
||
]"
|
||
:draggable="!dialogReadonly && !isRouteChangeNodeLocked(node)"
|
||
@dragstart="handleRouteDragStart(index)"
|
||
@dragover.prevent
|
||
@drop="handleRouteDrop(index)"
|
||
>
|
||
<el-icon
|
||
v-if="!dialogReadonly && !isRouteChangeNodeLocked(node)"
|
||
class="loading-manage-dialog__route-handle"
|
||
><Rank
|
||
/></el-icon>
|
||
<span v-else class="loading-manage-dialog__route-handle" />
|
||
<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="120">
|
||
<template #default="{ row }">{{ formatCargoQuantityDisplay(row) }}</template>
|
||
</el-table-column>
|
||
<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 || isRunningTaskRequiredLocked"
|
||
@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 === '承运商'
|
||
? taskCarrierSelectedValue
|
||
: dialogForm.carrierName
|
||
"
|
||
clearable
|
||
filterable
|
||
:disabled="
|
||
dialogReadonly ||
|
||
isRunningTaskRequiredLocked ||
|
||
!selectedWaybillRows.length
|
||
"
|
||
:loading="taskCarrierLoading"
|
||
:placeholder="taskCarrierPlaceholder"
|
||
@change="handleTaskCarrierChange"
|
||
@visible-change="visible => visible && loadTaskCarrierOptions()"
|
||
>
|
||
<el-option
|
||
v-for="item in taskCarrierOptions"
|
||
:key="carrierOptionValue(item)"
|
||
:label="
|
||
dialogForm.carrierType === '承运商'
|
||
? formatTaskCarrierContractLabel(item)
|
||
: item.carrierName || item.partyB
|
||
"
|
||
:value="
|
||
dialogForm.carrierType === '承运商'
|
||
? carrierOptionValue(item)
|
||
: 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="请输入"
|
||
:disabled="dialogReadonly || isRunningSelfDriverLocked"
|
||
@select="item => handleDriverSuggestionSelect('dialog', item)"
|
||
>
|
||
<template #default="{ item }">
|
||
<span>{{ driverOptionText(item) }}</span>
|
||
</template>
|
||
</el-autocomplete>
|
||
</el-form-item>
|
||
<el-form-item
|
||
label="手机号"
|
||
:required="dialogForm.carrierType === '自运'"
|
||
>
|
||
<el-input
|
||
v-model="dialogForm.driverPhone"
|
||
clearable
|
||
placeholder="请输入"
|
||
:disabled="dialogReadonly || isRunningSelfDriverLocked"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="车牌号" required>
|
||
<el-input
|
||
v-model="dialogForm.vehicleNo"
|
||
clearable
|
||
placeholder="请输入"
|
||
:disabled="dialogReadonly || isRunningTaskRequiredLocked"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item v-if="dialogForm.carrierType !== '承运商'" label="挂车车牌号">
|
||
<el-input
|
||
v-model="dialogForm.trailerVehicleNo"
|
||
clearable
|
||
placeholder="请输入"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</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="请输入"
|
||
:disabled="dialogReadonly"
|
||
@select="item => handleEscortSuggestionSelect('dialog', item)"
|
||
>
|
||
<template #default="{ item }">
|
||
<span>{{ driverOptionText(item) }}</span>
|
||
</template>
|
||
</el-autocomplete>
|
||
</el-form-item>
|
||
<el-form-item v-if="dialogForm.carrierType !== '承运商'" label="押运人手机号">
|
||
<el-input
|
||
v-model="dialogForm.escortPhone"
|
||
clearable
|
||
placeholder="请输入"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="里程(km)">
|
||
<el-input
|
||
:model-value="normalizeLoadingMileage(dialogForm.mileage)"
|
||
clearable
|
||
placeholder="请输入"
|
||
:disabled="dialogReadonly"
|
||
@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="请选择"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</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="请选择"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</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字以内"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</el-form-item>
|
||
</div>
|
||
</section-card>
|
||
</div>
|
||
</el-form>
|
||
</div>
|
||
|
||
<div class="loading-manage-dialog__footer">
|
||
<template v-if="dialogReadonly">
|
||
<el-button @click="closeLoadingDialog">关闭</el-button>
|
||
</template>
|
||
<template v-else>
|
||
<el-button @click="closeLoadingDialog">关闭</el-button>
|
||
<el-button
|
||
v-if="!isRunningEdit"
|
||
type="primary"
|
||
plain
|
||
:loading="dialogSaving === 'draft'"
|
||
@click="saveDraft"
|
||
>
|
||
暂存
|
||
</el-button>
|
||
<el-button type="primary" :loading="dialogSaving === 'submit'" @click="submitLoading">
|
||
{{ dialogMode === 'reassign' ? '确认派单' : isRunningEdit ? '保存' : '确认' }}
|
||
</el-button>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
<el-dialog
|
||
v-model="routeChangeVisible"
|
||
title="变更运输路线"
|
||
append-to-body
|
||
destroy-on-close
|
||
width="78%"
|
||
class="loading-route-change-dialog"
|
||
>
|
||
<el-tabs type="border-card" v-model="routeChangeTab">
|
||
<el-tab-pane label="变更路线" name="change">
|
||
<section-card title="关联子运单地址">
|
||
<el-table :data="routeChangeRows" border>
|
||
<el-table-column type="index" label="序号" width="70" />
|
||
<el-table-column prop="waybillNo" label="运单号" min-width="150"
|
||
><template #default="{ row }"
|
||
><el-link type="primary">{{ row.waybillNo || '-' }}</el-link></template
|
||
></el-table-column
|
||
>
|
||
<el-table-column prop="projectName" label="项目名称" min-width="160" />
|
||
<el-table-column label="发货地" min-width="360"
|
||
><template #default="{ row }"
|
||
><div class="loading-route-change-dialog__original">
|
||
原:{{ row.originalDepartureAddress || '-' }}
|
||
</div>
|
||
<div class="loading-route-change-dialog__address-row">
|
||
<el-input
|
||
:model-value="row.departureAddress"
|
||
readonly
|
||
placeholder="请点击地图选点"
|
||
:disabled="isWaybillCompleted(row)"
|
||
@click="openRouteChangeMapDialog(row, 'departure')"
|
||
><template v-if="!isWaybillCompleted(row)" #suffix
|
||
><el-icon
|
||
class="loading-route-change-dialog__location"
|
||
@click.stop="openRouteChangeMapDialog(row, 'departure')"
|
||
><Location /></el-icon
|
||
></template>
|
||
</el-input>
|
||
</div></template
|
||
></el-table-column>
|
||
<el-table-column label="到货地" min-width="360"
|
||
><template #default="{ row }"
|
||
><div class="loading-route-change-dialog__original">
|
||
原:{{ row.originalArrivalAddress || '-' }}
|
||
</div>
|
||
<div class="loading-route-change-dialog__address-row">
|
||
<el-input
|
||
:model-value="row.arrivalAddress"
|
||
readonly
|
||
placeholder="请点击地图选点"
|
||
:disabled="isWaybillCompleted(row)"
|
||
@click="openRouteChangeMapDialog(row, 'arrival')"
|
||
><template v-if="!isWaybillCompleted(row)" #suffix
|
||
><el-icon
|
||
class="loading-route-change-dialog__location"
|
||
@click.stop="openRouteChangeMapDialog(row, 'arrival')"
|
||
><Location /></el-icon
|
||
></template>
|
||
</el-input>
|
||
</div></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',
|
||
{ 'is-locked': isRouteChangeNodeLocked(node) },
|
||
]"
|
||
:draggable="!isRouteChangeNodeLocked(node)"
|
||
@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 v-if="!isRouteChangeNodeLocked(node)"><Rank /></el-icon
|
||
><span v-else class="loading-route-change-dialog__route-handle-placeholder" />
|
||
</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"
|
||
:show-overflow-tooltip="false"
|
||
><template #default="{ row }"
|
||
><div class="loading-route-change-dialog__record-content">
|
||
<div
|
||
v-for="(group, groupIndex) in parseRouteChangeContent(row.content)"
|
||
:key="groupIndex"
|
||
class="loading-route-change-dialog__change-group"
|
||
>
|
||
<div
|
||
v-if="group.title"
|
||
class="loading-route-change-dialog__change-group-title"
|
||
>
|
||
{{ group.title }}
|
||
</div>
|
||
<div v-if="group.text" class="loading-route-change-dialog__change-group-text">
|
||
{{ group.text }}
|
||
</div>
|
||
<div
|
||
v-for="(field, fieldIndex) in group.fields"
|
||
:key="fieldIndex"
|
||
class="loading-route-change-dialog__change-field"
|
||
>
|
||
<span class="loading-route-change-dialog__change-field-label">
|
||
{{ field.label }}
|
||
</span>
|
||
<template v-if="field.changed">
|
||
<span class="loading-route-change-dialog__change-before">
|
||
{{ field.before }}
|
||
</span>
|
||
<span class="loading-route-change-dialog__change-arrow">→</span>
|
||
<span class="loading-route-change-dialog__change-after">
|
||
{{ field.after }}
|
||
</span>
|
||
</template>
|
||
<span v-else class="loading-route-change-dialog__change-single">
|
||
{{ field.single }}
|
||
<span
|
||
v-if="field.unchanged"
|
||
class="loading-route-change-dialog__change-flag"
|
||
>
|
||
未变更
|
||
</span>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</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="routeChangeMapVisible"
|
||
title="地图选择地址"
|
||
append-to-body
|
||
width="920px"
|
||
class="loading-route-change-map-dialog"
|
||
@opened="initRouteChangeMap"
|
||
@closed="destroyRouteChangeMap"
|
||
>
|
||
<div class="loading-route-change-map-dialog__toolbar">
|
||
<el-input
|
||
v-model="routeChangeMapKeyword"
|
||
clearable
|
||
placeholder="输入地址关键词"
|
||
@keyup.enter="searchRouteChangeMap"
|
||
/>
|
||
<el-button
|
||
type="primary"
|
||
:icon="Search"
|
||
:loading="routeChangeMapLoading"
|
||
@click="searchRouteChangeMap"
|
||
>
|
||
搜索
|
||
</el-button>
|
||
</div>
|
||
<div class="map-picker-content">
|
||
<div ref="routeChangeMap" class="loading-route-change-map-dialog__map" />
|
||
<map-search-results
|
||
:results="routeChangeMapSearchResults"
|
||
@select="selectRouteChangeMapSearchResult"
|
||
/>
|
||
</div>
|
||
<div class="loading-route-change-map-dialog__status">
|
||
{{ routeChangeMapStatus
|
||
}}<span v-if="routeChangeMapSelected.regionName"
|
||
> 行政区划:{{ routeChangeMapSelected.regionName }}</span
|
||
>
|
||
</div>
|
||
<template #footer>
|
||
<el-button @click="routeChangeMapVisible = false">取消</el-button>
|
||
<el-button
|
||
type="primary"
|
||
:disabled="!routeChangeMapSelected.longitude"
|
||
@click="confirmRouteChangeMap"
|
||
>
|
||
确定
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
<el-image-viewer
|
||
v-if="attachmentImagePreviewVisible"
|
||
:url-list="attachmentImagePreviewUrls"
|
||
:initial-index="attachmentImagePreviewIndex"
|
||
@close="attachmentImagePreviewVisible = false"
|
||
/>
|
||
</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,
|
||
getPunchRecords as getWaybillPunchRecords,
|
||
} 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 { fetchDriverCandidates, driverOptionText } from '@/utils/carrier-org-resource';
|
||
import { formatContractOptionLabel } from '@/utils/contract';
|
||
import {
|
||
getList as getProcessConfigList,
|
||
getDetail as getProcessConfigDetail,
|
||
getVoucherImages as getProcessConfigVoucherImages,
|
||
} from '@/api/business/process-config';
|
||
import VoucherExecutionDetail from './components/voucher-execution-detail.vue';
|
||
import PunchRecordPhotos from './components/punch-record-photos.vue';
|
||
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, Search } from '@element-plus/icons-vue';
|
||
import TableHeaderTools from '@/components/table-header-tools/main.vue';
|
||
import dayjs from 'dayjs';
|
||
import { isMobile } from '@/utils/validate';
|
||
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
|
||
import { parseRouteChangeContent } from '@/utils/route-change';
|
||
|
||
const defaultTransportType = '';
|
||
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
|
||
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
|
||
const loadingCreateWaybillsStorageKey = 'loading-manage-create-waybills';
|
||
const loadingManageListRefreshKey = 'loading-manage-list-refresh';
|
||
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: '',
|
||
});
|
||
|
||
const createDialogForm = () => ({
|
||
id: '',
|
||
loadingNo: '',
|
||
loadingSubNos: '',
|
||
waybillIdsJson: '',
|
||
projectName: '',
|
||
customerName: '',
|
||
transportType: '',
|
||
cargoType: '',
|
||
cargoName: '',
|
||
vehicleNo: '',
|
||
trailerVehicleNo: '',
|
||
driverName: '',
|
||
driverPhone: '',
|
||
carrierType: '承运商',
|
||
carrierName: '',
|
||
carrierContractId: '',
|
||
carrierPartyId: '',
|
||
carrierPartyRole: '',
|
||
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;
|
||
};
|
||
|
||
// 货物等业务字段用 -1 表示未填写,展示时留空
|
||
const blankCargoSentinel = value => {
|
||
if (value === undefined || value === null || value === '') return '';
|
||
if (Number(value) === -1) return '';
|
||
return value;
|
||
};
|
||
|
||
const sanitizeCargoRow = (row = {}) => {
|
||
const result = { ...row };
|
||
Object.keys(result).forEach(key => {
|
||
result[key] = blankCargoSentinel(result[key]);
|
||
});
|
||
return result;
|
||
};
|
||
|
||
// 提交时未填写里程传 null,禁止自动填充 -1
|
||
const toSubmitMileage = value => {
|
||
const normalized = normalizeLoadingMileage(value);
|
||
if (normalized === '') return null;
|
||
const number = Number(normalized);
|
||
return Number.isFinite(number) ? number : null;
|
||
};
|
||
|
||
const createCopiedDialogForm = (detail = {}) => {
|
||
const form = createDialogForm();
|
||
Object.keys(form).forEach(prop => {
|
||
if (Object.prototype.hasOwnProperty.call(detail, prop)) form[prop] = detail[prop];
|
||
});
|
||
let taskInfo = {};
|
||
try {
|
||
const parsed = JSON.parse(form.taskInfoJson || '{}');
|
||
taskInfo = parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
||
} catch (error) {
|
||
taskInfo = {};
|
||
}
|
||
const { routeChangeRecords, ...copiedTaskInfo } = taskInfo;
|
||
return {
|
||
...form,
|
||
id: '',
|
||
loadingNo: '',
|
||
startDate: '',
|
||
endDate: '',
|
||
dataSource: '手工创建',
|
||
currentProcessNode: '接单',
|
||
mileage: normalizeLoadingMileage(form.mileage),
|
||
taskInfoJson: JSON.stringify(copiedTaskInfo),
|
||
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('、');
|
||
|
||
const LOADING_TABLE_COLUMNS = [
|
||
{ prop: 'loadingNo', label: '配载单号' },
|
||
{ prop: 'loadingSubNos', label: '运单号' },
|
||
{ prop: 'vehicleNo', label: '车牌号' },
|
||
{ prop: 'driverName', label: '司机' },
|
||
{ prop: 'driverPhone', label: '联系电话' },
|
||
{ prop: 'carrierType', label: '承运类型' },
|
||
{ prop: 'departureAddress', label: '发货地' },
|
||
{ prop: 'transitAddress', label: '途经地' },
|
||
{ prop: 'arrivalAddress', label: '到货地' },
|
||
{ prop: 'carrierName', label: '承运商' },
|
||
{ prop: 'transportType', label: '运输方式' },
|
||
{ prop: 'dataSource', label: '数据来源' },
|
||
{ prop: 'createUserName', label: '创建人' },
|
||
{ prop: 'createTime', label: '创建时间' },
|
||
{ prop: 'updateTime', label: '更新时间' },
|
||
{ prop: 'currentProcessNode', label: '过程节点' },
|
||
{ prop: 'businessStatus', label: '状态' },
|
||
];
|
||
|
||
export default {
|
||
components: {
|
||
Rank,
|
||
Search,
|
||
TableHeaderTools,
|
||
VoucherExecutionDetail,
|
||
PunchRecordPhotos,
|
||
},
|
||
data() {
|
||
return {
|
||
Rank,
|
||
Location,
|
||
Search,
|
||
loading: false,
|
||
tableSize: 'default',
|
||
searchVisible: true,
|
||
columnSettingOptions: LOADING_TABLE_COLUMNS,
|
||
visibleColumnKeys: LOADING_TABLE_COLUMNS.map(column => column.prop),
|
||
tableData: [],
|
||
page: {
|
||
currentPage: 1,
|
||
pageSize: 10,
|
||
total: 0,
|
||
pageSizes: [10, 20, 50, 100],
|
||
},
|
||
searchExpanded: false,
|
||
searchForm: createSearchForm(),
|
||
selectionRows: [],
|
||
dialogMode: 'add',
|
||
dialogLoading: false,
|
||
dialogSaving: '',
|
||
dialogForm: createDialogForm(),
|
||
candidateSearchVisible: false,
|
||
candidateQuery: createCandidateSearchForm(),
|
||
candidateLoading: false,
|
||
candidateAdding: false,
|
||
candidateRows: [],
|
||
candidateSelection: [],
|
||
candidatePage: {
|
||
currentPage: 1,
|
||
pageSize: 10,
|
||
total: 0,
|
||
pageSizes: [10, 20, 50, 100],
|
||
},
|
||
selectedWaybillRows: [],
|
||
routeNodes: [],
|
||
cargoRows: [],
|
||
routeDragIndex: -1,
|
||
/** 进行中编辑:子运单打卡记录映射 waybillId -> records[] */
|
||
waybillPunchMap: {},
|
||
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,
|
||
processDetailTab: 'punch',
|
||
processWaybillId: '',
|
||
processWaybillOptions: [],
|
||
punchRecords: [],
|
||
punchRecordsLoading: false,
|
||
driverUploads: [],
|
||
hasRelatedVoucher: false,
|
||
voucherImages: [],
|
||
voucherFolders: [],
|
||
voucherFolder: null,
|
||
voucherFolderView: false,
|
||
voucherImagesLoading: false,
|
||
voucherImagesLoaded: false,
|
||
attachmentImagePreviewVisible: false,
|
||
attachmentImagePreviewUrls: [],
|
||
attachmentImagePreviewIndex: 0,
|
||
routeChangeVisible: false,
|
||
routeChangeTab: 'change',
|
||
routeChangeRows: [],
|
||
routeChangeNodes: [],
|
||
routeChangeRemark: '',
|
||
routeChangeRecords: [],
|
||
routeChangeDragIndex: -1,
|
||
routeChangeOriginalAddresses: [],
|
||
routeChangeOrderDirty: false,
|
||
routeChangeMapVisible: false,
|
||
routeChangeMapLoading: false,
|
||
routeChangeMapKeyword: '',
|
||
routeChangeMapStatus: '可搜索地址或点击地图选点',
|
||
routeChangeMapSelected: {},
|
||
routeChangeMapSearchResults: [],
|
||
routeChangeMapTarget: null,
|
||
routeChangeMapInstance: null,
|
||
routeChangeMapMarker: null,
|
||
routeChangeMapGeocoder: null,
|
||
detailAmap: null,
|
||
};
|
||
},
|
||
computed: {
|
||
dialogReadonly() {
|
||
return this.dialogMode === 'view';
|
||
},
|
||
/** 进行中配载编辑:可加子单/调未打卡路线,但任务必填字段锁定 */
|
||
isRunningEdit() {
|
||
return (
|
||
this.dialogMode === 'edit' &&
|
||
String(this.dialogForm.businessStatus || '') === 'running'
|
||
);
|
||
},
|
||
/** 进行中任务必填字段(已派车)不可改 */
|
||
isRunningTaskRequiredLocked() {
|
||
return this.isRunningEdit;
|
||
},
|
||
/** 自运进行中:司机/手机号同属必填派车信息,不可改 */
|
||
isRunningSelfDriverLocked() {
|
||
return (
|
||
this.isRunningEdit && String(this.dialogForm.carrierType || '').trim() === '自运'
|
||
);
|
||
},
|
||
// 打卡时间轴:已打卡按时间升序,未打卡保持原节点顺序排在其后
|
||
orderedPunchRecords() {
|
||
const records = Array.isArray(this.punchRecords) ? this.punchRecords : [];
|
||
return records
|
||
.map((record, index) => ({
|
||
record,
|
||
index,
|
||
punchedAt: this.punchRecordTimestamp(record),
|
||
}))
|
||
.sort((a, b) => {
|
||
const aPunched = a.punchedAt !== null;
|
||
const bPunched = b.punchedAt !== null;
|
||
if (aPunched && bPunched) return a.punchedAt - b.punchedAt || a.index - b.index;
|
||
if (aPunched !== bPunched) return aPunched ? -1 : 1;
|
||
return a.index - b.index;
|
||
})
|
||
.map(item => item.record);
|
||
},
|
||
taskCarrierPlaceholder() {
|
||
if (!this.selectedWaybillRows.length) return '请先选择运单';
|
||
if (!this.taskCarrierLoading && !this.taskCarrierOptions.length) return '暂无可用承运商合同';
|
||
return '请选择';
|
||
},
|
||
taskCarrierSelectedValue() {
|
||
return this.carrierOptionValue({
|
||
id: this.dialogForm.carrierContractId,
|
||
carrierPartyId: this.dialogForm.carrierPartyId,
|
||
carrierName: this.dialogForm.carrierName,
|
||
});
|
||
},
|
||
detailStatusType() {
|
||
const typeMap = {
|
||
pending: 'success',
|
||
running: 'success',
|
||
processing: 'success',
|
||
completed: 'primary',
|
||
cancelled: 'info',
|
||
draft: 'info',
|
||
};
|
||
return typeMap[this.dialogForm.businessStatus] || 'warning';
|
||
},
|
||
taskSummary() {
|
||
return (
|
||
[this.dialogForm.carrierName, this.dialogForm.driverName, this.dialogForm.vehicleNo]
|
||
.filter(Boolean)
|
||
.join(' / ') || '-'
|
||
);
|
||
},
|
||
isSelfCarrierDetail() {
|
||
return String(this.dialogForm.carrierType || '').trim() === '自运';
|
||
},
|
||
isCarrierExecutionVoucher() {
|
||
if (String(this.dialogForm.carrierType || '').trim() === '承运商') return true;
|
||
const waybill = (this.selectedWaybillRows || []).find(
|
||
item => String(item.id) === String(this.processWaybillId)
|
||
);
|
||
return String(waybill?.carrierTypeName || waybill?.carrierType || '').trim() === '承运商';
|
||
},
|
||
detailTaskMileage() {
|
||
return normalizeLoadingMileage(this.dialogForm.mileage);
|
||
},
|
||
detailAttachments() {
|
||
return parseJsonArray(this.dialogForm.attachmentsJson || this.dialogForm.attachmentJson).map(
|
||
item =>
|
||
typeof item === 'string'
|
||
? { name: item.split('/').pop(), url: item }
|
||
: {
|
||
name: item.name || item.fileName || '附件',
|
||
url: item.url || item.link || item.fileUrl,
|
||
}
|
||
);
|
||
},
|
||
// 独立表单页:/business/loading-manage/form?mode=add|edit|reassign
|
||
isStandaloneFormPage() {
|
||
return (
|
||
this.$route.path === '/business/loading-manage/form' &&
|
||
['add', 'edit', 'reassign'].includes(this.$route.query.mode)
|
||
);
|
||
},
|
||
// 独立详情页:/business/loading-manage/detail?id=xxx
|
||
isStandaloneDetailPage() {
|
||
return this.$route.path === '/business/loading-manage/detail';
|
||
},
|
||
// 列表容器隐藏搜索栏 / 工具栏 / 表格 / 分页的统一开关
|
||
isStandalonePage() {
|
||
return this.isStandaloneFormPage || this.isStandaloneDetailPage;
|
||
},
|
||
formPageDefaultTitle() {
|
||
const titleMap = {
|
||
add: '新增配载管理',
|
||
edit: '编辑配载管理',
|
||
reassign: '重新派单',
|
||
};
|
||
return titleMap[this.$route.query.mode] || '新增配载管理';
|
||
},
|
||
formPageTitle() {
|
||
return this.$route.query.name || this.formPageDefaultTitle;
|
||
},
|
||
detailPageTitle() {
|
||
return this.$route.query.name || '配载单详情';
|
||
},
|
||
},
|
||
mounted() {
|
||
this.loadTransportTypeOptions();
|
||
this.loadTable();
|
||
if (this.isStandalonePage) {
|
||
this.syncStandaloneTagTitle();
|
||
if (this.isStandaloneDetailPage) this.initStandaloneDetailPage();
|
||
else this.initStandaloneFormPage();
|
||
}
|
||
// 兼容旧链接:/business/loading-manage?detailId=xxx 直接跳独立详情页
|
||
const detailId = this.$route.query.detailId;
|
||
if (detailId) this.gotoLoadingDetail({ id: detailId });
|
||
},
|
||
// 标签切走(实例被 keep-alive 缓存)或销毁时,先收起页内子弹窗
|
||
deactivated() {
|
||
this.closeInnerDialogs();
|
||
},
|
||
activated() {
|
||
if (this.isStandalonePage) return;
|
||
if (sessionStorage.getItem(loadingManageListRefreshKey)) {
|
||
sessionStorage.removeItem(loadingManageListRefreshKey);
|
||
this.loadTable();
|
||
}
|
||
},
|
||
beforeUnmount() {
|
||
this.closeInnerDialogs();
|
||
},
|
||
watch: {
|
||
$route(to, from) {
|
||
if (!this.isStandalonePage) {
|
||
// 从表单/详情返回列表时刷新,避免草稿确认后仍显示旧状态
|
||
if (
|
||
from?.path?.includes('/loading-manage/form') ||
|
||
from?.path?.includes('/loading-manage/detail')
|
||
) {
|
||
sessionStorage.removeItem(loadingManageListRefreshKey);
|
||
this.loadTable();
|
||
}
|
||
return;
|
||
}
|
||
this.syncStandaloneTagTitle();
|
||
if (this.isStandaloneDetailPage) {
|
||
// 记录变化或上一个页面不是详情态(例如从表单页/列表页进来)时重新拉取详情
|
||
const idChanged = String(to.query.id || '') !== String(from?.query?.id || '');
|
||
if (idChanged || this.dialogMode !== 'view') this.initStandaloneDetailPage();
|
||
return;
|
||
}
|
||
if (
|
||
to.query.mode !== from?.query?.mode ||
|
||
String(to.query.id || '') !== String(from?.query.id || '') ||
|
||
String(to.query.copyId || '') !== String(from?.query.copyId || '')
|
||
) {
|
||
this.initStandaloneFormPage();
|
||
}
|
||
},
|
||
'$route.query.detailId'(detailId) {
|
||
if (detailId) this.gotoLoadingDetail({ id: detailId });
|
||
},
|
||
},
|
||
methods: {
|
||
parseRouteChangeContent,
|
||
driverOptionText,
|
||
formatTaskCarrierContractLabel(item = {}) {
|
||
const carrierName = item.carrierName || item.partyB || '';
|
||
const roleLabel =
|
||
item.carrierPartyRole === 'C' || item.carrierPartyRole === '丙'
|
||
? '丙方'
|
||
: item.carrierPartyRole === 'B' || item.carrierPartyRole === '乙'
|
||
? '乙方'
|
||
: '';
|
||
const contractLabel = formatContractOptionLabel(item);
|
||
const nameWithRole = roleLabel && carrierName ? `${carrierName}(${roleLabel})` : carrierName;
|
||
if (nameWithRole && contractLabel) {
|
||
return `${nameWithRole} - ${contractLabel}`;
|
||
}
|
||
return nameWithRole || contractLabel;
|
||
},
|
||
carrierOptionValue(item = {}) {
|
||
return [item.id || '', item.carrierPartyId || '', item.carrierName || item.partyB || ''].join(
|
||
'__'
|
||
);
|
||
},
|
||
syncStandaloneTagTitle() {
|
||
if (!this.isStandalonePage) return;
|
||
const title = this.isStandaloneDetailPage ? this.detailPageTitle : this.formPageDefaultTitle;
|
||
if (this.$route.query.name === title) return;
|
||
this.$store.commit('SET_TAG', {
|
||
fullPath: this.$route.fullPath,
|
||
query: { ...this.$route.query, name: title },
|
||
});
|
||
},
|
||
buildQueryParams(form) {
|
||
const params = { ...form };
|
||
const [startDateStart, startDateEnd] = params.startDateRange || [];
|
||
const [endDateStart, endDateEnd] = params.endDateRange || [];
|
||
delete params.startDateRange;
|
||
delete params.endDateRange;
|
||
return {
|
||
...params,
|
||
startDateStart,
|
||
startDateEnd,
|
||
endDateStart,
|
||
endDateEnd,
|
||
};
|
||
},
|
||
async loadTable() {
|
||
this.loading = true;
|
||
try {
|
||
const res = await loadingApi.getList(
|
||
this.page.currentPage,
|
||
this.page.pageSize,
|
||
this.buildQueryParams(this.searchForm)
|
||
);
|
||
const page = unwrapPage(res);
|
||
this.tableData = page.records;
|
||
this.page.total = page.total;
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
},
|
||
handleSearch() {
|
||
this.page.currentPage = 1;
|
||
this.loadTable();
|
||
},
|
||
handleResetSearch() {
|
||
this.searchForm = createSearchForm();
|
||
this.page.currentPage = 1;
|
||
this.loadTable();
|
||
},
|
||
handleSizeChange(size) {
|
||
this.page.currentPage = 1;
|
||
this.page.pageSize = size;
|
||
this.loadTable();
|
||
},
|
||
handleCurrentChange(current) {
|
||
this.page.currentPage = current;
|
||
this.loadTable();
|
||
},
|
||
selectionChange(rows) {
|
||
this.selectionRows = rows || [];
|
||
},
|
||
isColumnVisible(prop) {
|
||
return this.visibleColumnKeys.includes(prop);
|
||
},
|
||
statusText(row) {
|
||
return row.businessStatusName || loadingStatusMap[row.businessStatus] || '未知';
|
||
},
|
||
formatCandidateWaybillValue(row, prop) {
|
||
if (!row) return '-';
|
||
if (prop === 'businessStatus') return this.statusText(row);
|
||
if (prop === 'transportType') return this.transportTypeLabel(row.transportType);
|
||
if (prop === 'goodsInfo') return this.formatCandidateGoodsInfo(row);
|
||
const column = waybillOption.column.find(item => item.prop === prop);
|
||
if (column?.formatter) {
|
||
const value = column.formatter(row, {}, row[prop]);
|
||
return value === undefined || value === null || value === '' ? '-' : value;
|
||
}
|
||
const value = row[prop];
|
||
return value === undefined || value === null || value === '' ? '-' : value;
|
||
},
|
||
formatCandidateGoodsInfo(row = {}) {
|
||
const goodsRows =
|
||
[row.goodsJson, row.goodsList, row.goodsRows, row.cargoList, row.cargoRows, row.goods]
|
||
.map(source => {
|
||
if (source === undefined || source === null || source === '') return [];
|
||
if (Array.isArray(source)) return source;
|
||
if (typeof source === 'object') return [source];
|
||
return parseJsonArray(source);
|
||
})
|
||
.filter(source => source.length)
|
||
.sort((left, right) => right.length - left.length)[0] || [];
|
||
if (!goodsRows.length) return String(row.goodsInfo || row.cargoInfo || '') || '-';
|
||
const groups = goodsRows.reduce((result, item = {}) => {
|
||
const unit = String(
|
||
item.quantityUnit ||
|
||
item.goodsQuantityUnit ||
|
||
item.cargoUnit ||
|
||
item.unit ||
|
||
row.quantityUnit ||
|
||
row.goodsQuantityUnit ||
|
||
row.cargoUnit ||
|
||
row.unit ||
|
||
''
|
||
).trim();
|
||
const key = unit || '__empty__';
|
||
if (!result[key]) {
|
||
result[key] = {
|
||
typeName:
|
||
item.secondCargoTypeName ||
|
||
item.secondCargoType ||
|
||
item.cargoType ||
|
||
item.goodsType ||
|
||
item.typeName ||
|
||
'货物',
|
||
count: 0,
|
||
quantity: 0,
|
||
unit,
|
||
};
|
||
}
|
||
result[key].count += 1;
|
||
result[key].quantity += this.parseGoodsQuantity(
|
||
item.quantity ?? item.cargoQuantity ?? item.goodsQuantity
|
||
);
|
||
return result;
|
||
}, {});
|
||
return (
|
||
Object.values(groups)
|
||
.map(group => {
|
||
const quantity = this.formatGoodsQuantity(group.quantity);
|
||
const typeName =
|
||
group.count > 1 ? `${group.typeName}等${group.count}种货物` : group.typeName;
|
||
return `${typeName} | ${quantity}${group.unit ? ` ${group.unit}` : ''}`;
|
||
})
|
||
.join('; ') || '-'
|
||
);
|
||
},
|
||
parseGoodsQuantity(value) {
|
||
if (value === undefined || value === null || value === '') return 0;
|
||
const text = String(value).replace(/,/g, '');
|
||
const match = text.match(/-?\d+(\.\d+)?/);
|
||
if (!match) return 0;
|
||
const number = Number(match[0]);
|
||
return Number.isFinite(number) ? number : 0;
|
||
},
|
||
formatGoodsQuantity(value) {
|
||
const number = Number(value || 0);
|
||
if (!Number.isFinite(number)) return '0';
|
||
const fixed = Math.round((number + Number.EPSILON) * 1000) / 1000;
|
||
return Number.isInteger(fixed) ? String(fixed) : String(fixed).replace(/\.?0+$/, '');
|
||
},
|
||
formatCandidateDate(value) {
|
||
if (!value) return '-';
|
||
const date = dayjs(value);
|
||
return date.isValid() ? date.format('YYYY-MM-DD HH:mm:ss') : value;
|
||
},
|
||
formatCandidateAddress(row, type) {
|
||
const prefix = type === 'departure' ? 'departure' : 'arrival';
|
||
const name = String(row?.[`${prefix}Name`] || '').trim();
|
||
const address = String(row?.[`${prefix}Address`] || '').trim();
|
||
if (!this.isRoadTransportType(row?.transportType)) {
|
||
return name || address || '-';
|
||
}
|
||
const region = this.mergeRouteAddressParts(
|
||
this.routeProvinceName(row, prefix),
|
||
row[`${prefix}CityName`] || row[`${prefix}City`],
|
||
row[`${prefix}DistrictName`] || row[`${prefix}District`]
|
||
);
|
||
const source = this.mergeRouteAddressParts(region, '', address || name);
|
||
const parts = this.parseWaybillAddress(source);
|
||
return (
|
||
[parts.province, parts.city, parts.district]
|
||
.map(value => String(value || '').trim())
|
||
.filter(Boolean)
|
||
.join('') ||
|
||
this.formatProvinceCityDistrict(source) ||
|
||
name ||
|
||
address ||
|
||
'-'
|
||
);
|
||
},
|
||
formatCandidateAddressTooltip(row, type) {
|
||
if (!this.isRoadTransportType(row?.transportType)) return '';
|
||
const prefix = type === 'departure' ? 'departure' : 'arrival';
|
||
const address = String(row?.[`${prefix}Address`] || '').trim();
|
||
const name = String(row?.[`${prefix}Name`] || '').trim();
|
||
const region = this.mergeRouteAddressParts(
|
||
this.routeProvinceName(row, prefix),
|
||
row[`${prefix}CityName`] || row[`${prefix}City`],
|
||
row[`${prefix}DistrictName`] || row[`${prefix}District`]
|
||
);
|
||
const detailAddress =
|
||
row[`${prefix}DetailAddress`] || row[`${prefix}AddressDetail`] || address;
|
||
return this.mergeRouteAddressParts(region, '', detailAddress || name) || '';
|
||
},
|
||
isRoadTransportType(type) {
|
||
const value = String(type || '')
|
||
.trim()
|
||
.toLowerCase();
|
||
return (
|
||
value === 'road' ||
|
||
value.includes('公路') ||
|
||
value.includes('道路') ||
|
||
value.includes('highway')
|
||
);
|
||
},
|
||
// 与总单详情一致:公路地址压缩为「市 区县」
|
||
formatRoadLocation(value) {
|
||
const text = String(value || '')
|
||
.replace(/[\//\s]+/g, '')
|
||
.trim();
|
||
if (!text) return '';
|
||
|
||
const parts = this.parseWaybillAddress(text);
|
||
let city = String(parts.city || '').trim();
|
||
let district = String(parts.district || '').trim();
|
||
let detail = String(parts.detailAddress || '').trim();
|
||
|
||
// 自治州/地区/盟下还有「XX市 + 区县」时,以市级作为展示主体
|
||
if (/(?:自治州|地区|盟)$/.test(city) && detail) {
|
||
const nestedCityMatch = detail.match(/^(.+?市)/);
|
||
if (nestedCityMatch) {
|
||
city = nestedCityMatch[1];
|
||
detail = detail.slice(nestedCityMatch[1].length);
|
||
const nestedDistrictMatch = detail.match(
|
||
/^(.+?(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗))/
|
||
);
|
||
if (nestedDistrictMatch) district = nestedDistrictMatch[1];
|
||
} else if (!district) {
|
||
const nestedDistrictMatch = detail.match(
|
||
/^(.+?(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗))/
|
||
);
|
||
if (nestedDistrictMatch) district = nestedDistrictMatch[1];
|
||
}
|
||
}
|
||
|
||
// 市级后紧跟区县,但 parse 未拆出时再补一次
|
||
if (city && !district && detail) {
|
||
const nestedDistrictMatch = detail.match(
|
||
/^(.+?(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗))/
|
||
);
|
||
if (nestedDistrictMatch) district = nestedDistrictMatch[1];
|
||
}
|
||
|
||
if (city && district) {
|
||
// 兼容「梅县区」:命中「县」后还需保留后面的「区」
|
||
if (district.endsWith('县') && detail.startsWith(`${district}区`)) {
|
||
district = `${district}区`;
|
||
}
|
||
return `${city} ${district}`;
|
||
}
|
||
if (city) return city;
|
||
if (district) return district;
|
||
|
||
// 兜底压缩
|
||
const withoutProvince = text.replace(/^.*?(?:省|自治区|特别行政区)/, '');
|
||
const cityMatch = withoutProvince.match(/.+?(?:市|自治州|州(?!市)|盟(?!市)|地区)/);
|
||
if (!cityMatch) return text;
|
||
const fallbackCity = cityMatch[0];
|
||
const districtSource = withoutProvince.slice(fallbackCity.length);
|
||
const districtMatch = districtSource.match(
|
||
/^[^市州盟]*?(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗)/
|
||
);
|
||
if (!districtMatch) {
|
||
// 州后面是县级市时,继续解析市 + 区
|
||
const nestedCityMatch = districtSource.match(/^(.+?市)/);
|
||
if (nestedCityMatch) {
|
||
const nestedDistrictSource = districtSource.slice(nestedCityMatch[1].length);
|
||
const nestedDistrictMatch = nestedDistrictSource.match(
|
||
/^[^市州盟]*?(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗)/
|
||
);
|
||
if (nestedDistrictMatch) {
|
||
return `${nestedCityMatch[1]} ${nestedDistrictMatch[0]}`;
|
||
}
|
||
return nestedCityMatch[1];
|
||
}
|
||
return fallbackCity;
|
||
}
|
||
const fallbackDistrict = `${districtMatch[0]}${
|
||
districtSource.slice(districtMatch[0].length).startsWith('区') ? '区' : ''
|
||
}`;
|
||
return `${fallbackCity} ${fallbackDistrict}`;
|
||
},
|
||
isStationLikeName(value) {
|
||
const text = String(value || '').trim();
|
||
if (!text) return false;
|
||
return (
|
||
/(?:火车站|高铁站|客运站|货运站|港口|码头|机场|空港|航站楼)$/.test(text) ||
|
||
/(?<!市|州|盟|区|县|旗)站$/.test(text)
|
||
);
|
||
},
|
||
// 详情运输路线短展示:公路仅市区,非公路仅名称
|
||
formatDetailRouteShort(node = {}) {
|
||
const name = String(node.name || '').trim();
|
||
const address = String(node.address || '').trim();
|
||
const displayAddress = String(node.displayAddress || '').trim();
|
||
if (!this.isRoadTransportType(this.dialogForm.transportType)) {
|
||
if (name) return name;
|
||
if (this.isStationLikeName(displayAddress)) return displayAddress;
|
||
if (this.isStationLikeName(address)) return address;
|
||
return displayAddress || address || '-';
|
||
}
|
||
const enriched = this.enrichRouteNodeRegion(node);
|
||
// 合并节点地址与运单上的市/区字段,避免只解析到市
|
||
const region = this.mergeRouteAddressParts(enriched.cityName, enriched.districtName, '');
|
||
const source = this.mergeRouteAddressParts(
|
||
address,
|
||
displayAddress,
|
||
region,
|
||
enriched.name || name
|
||
);
|
||
const formatted = this.formatRoadLocation(source);
|
||
if (enriched.cityName && enriched.districtName) {
|
||
return `${enriched.cityName} ${enriched.districtName}`;
|
||
}
|
||
if (/(?:市|州|盟|区|县|旗)/.test(formatted)) return formatted;
|
||
return formatted || name || displayAddress || address || '-';
|
||
},
|
||
// 公路悬停展示完整地址;非公路不展示 tooltip
|
||
formatDetailRouteTooltip(node = {}) {
|
||
if (!this.isRoadTransportType(this.dialogForm.transportType)) return '';
|
||
const enriched = this.enrichRouteNodeRegion(node);
|
||
const region = this.mergeRouteAddressParts(enriched.cityName, enriched.districtName, '');
|
||
const full = this.mergeRouteAddressParts(
|
||
String(node.address || '').trim(),
|
||
String(node.displayAddress || '').trim(),
|
||
region,
|
||
String(node.name || enriched.name || '').trim()
|
||
);
|
||
const short = this.formatDetailRouteShort(node);
|
||
if (!full) return '';
|
||
return full === short ? '' : full;
|
||
},
|
||
// 按装/卸标签从已选运单回填市、区,补充 routeJson 中缺失的区县
|
||
enrichRouteNodeRegion(node = {}) {
|
||
const tags = Array.isArray(node.tags) ? node.tags : [];
|
||
const result = { cityName: '', districtName: '', name: '' };
|
||
for (const tag of tags) {
|
||
const text = String(tag || '');
|
||
const loadMatch = text.match(/^装(\d+)$/);
|
||
const unloadMatch = text.match(/^卸(\d+)$/);
|
||
const index = Number((loadMatch || unloadMatch)?.[1] || 0) - 1;
|
||
if (index < 0) continue;
|
||
const row = this.selectedWaybillRows[index];
|
||
if (!row) continue;
|
||
const prefix = loadMatch ? 'departure' : 'arrival';
|
||
result.cityName = String(
|
||
row[`${prefix}CityName`] || row[`${prefix}City`] || ''
|
||
).trim();
|
||
result.districtName = String(
|
||
row[`${prefix}DistrictName`] || row[`${prefix}District`] || ''
|
||
).trim();
|
||
result.name = String(row[`${prefix}Name`] || '').trim();
|
||
if (result.cityName || result.districtName || result.name) break;
|
||
}
|
||
return result;
|
||
},
|
||
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, name: '运单详情' },
|
||
});
|
||
},
|
||
formatRouteAddress(row, type) {
|
||
const prefix =
|
||
type === 'departure' ? 'departure' : type === 'arrival' ? 'arrival' : 'transit';
|
||
if (type === 'transit') {
|
||
const values = this.splitText(row.transitAddress || row.transitName || '')
|
||
.map(value => this.formatListRouteShort(row, value, value))
|
||
.filter(value => value && value !== '-');
|
||
return [...new Set(values)].join(',') || '-';
|
||
}
|
||
const name = String(row[`${prefix}Name`] || '').trim();
|
||
const address = String(row[`${prefix}Address`] || '').trim();
|
||
const cityName = String(
|
||
row[`${prefix}CityName`] || row[`${prefix}City`] || ''
|
||
).trim();
|
||
const districtName = String(
|
||
row[`${prefix}DistrictName`] || row[`${prefix}District`] || ''
|
||
).trim();
|
||
if (
|
||
this.isRoadTransportType(row?.transportType) &&
|
||
cityName &&
|
||
districtName
|
||
) {
|
||
return `${cityName} ${districtName}`;
|
||
}
|
||
const region = this.mergeRouteAddressParts(
|
||
this.routeProvinceName(row, prefix),
|
||
cityName,
|
||
districtName
|
||
);
|
||
const source = this.mergeRouteAddressParts(region, '', address || name);
|
||
return this.formatListRouteShort(row, name, source || address || name);
|
||
},
|
||
formatFullRouteAddress(row, type) {
|
||
if (!this.isRoadTransportType(row?.transportType)) return '';
|
||
const prefix =
|
||
type === 'departure' ? 'departure' : type === 'arrival' ? 'arrival' : 'transit';
|
||
if (type === 'transit') {
|
||
const values = this.splitText(row.transitAddress || '')
|
||
.map(value => String(value || '').trim())
|
||
.filter(Boolean);
|
||
const full = [...new Set(values)].join(',');
|
||
const short = this.formatRouteAddress(row, 'transit');
|
||
return full && full !== short ? full : '';
|
||
}
|
||
const name = String(row[`${prefix}Name`] || '').trim();
|
||
const address = String(row[`${prefix}Address`] || '').trim();
|
||
const region = this.mergeRouteAddressParts(
|
||
this.routeProvinceName(row, prefix),
|
||
row[`${prefix}CityName`] || row[`${prefix}City`],
|
||
row[`${prefix}DistrictName`] || row[`${prefix}District`]
|
||
);
|
||
const full = this.mergeRouteAddressParts(region, '', address || name);
|
||
const short = this.formatRouteAddress(row, type);
|
||
if (!full) return '';
|
||
return full === short ? '' : full;
|
||
},
|
||
// 列表短地址:公路「市 区县」,非公路仅名称
|
||
formatListRouteShort(row = {}, name = '', address = '') {
|
||
const rawName = String(name || '').trim();
|
||
const rawAddress = String(address || '').trim();
|
||
if (!this.isRoadTransportType(row?.transportType)) {
|
||
if (rawName) return rawName;
|
||
if (this.isStationLikeName(rawAddress)) return rawAddress;
|
||
return rawAddress || '-';
|
||
}
|
||
const source = rawAddress || rawName;
|
||
const formatted = this.formatRoadLocation(source);
|
||
if (/(?:市|州|盟)/.test(formatted)) return formatted;
|
||
return formatted || rawName || rawAddress || '-';
|
||
},
|
||
formatWaybillAddress(row, type) {
|
||
const prefix = type === 'departure' ? 'departure' : 'arrival';
|
||
const address = row[`${prefix}Address`] || row[`${prefix}Name`] || '';
|
||
const region = this.mergeRouteAddressParts(
|
||
this.routeProvinceName(row, prefix),
|
||
row[`${prefix}CityName`] || row[`${prefix}City`],
|
||
row[`${prefix}DistrictName`] || row[`${prefix}District`]
|
||
);
|
||
const source = this.mergeRouteAddressParts(region, '', address);
|
||
const parts = this.parseWaybillAddress(source);
|
||
const detailAddress =
|
||
row[`${prefix}DetailAddress`] ||
|
||
row[`${prefix}AddressDetail`] ||
|
||
parts.detailAddress ||
|
||
address;
|
||
return (
|
||
[parts.province, parts.city, parts.district, detailAddress]
|
||
.map(value => String(value || '').trim())
|
||
.filter(Boolean)
|
||
.join(' / ') || '-'
|
||
);
|
||
},
|
||
parseWaybillAddress(address) {
|
||
let rest = String(address || '').trim();
|
||
const result = { province: '', city: '', district: '', detailAddress: '' };
|
||
if (!rest) return result;
|
||
|
||
const municipalityMatch = rest.match(/^(北京市|天津市|上海市|重庆市)/);
|
||
if (municipalityMatch) {
|
||
result.province = municipalityMatch[0];
|
||
result.city = municipalityMatch[0];
|
||
rest = rest.slice(municipalityMatch[0].length);
|
||
} else {
|
||
const provinceMatch = rest.match(/^(.+?(?:省|自治区|特别行政区))/);
|
||
if (provinceMatch) {
|
||
result.province = provinceMatch[0];
|
||
rest = rest.slice(provinceMatch[0].length);
|
||
}
|
||
const cityMatch = rest.match(/^(.+?(?:市|自治州|地区|盟))/);
|
||
if (cityMatch) {
|
||
result.city = cityMatch[0];
|
||
rest = rest.slice(cityMatch[0].length);
|
||
}
|
||
}
|
||
|
||
const districtMatch = rest.match(/^(.+?(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗))/);
|
||
if (districtMatch) {
|
||
result.district = districtMatch[0];
|
||
rest = rest.slice(districtMatch[0].length);
|
||
}
|
||
result.detailAddress = rest;
|
||
return result;
|
||
},
|
||
formatAddressText(address) {
|
||
const parts = this.parseWaybillAddress(address);
|
||
return (
|
||
[parts.province, parts.city, parts.district, parts.detailAddress]
|
||
.map(value => String(value || '').trim())
|
||
.filter(Boolean)
|
||
.join(' / ') ||
|
||
String(address || '').trim() ||
|
||
'-'
|
||
);
|
||
},
|
||
mergeRouteAddressParts(region, district, address) {
|
||
const values = [region, district, address]
|
||
.map(value => String(value || '').trim())
|
||
.filter(Boolean);
|
||
if (!values.length) return '';
|
||
return values.reduce((result, value) => {
|
||
if (!result) return value;
|
||
if (result.includes(value) || value.includes(result))
|
||
return result.length >= value.length ? result : value;
|
||
return `${result}${value}`;
|
||
}, '');
|
||
},
|
||
routeProvinceName(row, prefix) {
|
||
return (
|
||
[
|
||
row[`${prefix}ProvinceName`],
|
||
row[`${prefix}Province`],
|
||
row[`${prefix}RegionName`],
|
||
row[`${prefix}Region`],
|
||
row[`${prefix}Name`],
|
||
row[`${prefix}AdministrativeRegionName`],
|
||
row[`${prefix}AddressRegionName`],
|
||
].find(value => String(value || '').trim()) || ''
|
||
);
|
||
},
|
||
formatProvinceCityDistrict(value) {
|
||
const text = String(value || '').trim();
|
||
if (!text) return '';
|
||
const districtMatch = text.match(/(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗)/);
|
||
if (districtMatch) return text.slice(0, districtMatch.index + districtMatch[0].length);
|
||
const cityMatch = text.match(/市/);
|
||
return cityMatch ? text.slice(0, cityMatch.index + 1) : text;
|
||
},
|
||
rowActions(row) {
|
||
const status = row.businessStatus;
|
||
if (status === 'draft') {
|
||
return [
|
||
{ type: 'edit', label: '编辑' },
|
||
{ type: 'delete', label: '删除' },
|
||
];
|
||
}
|
||
if (status === 'pending') {
|
||
const actions = [
|
||
{ type: 'view', label: '详情' },
|
||
{ type: 'start', label: '改为进行中' },
|
||
];
|
||
if (row.driverRejected) {
|
||
actions.push({ type: 'reassign', label: '重新派单' });
|
||
}
|
||
actions.push({ type: 'edit', label: '编辑' }, { type: 'cancel', label: '取消' });
|
||
return actions;
|
||
}
|
||
if (status === 'running') {
|
||
return [
|
||
{ type: 'view', label: '详情' },
|
||
{ type: 'edit', label: '编辑' },
|
||
{ type: 'complete', label: '完成' },
|
||
];
|
||
}
|
||
if (status === 'completed' || status === 'cancelled') {
|
||
return [{ type: 'view', label: '详情' }];
|
||
}
|
||
return [{ type: 'view', label: '详情' }];
|
||
},
|
||
handleRowAction(type, row) {
|
||
const actionMap = {
|
||
view: () => this.gotoLoadingDetail(row),
|
||
edit: () => this.openLoadingDialog('edit', row),
|
||
reassign: () => this.gotoLoadingReassign(row),
|
||
delete: () => this.deleteLoading(row),
|
||
start: () => this.startLoading(row),
|
||
cancel: () => this.cancelLoading(row),
|
||
complete: () => this.completeLoading(row),
|
||
};
|
||
actionMap[type]?.();
|
||
},
|
||
// 详情改为独立页面(与新增/编辑一致)
|
||
gotoLoadingDetail(row = {}) {
|
||
const id = row.id || row.loadingId || row.loadingManageId;
|
||
if (!id) {
|
||
ElMessage.info('当前配载单暂无详情数据');
|
||
return;
|
||
}
|
||
if (this.isStandaloneDetailPage && String(this.$route.query.id || '') === String(id)) return;
|
||
this.$router.push({
|
||
path: '/business/loading-manage/detail',
|
||
query: { id, name: '配载单详情' },
|
||
});
|
||
},
|
||
// 复制:进入新增页并带回原配载数据
|
||
gotoLoadingCopy(row = {}) {
|
||
const id = row.id || row.loadingId || row.loadingManageId;
|
||
if (!id) {
|
||
ElMessage.warning('配载单不存在,无法复制');
|
||
return;
|
||
}
|
||
this.$router.push({
|
||
path: '/business/loading-manage/form',
|
||
query: { mode: 'add', copyId: id, name: '新增配载管理' },
|
||
});
|
||
},
|
||
// 重新派单改为独立页面
|
||
gotoLoadingReassign(row = {}) {
|
||
if (!row.id) {
|
||
ElMessage.warning('配载单不存在,无法重新派单');
|
||
return;
|
||
}
|
||
this.$router.push({
|
||
path: '/business/loading-manage/form',
|
||
query: { mode: 'reassign', id: row.id, name: '重新派单' },
|
||
});
|
||
},
|
||
async initStandaloneDetailPage() {
|
||
if (!this.isStandaloneDetailPage) return;
|
||
const id = this.$route.query.id;
|
||
if (!id) {
|
||
ElMessage.warning('缺少配载单参数,无法查看详情');
|
||
this.closeLoadingDialog();
|
||
return;
|
||
}
|
||
await this.openLoadingDialog('view', { id });
|
||
},
|
||
async initStandaloneFormPage() {
|
||
if (!this.isStandaloneFormPage) return;
|
||
const queryMode = this.$route.query.mode;
|
||
const mode = ['edit', 'reassign'].includes(queryMode) ? queryMode : 'add';
|
||
const copyId = mode === 'add' ? this.$route.query.copyId : '';
|
||
await this.openLoadingDialog(mode, {
|
||
id: copyId || this.$route.query.id,
|
||
copy: Boolean(copyId),
|
||
});
|
||
if (mode === 'add' && !copyId) await this.consumeLoadingCreateWaybills();
|
||
},
|
||
async consumeLoadingCreateWaybills() {
|
||
const raw = sessionStorage.getItem(loadingCreateWaybillsStorageKey);
|
||
if (!raw) return;
|
||
sessionStorage.removeItem(loadingCreateWaybillsStorageKey);
|
||
try {
|
||
const payload = JSON.parse(raw);
|
||
if (payload?.source !== 'waybill-manage' || !Array.isArray(payload.rows)) return;
|
||
await this.addWaybillRows(payload.rows);
|
||
} catch (error) {
|
||
ElMessage.warning('待配载运单数据读取失败,请重新选择');
|
||
}
|
||
},
|
||
async openLoadingDialog(mode, row) {
|
||
// 列表页进入新增/编辑/重新派单:统一走独立页面
|
||
if (['add', 'edit', 'reassign'].includes(mode) && !this.isStandaloneFormPage) {
|
||
if (mode === 'reassign') {
|
||
this.gotoLoadingReassign(row || {});
|
||
return;
|
||
}
|
||
this.$router.push({
|
||
path: '/business/loading-manage/form',
|
||
query: {
|
||
mode,
|
||
...(mode === 'edit' && row?.id ? { id: row.id } : {}),
|
||
name:
|
||
mode === 'edit'
|
||
? row?.businessStatus === 'running'
|
||
? '编辑进行中配载'
|
||
: '编辑配载管理'
|
||
: '新增配载管理',
|
||
},
|
||
});
|
||
return;
|
||
}
|
||
this.dialogMode = mode;
|
||
this.dialogLoading = true;
|
||
this.resetDialogData();
|
||
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.applyLoadingTaskInfoFromDetail(this.dialogForm);
|
||
this.applyLoadingMileageFromDetail(this.dialogForm);
|
||
await this.restoreWaybillRows(
|
||
this.dialogForm.waybillIdsJson,
|
||
this.dialogForm.loadingSubNos
|
||
);
|
||
this.syncProcessWaybillOptions();
|
||
if (this.isRunningEdit) {
|
||
await this.refreshWaybillPunchMap();
|
||
}
|
||
this.restoreRouteAndCargo();
|
||
this.restoreRouteChangeRecords();
|
||
this.loadPunchRecords();
|
||
this.probeHasRelatedVoucher();
|
||
this.$nextTick(() => this.initDetailAmap());
|
||
await this.loadCandidateWaybills();
|
||
} else {
|
||
this.dialogForm = createDialogForm();
|
||
this.candidateQuery = createCandidateSearchForm();
|
||
await this.loadCandidateWaybills();
|
||
}
|
||
} finally {
|
||
this.dialogLoading = false;
|
||
}
|
||
},
|
||
closeLoadingDialog() {
|
||
if (!this.isStandalonePage) return;
|
||
this.$router.$avueRouter?.closeTag?.();
|
||
this.$router.push({ path: '/business/loading-manage', query: {} });
|
||
},
|
||
resetDialogData() {
|
||
this.detailAmap?.destroy?.();
|
||
this.detailAmap = null;
|
||
this.dialogForm = createDialogForm();
|
||
this.candidateQuery = createCandidateSearchForm();
|
||
this.candidateRows = [];
|
||
this.candidateSelection = [];
|
||
this.selectedWaybillRows = [];
|
||
this.taskCarrierRequestId += 1;
|
||
this.taskCarrierOptions = [];
|
||
this.taskCarrierLoading = false;
|
||
this.routeNodes = [];
|
||
this.cargoRows = [];
|
||
this.routeDragIndex = -1;
|
||
this.waybillPunchMap = {};
|
||
this.candidatePage.currentPage = 1;
|
||
this.candidatePage.total = 0;
|
||
this.candidateSearchVisible = false;
|
||
this.processDetailTab = 'punch';
|
||
this.processWaybillId = '';
|
||
this.processWaybillOptions = [];
|
||
this.punchRecords = [];
|
||
this.driverUploads = [];
|
||
this.hasRelatedVoucher = false;
|
||
this.voucherImages = [];
|
||
this.voucherFolders = [];
|
||
this.voucherFolder = null;
|
||
this.voucherFolderView = false;
|
||
this.voucherImagesLoading = false;
|
||
this.voucherImagesLoaded = false;
|
||
this.attachmentImagePreviewVisible = false;
|
||
this.attachmentImagePreviewUrls = [];
|
||
this.attachmentImagePreviewIndex = 0;
|
||
},
|
||
// 离开本页(标签缓存/卸载)时收起页内子弹窗,避免 append-to-body 的弹窗 DOM 残留在 body 上
|
||
closeInnerDialogs() {
|
||
this.routeChangeVisible = false;
|
||
this.routeChangeMapVisible = false;
|
||
this.candidateSearchVisible = false;
|
||
},
|
||
restoreRouteAndCargo() {
|
||
const routeRows = parseJsonArray(this.dialogForm.routeJson);
|
||
const cargoRows = parseJsonArray(this.dialogForm.goodsJson);
|
||
this.routeNodes = routeRows.length
|
||
? this.syncRouteNodeLockedFlags(routeRows, this.selectedWaybillRows)
|
||
: this.buildRouteNodes(this.selectedWaybillRows);
|
||
this.cargoRows = (cargoRows.length ? cargoRows : this.buildCargoRows(this.selectedWaybillRows)).map(
|
||
row => sanitizeCargoRow(row)
|
||
);
|
||
// 运单回填时 routeNodes 尚为空会清空发到货地,路线恢复后需再同步
|
||
this.syncRouteAddressFields();
|
||
},
|
||
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 = [];
|
||
}
|
||
},
|
||
syncProcessWaybillOptions() {
|
||
this.processWaybillOptions = (this.selectedWaybillRows || [])
|
||
.filter(row => row?.id)
|
||
.map(row => ({
|
||
id: row.id,
|
||
waybillNo: row.waybillNo || String(row.id),
|
||
}));
|
||
const currentExists = this.processWaybillOptions.some(
|
||
item => String(item.id) === String(this.processWaybillId)
|
||
);
|
||
this.processWaybillId = currentExists
|
||
? this.processWaybillId
|
||
: this.processWaybillOptions[0]?.id || '';
|
||
this.processDetailTab = 'punch';
|
||
this.resetVoucherState();
|
||
},
|
||
resetVoucherState() {
|
||
this.hasRelatedVoucher = false;
|
||
this.voucherImages = [];
|
||
this.voucherFolders = [];
|
||
this.voucherFolder = null;
|
||
this.voucherFolderView = false;
|
||
this.voucherImagesLoading = false;
|
||
this.voucherImagesLoaded = false;
|
||
},
|
||
handleProcessWaybillChange() {
|
||
this.processDetailTab = 'punch';
|
||
this.resetVoucherState();
|
||
this.loadPunchRecords();
|
||
this.probeHasRelatedVoucher();
|
||
},
|
||
handleProcessDetailTabChange(tab) {
|
||
if (tab === 'punch' || tab === 'driverUpload') this.loadPunchRecords();
|
||
if (tab === 'batchSupplement') this.loadVoucherImages();
|
||
},
|
||
probeHasRelatedVoucher() {
|
||
const waybillId = this.processWaybillId;
|
||
const waybill = (this.selectedWaybillRows || []).find(
|
||
row => String(row.id) === String(waybillId)
|
||
);
|
||
const projectId = waybill?.projectId;
|
||
if (!waybillId || !projectId) {
|
||
this.hasRelatedVoucher = false;
|
||
return;
|
||
}
|
||
getProcessConfigList(1, 100, { projectIds: projectId, status: 1 })
|
||
.then(res => {
|
||
const config = unwrapRecords(res).find(row => {
|
||
const projectIds = String(row.projectIds || '')
|
||
.split(',')
|
||
.map(item => item.trim());
|
||
return projectIds.includes(String(projectId));
|
||
});
|
||
if (!config?.id) {
|
||
this.hasRelatedVoucher = false;
|
||
return null;
|
||
}
|
||
return getProcessConfigDetail(config.id, waybillId);
|
||
})
|
||
.then(detailRes => {
|
||
if (!detailRes) return;
|
||
const detail = detailRes?.data?.data || detailRes?.data || {};
|
||
this.hasRelatedVoucher = Boolean(detail.hasRelatedVoucher);
|
||
})
|
||
.catch(() => {
|
||
this.hasRelatedVoucher = false;
|
||
});
|
||
},
|
||
punchRecordTimestamp(record = {}) {
|
||
if (record.punched === false || record.statusName === '未打卡') return null;
|
||
const raw = record.punchTime || record.punchAt || record.punchDateTime || '';
|
||
const text = String(raw || '').trim();
|
||
if (!text) return null;
|
||
const parsed = dayjs(text);
|
||
if (parsed.isValid()) return parsed.valueOf();
|
||
const fallback = new Date(text.replace(/-/g, '/')).getTime();
|
||
return Number.isNaN(fallback) ? null : fallback;
|
||
},
|
||
loadPunchRecords() {
|
||
const waybillId = this.processWaybillId;
|
||
if (!waybillId || this.punchRecordsLoading) return;
|
||
this.punchRecordsLoading = true;
|
||
getWaybillPunchRecords(waybillId)
|
||
.then(res => {
|
||
const data = res?.data?.data || res?.data || {};
|
||
this.punchRecords = Array.isArray(data.records) ? data.records : [];
|
||
this.driverUploads = Array.isArray(data.driverUploads) ? data.driverUploads : [];
|
||
if (data.hasRelatedVoucher) this.hasRelatedVoucher = true;
|
||
})
|
||
.catch(() => {
|
||
this.punchRecords = [];
|
||
this.driverUploads = [];
|
||
})
|
||
.finally(() => {
|
||
this.punchRecordsLoading = false;
|
||
});
|
||
},
|
||
previewDriverUploadPhoto(photos, index) {
|
||
const list = (photos || []).map(item => item?.url).filter(Boolean);
|
||
if (!list.length) return;
|
||
this.attachmentImagePreviewUrls = list;
|
||
this.attachmentImagePreviewIndex = Math.min(
|
||
Math.max(Number(index) || 0, 0),
|
||
list.length - 1
|
||
);
|
||
this.attachmentImagePreviewVisible = true;
|
||
},
|
||
loadVoucherImages() {
|
||
if (!this.processWaybillId || this.voucherImagesLoading || this.voucherImagesLoaded) {
|
||
return;
|
||
}
|
||
this.voucherImagesLoading = true;
|
||
getProcessConfigVoucherImages(this.processWaybillId)
|
||
.then(res => {
|
||
this.voucherImages = unwrapRecords(res);
|
||
this.voucherFolders = this.voucherImages.filter(item => this.isVoucherFolder(item));
|
||
this.voucherFolder = null;
|
||
this.voucherFolderView = false;
|
||
if (this.voucherImages.length) this.hasRelatedVoucher = true;
|
||
this.voucherImagesLoaded = true;
|
||
})
|
||
.finally(() => {
|
||
this.voucherImagesLoading = false;
|
||
});
|
||
},
|
||
isVoucherFolder(item) {
|
||
return Boolean(item && (item.isFolder === true || item.type === 'folder'));
|
||
},
|
||
voucherFolderName(folder) {
|
||
return folder?.folderName || folder?.name || folder?.plateNo || '凭证文件夹';
|
||
},
|
||
openVoucherFolder(folder) {
|
||
if (!this.processWaybillId || !this.isVoucherFolder(folder)) return;
|
||
if (!folder.voucherId || !folder.folderName) {
|
||
ElMessage.warning('凭证文件夹信息不完整');
|
||
return;
|
||
}
|
||
this.voucherImagesLoading = true;
|
||
getProcessConfigVoucherImages(this.processWaybillId, {
|
||
voucherId: folder.voucherId,
|
||
folderName: folder.folderName,
|
||
})
|
||
.then(res => {
|
||
this.voucherFolder = folder;
|
||
this.voucherFolderView = true;
|
||
this.voucherImages = unwrapRecords(res).filter(item => !this.isVoucherFolder(item));
|
||
})
|
||
.finally(() => {
|
||
this.voucherImagesLoading = false;
|
||
});
|
||
},
|
||
backToVoucherFolders() {
|
||
this.voucherFolder = null;
|
||
this.voucherFolderView = false;
|
||
this.voucherImages = this.voucherFolders;
|
||
},
|
||
previewVoucherImage(index) {
|
||
const list = (this.voucherImages || [])
|
||
.filter(item => !this.isVoucherFolder(item))
|
||
.map(item => item?.url)
|
||
.filter(Boolean);
|
||
if (!list.length) return;
|
||
const current = this.voucherImages[index];
|
||
const currentUrl = current?.url;
|
||
const currentIndex = Math.max(
|
||
0,
|
||
list.findIndex(url => url === currentUrl)
|
||
);
|
||
this.attachmentImagePreviewUrls = list;
|
||
this.attachmentImagePreviewIndex = currentIndex >= 0 ? currentIndex : 0;
|
||
this.attachmentImagePreviewVisible = true;
|
||
},
|
||
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' ? '暂无可回放的物流轨迹' : '暂无车辆实时定位数据');
|
||
},
|
||
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 || '',
|
||
}));
|
||
const builtNodes = this.buildRouteNodes(this.routeChangeRows);
|
||
this.routeChangeNodes = this.routeNodes.length
|
||
? this.syncRouteNodeLockedFlags(
|
||
this.routeNodes.map(node => ({
|
||
...node,
|
||
tags: [...(node.tags || [])],
|
||
})),
|
||
this.routeChangeRows
|
||
)
|
||
: builtNodes;
|
||
this.routeChangeOriginalAddresses = this.routeChangeNodes.map(node => node.address);
|
||
this.routeChangeOrderDirty = false;
|
||
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) {
|
||
if (this.isRouteChangeNodeLocked(this.routeChangeNodes[index])) {
|
||
this.routeChangeDragIndex = -1;
|
||
return;
|
||
}
|
||
this.routeChangeDragIndex = index;
|
||
},
|
||
handleRouteChangeDrop(index) {
|
||
if (this.routeChangeDragIndex < 0 || this.routeChangeDragIndex === index) return;
|
||
if (
|
||
this.isRouteChangeNodeLocked(this.routeChangeNodes[this.routeChangeDragIndex]) ||
|
||
this.isRouteChangeNodeLocked(this.routeChangeNodes[index])
|
||
) {
|
||
this.routeChangeDragIndex = -1;
|
||
return;
|
||
}
|
||
const nodes = [...this.routeChangeNodes];
|
||
const [node] = nodes.splice(this.routeChangeDragIndex, 1);
|
||
nodes.splice(index, 0, node);
|
||
const originalLockedKeys = this.routeChangeNodes.map(item =>
|
||
this.isRouteChangeNodeLocked(item) ? item.key : null
|
||
);
|
||
const nextLockedKeys = nodes.map(item =>
|
||
this.isRouteChangeNodeLocked(item) ? item.key : null
|
||
);
|
||
if (
|
||
originalLockedKeys.some((key, lockedIndex) => key && key !== nextLockedKeys[lockedIndex])
|
||
) {
|
||
ElMessage.warning('已完成运单对应的路线不可调整');
|
||
this.routeChangeDragIndex = -1;
|
||
return;
|
||
}
|
||
this.routeChangeNodes = nodes;
|
||
this.routeChangeOrderDirty = true;
|
||
this.routeChangeDragIndex = -1;
|
||
},
|
||
updateRouteChangeNodes() {
|
||
const previousAddresses = this.routeChangeNodes.map(node => node.address);
|
||
const builtNodes = this.buildRouteNodes(this.routeChangeRows);
|
||
const remaining = [...builtNodes];
|
||
const orderedNodes = [];
|
||
previousAddresses.forEach(address => {
|
||
const index = remaining.findIndex(node => node.address === address);
|
||
if (index >= 0) {
|
||
orderedNodes.push(remaining.splice(index, 1)[0]);
|
||
}
|
||
});
|
||
this.routeChangeNodes = [...orderedNodes, ...remaining];
|
||
},
|
||
isRouteChangeOrderChanged() {
|
||
if (!this.routeChangeOrderDirty) return false;
|
||
const currentAddresses = this.routeChangeNodes.map(node => node.address);
|
||
if (currentAddresses.length !== this.routeChangeOriginalAddresses.length) return true;
|
||
return currentAddresses.some(
|
||
(address, index) => address !== this.routeChangeOriginalAddresses[index]
|
||
);
|
||
},
|
||
formatRouteChangeOrderText(addresses = []) {
|
||
return addresses.filter(Boolean).join(' → ') || '-';
|
||
},
|
||
isWaybillCompleted(row = {}) {
|
||
const status = String(row.businessStatus || row.status || row.waybillStatus || '');
|
||
if (status === 'completed') return true;
|
||
const statusName = String(row.businessStatusName || row.statusName || '');
|
||
return statusName.includes('已完成');
|
||
},
|
||
isPunchRecordDone(record = {}) {
|
||
if (record.punched === false || record.statusName === '未打卡') return false;
|
||
return Boolean(record.punched || record.punchTime || record.punchAt || record.punchDateTime);
|
||
},
|
||
isLoadPunchRecord(record = {}) {
|
||
if (!this.isPunchRecordDone(record)) return false;
|
||
const code = String(record.nodeCode || '')
|
||
.trim()
|
||
.toLowerCase();
|
||
const name = String(record.nodeName || '').trim();
|
||
return code === 'load' || name.includes('装货');
|
||
},
|
||
isUnloadPunchRecord(record = {}) {
|
||
if (!this.isPunchRecordDone(record)) return false;
|
||
const code = String(record.nodeCode || '')
|
||
.trim()
|
||
.toLowerCase();
|
||
const name = String(record.nodeName || '').trim();
|
||
return (
|
||
code === 'unload' ||
|
||
name.includes('卸货') ||
|
||
code === 'arrival' ||
|
||
name.includes('到货') ||
|
||
code === 'sign' ||
|
||
name.includes('签收')
|
||
);
|
||
},
|
||
waybillPunchRecords(row = {}) {
|
||
const key = String(row?.id || '');
|
||
if (!key) return [];
|
||
const records = this.waybillPunchMap[key];
|
||
return Array.isArray(records) ? records : [];
|
||
},
|
||
isWaybillLoadPunched(row = {}) {
|
||
return this.waybillPunchRecords(row).some(record => this.isLoadPunchRecord(record));
|
||
},
|
||
isWaybillUnloadPunched(row = {}) {
|
||
return this.waybillPunchRecords(row).some(record => this.isUnloadPunchRecord(record));
|
||
},
|
||
isWaybillPunchLocked(row = {}) {
|
||
return this.isWaybillLoadPunched(row) || this.isWaybillUnloadPunched(row);
|
||
},
|
||
isSelectedWaybillRemoveLocked(row = {}) {
|
||
if (this.isWaybillCompleted(row)) return true;
|
||
return this.isRunningEdit && this.isWaybillPunchLocked(row);
|
||
},
|
||
isRouteNodeTagLocked(tag, rows = this.selectedWaybillRows) {
|
||
const match = String(tag || '').match(/^(装|卸)(\d+)$/);
|
||
if (!match) return false;
|
||
const row = rows[Number(match[2]) - 1];
|
||
if (!row) return false;
|
||
if (this.isRunningEdit) {
|
||
return match[1] === '装' ? this.isWaybillLoadPunched(row) : this.isWaybillUnloadPunched(row);
|
||
}
|
||
return this.isWaybillCompleted(row);
|
||
},
|
||
isRouteChangeNodeLocked(node = {}) {
|
||
return Boolean(node?.locked);
|
||
},
|
||
syncRouteNodeLockedFlags(nodes = [], rows = this.selectedWaybillRows) {
|
||
return (nodes || []).map(node => {
|
||
const locked = (node.tags || []).some(tag => this.isRouteNodeTagLocked(tag, rows));
|
||
return { ...node, locked };
|
||
});
|
||
},
|
||
async refreshWaybillPunchMap() {
|
||
const rows = (this.selectedWaybillRows || []).filter(row => row?.id);
|
||
if (!rows.length) {
|
||
this.waybillPunchMap = {};
|
||
return;
|
||
}
|
||
const entries = await Promise.all(
|
||
rows.map(async row => {
|
||
try {
|
||
const res = await getWaybillPunchRecords(row.id);
|
||
const data = res?.data?.data || res?.data || {};
|
||
return [String(row.id), Array.isArray(data.records) ? data.records : []];
|
||
} catch (error) {
|
||
return [String(row.id), []];
|
||
}
|
||
})
|
||
);
|
||
this.waybillPunchMap = Object.fromEntries(entries);
|
||
},
|
||
/**
|
||
* 进行中编辑:已打卡地点保持在前且不可插入,新增地址只能排在已打卡节点之后。
|
||
*/
|
||
mergeRouteNodesKeepingPunched(prevNodes = [], builtNodes = []) {
|
||
const prev = Array.isArray(prevNodes) ? prevNodes : [];
|
||
const built = Array.isArray(builtNodes) ? builtNodes : [];
|
||
if (!this.isRunningEdit) {
|
||
return built;
|
||
}
|
||
const builtByAddress = new Map();
|
||
built.forEach(node => {
|
||
if (node?.address && !builtByAddress.has(node.address)) {
|
||
builtByAddress.set(node.address, node);
|
||
}
|
||
});
|
||
const lockedAddresses = new Set(
|
||
built.filter(node => node?.locked && node.address).map(node => node.address)
|
||
);
|
||
const used = new Set();
|
||
const lockedResult = [];
|
||
prev.forEach(node => {
|
||
if (!node?.address || !lockedAddresses.has(node.address) || used.has(node.address)) {
|
||
return;
|
||
}
|
||
const latest = builtByAddress.get(node.address);
|
||
if (!latest) return;
|
||
lockedResult.push({ ...latest, locked: true });
|
||
used.add(node.address);
|
||
});
|
||
built.forEach(node => {
|
||
if (!node?.locked || !node.address || used.has(node.address)) return;
|
||
lockedResult.push({ ...node, locked: true });
|
||
used.add(node.address);
|
||
});
|
||
const unlockedResult = [];
|
||
prev.forEach(node => {
|
||
if (!node?.address || lockedAddresses.has(node.address) || used.has(node.address)) {
|
||
return;
|
||
}
|
||
const latest = builtByAddress.get(node.address);
|
||
if (!latest) return;
|
||
unlockedResult.push({ ...latest, locked: false });
|
||
used.add(node.address);
|
||
});
|
||
built.forEach(node => {
|
||
if (node?.locked || !node?.address || used.has(node.address)) return;
|
||
unlockedResult.push({ ...node, locked: false });
|
||
used.add(node.address);
|
||
});
|
||
return [...lockedResult, ...unlockedResult].map((node, index) => ({
|
||
...node,
|
||
key: `${node.address}-${index}`,
|
||
}));
|
||
},
|
||
openRouteChangeMapDialog(row, field) {
|
||
if (this.isWaybillCompleted(row)) return;
|
||
this.routeChangeMapTarget = { row, field };
|
||
this.routeChangeMapKeyword = row[`${field}Address`] || row[`${field}Name`] || '';
|
||
this.routeChangeMapSelected = {
|
||
longitude: row[`${field}Longitude`] || '',
|
||
latitude: row[`${field}Latitude`] || '',
|
||
detailAddress: this.routeChangeMapKeyword,
|
||
regionName: row[`${field}Name`] || '',
|
||
regionCode: row[`${field}RegionCode`] || '',
|
||
};
|
||
this.routeChangeMapSearchResults = [];
|
||
this.routeChangeMapStatus = this.routeChangeMapSelected.longitude
|
||
? '已加载当前选点,可重新选点'
|
||
: '可搜索地址或点击地图选点';
|
||
this.routeChangeMapVisible = true;
|
||
},
|
||
initRouteChangeMap() {
|
||
this.loadDetailAmap()
|
||
.then(() =>
|
||
this.$nextTick(() => {
|
||
if (!this.$refs.routeChangeMap) return;
|
||
const center = this.routeChangeMapSelected.longitude
|
||
? [
|
||
Number(this.routeChangeMapSelected.longitude),
|
||
Number(this.routeChangeMapSelected.latitude),
|
||
]
|
||
: [116.40769, 39.89945];
|
||
if (!this.routeChangeMapInstance) {
|
||
this.routeChangeMapInstance = new window.AMap.Map(this.$refs.routeChangeMap, {
|
||
center,
|
||
zoom: this.routeChangeMapSelected.longitude ? 14 : 11,
|
||
});
|
||
window.AMap.plugin(['AMap.Geocoder'], () => {
|
||
this.routeChangeMapGeocoder = new window.AMap.Geocoder();
|
||
});
|
||
this.routeChangeMapInstance.on('click', event =>
|
||
this.pickRouteChangeMap(event.lnglat)
|
||
);
|
||
} else {
|
||
this.routeChangeMapInstance.resize();
|
||
}
|
||
if (this.routeChangeMapSelected.longitude) {
|
||
this.renderRouteChangeMapMarker(center);
|
||
}
|
||
})
|
||
)
|
||
.catch(() => ElMessage.error('高德地图组件加载失败'));
|
||
},
|
||
destroyRouteChangeMap() {
|
||
this.routeChangeMapMarker?.setMap?.(null);
|
||
this.routeChangeMapMarker = null;
|
||
this.routeChangeMapInstance?.destroy?.();
|
||
this.routeChangeMapInstance = null;
|
||
this.routeChangeMapGeocoder = null;
|
||
this.routeChangeMapSearchResults = [];
|
||
this.routeChangeMapTarget = null;
|
||
},
|
||
ensureRouteChangeMapGeocoder() {
|
||
if (this.routeChangeMapGeocoder) return Promise.resolve(this.routeChangeMapGeocoder);
|
||
return new Promise(resolve => {
|
||
window.AMap.plugin(['AMap.Geocoder'], () => {
|
||
this.routeChangeMapGeocoder = new window.AMap.Geocoder();
|
||
resolve(this.routeChangeMapGeocoder);
|
||
});
|
||
});
|
||
},
|
||
searchRouteChangeMap() {
|
||
const keyword = String(this.routeChangeMapKeyword || '').trim();
|
||
if (!keyword) {
|
||
ElMessage.warning('请输入地址关键词');
|
||
return;
|
||
}
|
||
if (this.routeChangeMapLoading) return;
|
||
this.routeChangeMapSearchResults = [];
|
||
this.routeChangeMapLoading = true;
|
||
this.loadDetailAmap()
|
||
.then(() => searchAmapPlaceText(keyword))
|
||
.then(pois => {
|
||
this.routeChangeMapSearchResults = normalizeMapSearchResults(pois, keyword);
|
||
const first = this.routeChangeMapSearchResults[0];
|
||
if (!first?.location) {
|
||
this.routeChangeMapStatus = '未找到匹配地址';
|
||
ElMessage.warning('地图搜索无匹配地址');
|
||
return;
|
||
}
|
||
return this.$nextTick().then(() => {
|
||
this.pickRouteChangeMap(first.location, first.address || first.name || keyword);
|
||
});
|
||
})
|
||
.catch(() => {
|
||
this.routeChangeMapStatus = '未找到匹配地址';
|
||
ElMessage.warning('地图搜索无匹配地址');
|
||
})
|
||
.finally(() => {
|
||
this.routeChangeMapLoading = false;
|
||
});
|
||
},
|
||
selectRouteChangeMapSearchResult(item) {
|
||
if (item?.location) {
|
||
this.pickRouteChangeMap(item.location, item.address || item.name || '');
|
||
}
|
||
},
|
||
pickRouteChangeMap(point, keyword) {
|
||
const longitude = point.getLng ? point.getLng() : point.lng;
|
||
const latitude = point.getLat ? point.getLat() : point.lat;
|
||
this.routeChangeMapSelected = {
|
||
longitude,
|
||
latitude,
|
||
detailAddress: keyword || this.routeChangeMapKeyword,
|
||
regionName: '',
|
||
regionCode: '',
|
||
};
|
||
this.renderRouteChangeMapMarker([longitude, latitude]);
|
||
this.routeChangeMapStatus = '已选点,可确认回填';
|
||
this.ensureRouteChangeMapGeocoder().then(() => {
|
||
this.routeChangeMapGeocoder.getAddress([longitude, latitude], (status, result) => {
|
||
if (status !== 'complete') return;
|
||
const component = result.regeocode?.addressComponent || {};
|
||
this.routeChangeMapSelected = {
|
||
...this.routeChangeMapSelected,
|
||
detailAddress:
|
||
result.regeocode?.formattedAddress || this.routeChangeMapSelected.detailAddress,
|
||
regionName: [component.province, component.city, component.district]
|
||
.filter(Boolean)
|
||
.join(''),
|
||
regionCode: component.adcode || '',
|
||
};
|
||
this.routeChangeMapKeyword = this.routeChangeMapSelected.detailAddress;
|
||
this.routeChangeMapStatus = '已解析地址,可确认回填';
|
||
});
|
||
});
|
||
},
|
||
renderRouteChangeMapMarker(position) {
|
||
if (!this.routeChangeMapInstance) return;
|
||
this.routeChangeMapMarker?.setMap?.(null);
|
||
this.routeChangeMapMarker = new window.AMap.Marker({ position });
|
||
this.routeChangeMapMarker.setMap(this.routeChangeMapInstance);
|
||
this.routeChangeMapInstance.setCenter(position);
|
||
},
|
||
confirmRouteChangeMap() {
|
||
const target = this.routeChangeMapTarget;
|
||
if (!target || !this.routeChangeMapSelected.longitude) return;
|
||
const { row, field } = target;
|
||
row[`${field}Address`] =
|
||
this.routeChangeMapSelected.detailAddress || this.routeChangeMapKeyword || '';
|
||
if (this.routeChangeMapSelected.regionName) {
|
||
row[`${field}Name`] = this.routeChangeMapSelected.regionName;
|
||
}
|
||
row[`${field}Longitude`] = this.routeChangeMapSelected.longitude;
|
||
row[`${field}Latitude`] = this.routeChangeMapSelected.latitude;
|
||
if (this.routeChangeMapSelected.regionCode) {
|
||
row[`${field}RegionCode`] = this.routeChangeMapSelected.regionCode;
|
||
}
|
||
this.updateRouteChangeNodes();
|
||
this.routeChangeMapVisible = false;
|
||
},
|
||
async confirmRouteChange() {
|
||
const changeRows = this.routeChangeRows.filter(
|
||
row =>
|
||
row.departureAddress !== row.originalDepartureAddress ||
|
||
row.arrivalAddress !== row.originalArrivalAddress
|
||
);
|
||
const orderChanged = this.isRouteChangeOrderChanged();
|
||
if (!changeRows.length && !orderChanged && !this.routeChangeRemark) {
|
||
ElMessage.warning('请修改地址、调整路线顺序或填写变更备注');
|
||
return;
|
||
}
|
||
const contentParts = [];
|
||
if (changeRows.length) {
|
||
contentParts.push(
|
||
changeRows
|
||
.map(
|
||
row =>
|
||
`${row.waybillNo || '运单'}:发货地 ${row.originalDepartureAddress} → ${
|
||
row.departureAddress
|
||
};到货地 ${row.originalArrivalAddress} → ${row.arrivalAddress}`
|
||
)
|
||
.join(';')
|
||
);
|
||
}
|
||
if (orderChanged) {
|
||
contentParts.push(
|
||
`路线顺序:${this.formatRouteChangeOrderText(
|
||
this.routeChangeOriginalAddresses
|
||
)} 调整为 ${this.formatRouteChangeOrderText(
|
||
this.routeChangeNodes.map(node => node.address)
|
||
)}`
|
||
);
|
||
}
|
||
const content = contentParts.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 selectedProjectId = this.selectedWaybillRows.map(row => row.projectId).find(Boolean);
|
||
const query = {
|
||
...this.buildQueryParams(this.candidateQuery),
|
||
// 公路、待执行、未配载的判定都在后端可配载开关内
|
||
// 该开关已把候选限定为公路,再按精确运输方式取值筛会漏掉「公路运输」这类历史写法
|
||
loadableOnly: 1,
|
||
// allDept=0 表示不指定组织:超管与「数据权限范围=全部组织」的账户后端不过滤组织,
|
||
// 其余账户限定本组织(不展开下级)
|
||
allDept: 0,
|
||
};
|
||
// 已选运单后,候选列表优先限定同一项目,便于再按承运商合同乙方交集筛选
|
||
if (selectedProjectId && !query.projectId && !query.projectName) {
|
||
query.projectId = selectedProjectId;
|
||
}
|
||
const res = await getWaybillList(
|
||
this.candidatePage.currentPage,
|
||
this.candidatePage.pageSize,
|
||
query
|
||
);
|
||
const page = unwrapPage(res);
|
||
this.candidateRows = page.records;
|
||
this.candidatePage.total = page.total;
|
||
} finally {
|
||
this.candidateLoading = false;
|
||
}
|
||
},
|
||
handleCandidateSearch() {
|
||
this.candidatePage.currentPage = 1;
|
||
this.loadCandidateWaybills();
|
||
},
|
||
handleCandidateReset() {
|
||
this.candidateQuery = createCandidateSearchForm();
|
||
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;
|
||
}
|
||
})
|
||
);
|
||
if (this.isRunningEdit && String(this.dialogForm.carrierType || '').trim() === '承运商') {
|
||
const contractId = this.dialogForm.carrierContractId;
|
||
if (contractId) {
|
||
const projectIds = [
|
||
...new Set(
|
||
[...this.selectedWaybillRows, ...selectedRows]
|
||
.map(row => row.projectId)
|
||
.filter(Boolean)
|
||
),
|
||
];
|
||
if (projectIds.length) {
|
||
try {
|
||
const waybillIds = [...this.selectedWaybillRows, ...selectedRows]
|
||
.map(row => row.id)
|
||
.filter(Boolean);
|
||
const response = await loadingApi.getCarrierContracts(projectIds, waybillIds);
|
||
const contracts = response?.data?.data || response?.data || [];
|
||
const stillAllowed = (Array.isArray(contracts) ? contracts : []).some(
|
||
item => String(item.id) === String(contractId)
|
||
);
|
||
if (!stillAllowed) {
|
||
ElMessage.warning('新增子运单须兼容当前配载单已选定的承运商合同');
|
||
return;
|
||
}
|
||
} catch (error) {
|
||
// 合同校验失败时仍允许添加,最终由后端确认拦截
|
||
}
|
||
}
|
||
}
|
||
}
|
||
const exists = new Set(
|
||
this.selectedWaybillRows.map(row => String(row.id || row.waybillNo))
|
||
);
|
||
// 一张配载单的全部子运单必须同项目:以已选运单(无已选时以本批首张)的项目为准
|
||
const anchorRow = [...this.selectedWaybillRows, ...selectedRows].find(row =>
|
||
this.waybillProjectKey(row)
|
||
);
|
||
const anchorKey = anchorRow ? this.waybillProjectKey(anchorRow) : '';
|
||
const mismatchedRows = anchorKey
|
||
? selectedRows.filter(
|
||
row => this.waybillProjectKey(row) && this.waybillProjectKey(row) !== anchorKey
|
||
)
|
||
: [];
|
||
if (mismatchedRows.length) {
|
||
const anchorName = anchorRow.projectName || anchorRow.projectId;
|
||
const mismatchedText = mismatchedRows
|
||
.map(
|
||
row =>
|
||
`${row.waybillNo || row.id}(项目:${row.projectName || row.projectId || '未知'})`
|
||
)
|
||
.join('、');
|
||
this.$message.warning(
|
||
`运单 ${mismatchedText} 与已选运单项目「${anchorName}」不一致,不能加入同一配载单`
|
||
);
|
||
}
|
||
const nextRows = [...this.selectedWaybillRows];
|
||
selectedRows.forEach(row => {
|
||
if (mismatchedRows.includes(row)) return;
|
||
const key = String(row.id || row.waybillNo);
|
||
if (!exists.has(key)) {
|
||
nextRows.push(row);
|
||
exists.add(key);
|
||
}
|
||
});
|
||
// 承运商类型:新增后须仍存在共同的承运商合同乙方(按客户合同乙方=承运商合同甲方匹配后取交集)
|
||
if (
|
||
String(this.dialogForm.carrierType || '').trim() === '承运商' &&
|
||
nextRows.length > this.selectedWaybillRows.length
|
||
) {
|
||
const compatible = await this.hasCompatibleCarrierContracts(nextRows);
|
||
if (!compatible) {
|
||
this.$message.warning(
|
||
'所选运单在同一项目下没有共同的承运商合同乙方,不能加入同一配载单'
|
||
);
|
||
return;
|
||
}
|
||
}
|
||
const prevRouteNodes = [...this.routeNodes];
|
||
this.selectedWaybillRows = nextRows;
|
||
if (this.isRunningEdit) {
|
||
await this.refreshWaybillPunchMap();
|
||
}
|
||
this.rebuildSummaryFromWaybills(true, prevRouteNodes);
|
||
await this.loadTaskCarrierOptions();
|
||
this.$refs.candidateTableRef?.clearSelection?.();
|
||
this.candidateSelection = [];
|
||
} finally {
|
||
this.candidateAdding = false;
|
||
}
|
||
},
|
||
async hasCompatibleCarrierContracts(rows = []) {
|
||
const projectId = (rows || []).map(row => row.projectId).find(Boolean);
|
||
const waybillIds = (rows || []).map(row => row.id).filter(Boolean);
|
||
if (!projectId || waybillIds.length < 2) return true;
|
||
try {
|
||
const response = await loadingApi.getCarrierContracts([projectId], waybillIds);
|
||
const contracts = response?.data?.data || response?.data || [];
|
||
return (Array.isArray(contracts) ? contracts : []).length > 0;
|
||
} catch (error) {
|
||
return true;
|
||
}
|
||
},
|
||
async addSelectedWaybills() {
|
||
await this.addWaybillRows(this.candidateSelection);
|
||
},
|
||
waybillProjectKey(row = {}) {
|
||
return String(row.projectId || row.projectName || '').trim();
|
||
},
|
||
removeSelectedWaybill(index) {
|
||
const row = this.selectedWaybillRows[index];
|
||
if (this.isSelectedWaybillRemoveLocked(row)) {
|
||
ElMessage.warning('已打卡的子运单不可移除');
|
||
return;
|
||
}
|
||
this.selectedWaybillRows.splice(index, 1);
|
||
if (this.isRunningEdit) {
|
||
this.refreshWaybillPunchMap().then(() => {
|
||
this.rebuildSummaryFromWaybills();
|
||
this.loadTaskCarrierOptions();
|
||
});
|
||
return;
|
||
}
|
||
this.rebuildSummaryFromWaybills();
|
||
this.loadTaskCarrierOptions();
|
||
},
|
||
rebuildSummaryFromWaybills(rebuildRoute = true, prevRouteNodes = null) {
|
||
const rows = this.selectedWaybillRows;
|
||
this.dialogForm.loadingSubNos = rows
|
||
.map(row => row.waybillNo)
|
||
.filter(Boolean)
|
||
.join(',');
|
||
const rowIds = rows
|
||
.map(row => row.id)
|
||
.filter(Boolean)
|
||
.map(id => String(id));
|
||
// 回显失败仅有运单号时,保留原 waybillIdsJson,避免确认提交清空运单绑定
|
||
if (rowIds.length) {
|
||
this.dialogForm.waybillIdsJson = JSON.stringify(rowIds);
|
||
} else if (!String(this.dialogForm.waybillIdsJson || '').trim()) {
|
||
this.dialogForm.waybillIdsJson = '[]';
|
||
}
|
||
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) {
|
||
const builtNodes = this.buildRouteNodes(rows);
|
||
this.routeNodes = this.isRunningEdit
|
||
? this.mergeRouteNodesKeepingPunched(
|
||
prevRouteNodes || this.routeNodes,
|
||
builtNodes
|
||
)
|
||
: builtNodes;
|
||
}
|
||
this.cargoRows = this.buildCargoRows(rows);
|
||
// 编辑回填阶段 routeNodes 尚未恢复时跳过,避免清空已有发到货地
|
||
if (rebuildRoute || this.routeNodes.length) {
|
||
this.syncRouteAddressFields();
|
||
}
|
||
},
|
||
buildRouteNodes(rows) {
|
||
const nodeMap = new Map();
|
||
const appendNode = (address, displayAddress, tag, locked) => {
|
||
if (!address) return;
|
||
if (!nodeMap.has(address)) {
|
||
nodeMap.set(address, { address, displayAddress, tags: [], locked: false });
|
||
}
|
||
const node = nodeMap.get(address);
|
||
node.tags.push(tag);
|
||
if (locked) node.locked = true;
|
||
};
|
||
rows.forEach((row, rowIndex) => {
|
||
const locked = this.isRunningEdit
|
||
? this.isWaybillLoadPunched(row)
|
||
: this.isWaybillCompleted(row);
|
||
const departure = row.departureAddress || row.departureName;
|
||
appendNode(
|
||
departure,
|
||
this.formatCandidateAddress(row, 'departure'),
|
||
`装${rowIndex + 1}`,
|
||
locked
|
||
);
|
||
});
|
||
rows.forEach((row, rowIndex) => {
|
||
const locked = this.isRunningEdit
|
||
? this.isWaybillUnloadPunched(row)
|
||
: this.isWaybillCompleted(row);
|
||
const arrival = row.arrivalAddress || row.arrivalName;
|
||
appendNode(
|
||
arrival,
|
||
this.formatCandidateAddress(row, 'arrival'),
|
||
`卸${rowIndex + 1}`,
|
||
locked
|
||
);
|
||
});
|
||
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) {
|
||
if (this.dialogReadonly || this.isRouteChangeNodeLocked(this.routeNodes[index])) {
|
||
this.routeDragIndex = -1;
|
||
return;
|
||
}
|
||
this.routeDragIndex = index;
|
||
},
|
||
handleRouteDrop(index) {
|
||
if (this.dialogReadonly || this.routeDragIndex < 0 || this.routeDragIndex === index) return;
|
||
if (
|
||
this.isRouteChangeNodeLocked(this.routeNodes[this.routeDragIndex]) ||
|
||
this.isRouteChangeNodeLocked(this.routeNodes[index])
|
||
) {
|
||
this.routeDragIndex = -1;
|
||
return;
|
||
}
|
||
const rows = [...this.routeNodes];
|
||
const [target] = rows.splice(this.routeDragIndex, 1);
|
||
rows.splice(index, 0, target);
|
||
const originalLockedKeys = this.routeNodes.map(item =>
|
||
this.isRouteChangeNodeLocked(item) ? item.key : null
|
||
);
|
||
const nextLockedKeys = rows.map(item =>
|
||
this.isRouteChangeNodeLocked(item) ? item.key : null
|
||
);
|
||
if (
|
||
originalLockedKeys.some((key, lockedIndex) => key && key !== nextLockedKeys[lockedIndex])
|
||
) {
|
||
ElMessage.warning(
|
||
this.isRunningEdit ? '已打卡地点不可调整,新地址只能在未打卡节点间拖拽' : '已完成运单对应的路线不可调整'
|
||
);
|
||
this.routeDragIndex = -1;
|
||
return;
|
||
}
|
||
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)];
|
||
});
|
||
},
|
||
formatCargoQuantityDisplay(row = {}) {
|
||
const quantity = blankCargoSentinel(row.quantity ?? row.cargoQuantity ?? '');
|
||
const unit = String(
|
||
blankCargoSentinel(
|
||
row.quantityUnit ||
|
||
row.goodsQuantityUnit ||
|
||
row.cargoUnit ||
|
||
row.unit ||
|
||
''
|
||
) || ''
|
||
).trim();
|
||
if (quantity === '' || quantity === undefined || quantity === null) {
|
||
return unit || '';
|
||
}
|
||
return unit ? `${quantity} ${unit}` : String(quantity);
|
||
},
|
||
normalizeCargoRow(waybill, item = {}) {
|
||
return sanitizeCargoRow({
|
||
projectName: item.projectName || waybill.projectName || '',
|
||
cargoName: item.cargoName || item.goodsName || waybill.cargoName || '',
|
||
cargoType: item.cargoType || item.goodsType || waybill.cargoType || '',
|
||
packageType: item.packageType || item.package || '',
|
||
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 || '',
|
||
});
|
||
},
|
||
applyLoadingMileageFromDetail(form = this.dialogForm) {
|
||
let taskMileage = '';
|
||
try {
|
||
const taskInfo = JSON.parse(form.taskInfoJson || '{}');
|
||
taskMileage = taskInfo?.mileage;
|
||
} catch (error) {
|
||
taskMileage = '';
|
||
}
|
||
form.mileage = normalizeLoadingMileage(
|
||
form.mileage !== undefined && form.mileage !== null && form.mileage !== ''
|
||
? form.mileage
|
||
: taskMileage
|
||
);
|
||
},
|
||
applyLoadingTaskInfoFromDetail(form = this.dialogForm) {
|
||
let taskInfo = {};
|
||
try {
|
||
taskInfo = JSON.parse(form.taskInfoJson || '{}') || {};
|
||
} catch (error) {
|
||
taskInfo = {};
|
||
}
|
||
if (!taskInfo || typeof taskInfo !== 'object' || Array.isArray(taskInfo)) return;
|
||
const fields = [
|
||
'carrierType',
|
||
'carrierName',
|
||
'carrierContractId',
|
||
'driverName',
|
||
'driverPhone',
|
||
'vehicleNo',
|
||
'trailerVehicleNo',
|
||
'escortName',
|
||
'escortPhone',
|
||
'estimatedStartDate',
|
||
'estimatedEndDate',
|
||
'taskRemark',
|
||
];
|
||
fields.forEach(prop => {
|
||
if (
|
||
(form[prop] === undefined || form[prop] === null || form[prop] === '') &&
|
||
taskInfo[prop] !== undefined &&
|
||
taskInfo[prop] !== null &&
|
||
taskInfo[prop] !== ''
|
||
) {
|
||
form[prop] = taskInfo[prop];
|
||
}
|
||
});
|
||
if (
|
||
(form.mileage === undefined || form.mileage === null || form.mileage === '') &&
|
||
taskInfo.mileage !== undefined
|
||
) {
|
||
form.mileage = normalizeLoadingMileage(taskInfo.mileage);
|
||
}
|
||
},
|
||
detailTaskField(prop) {
|
||
const value = String(this.dialogForm?.[prop] ?? '').trim();
|
||
return value || '-';
|
||
},
|
||
detailTaskDate(prop) {
|
||
const value = String(this.dialogForm?.[prop] ?? '').trim();
|
||
return value ? value.slice(0, 10) : '-';
|
||
},
|
||
normalizePayload() {
|
||
this.syncRouteAddressFields();
|
||
this.rebuildSummaryFromWaybills(false);
|
||
this.dialogForm.mileage = normalizeLoadingMileage(this.dialogForm.mileage);
|
||
const mileage = toSubmitMileage(this.dialogForm.mileage);
|
||
const taskInfo = {
|
||
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,
|
||
estimatedStartDate: this.dialogForm.estimatedStartDate,
|
||
estimatedEndDate: this.dialogForm.estimatedEndDate,
|
||
taskRemark: this.dialogForm.taskRemark,
|
||
};
|
||
if (mileage !== null) {
|
||
taskInfo.mileage = mileage;
|
||
}
|
||
return {
|
||
...this.dialogForm,
|
||
mileage,
|
||
routeJson: JSON.stringify(this.routeNodes),
|
||
goodsJson: JSON.stringify(this.cargoRows),
|
||
taskInfoJson: JSON.stringify(taskInfo),
|
||
};
|
||
},
|
||
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() {
|
||
// 暂存不做任何业务校验,直接保存
|
||
this.dialogSaving = 'draft';
|
||
try {
|
||
const res = await loadingApi.saveDraft(this.normalizePayload());
|
||
this.dialogForm = {
|
||
...this.dialogForm,
|
||
...(res.data?.data || res.data || {}),
|
||
};
|
||
this.applyLoadingMileageFromDetail(this.dialogForm);
|
||
ElMessage.success('暂存成功');
|
||
sessionStorage.setItem(loadingManageListRefreshKey, '1');
|
||
this.closeLoadingDialog();
|
||
this.loadTable();
|
||
} finally {
|
||
this.dialogSaving = '';
|
||
}
|
||
},
|
||
async submitLoading() {
|
||
if (!this.validatePayload(false)) return;
|
||
this.dialogSaving = 'submit';
|
||
try {
|
||
const payload = {
|
||
...this.normalizePayload(),
|
||
// 进行中编辑保持 running;新建/草稿确认由后端按项目是否需接单写入 pending/running
|
||
...(this.isRunningEdit ? { businessStatus: 'running' } : {}),
|
||
};
|
||
if (this.dialogMode === 'reassign') {
|
||
await loadingApi.reassign(payload);
|
||
ElMessage.success('派单成功');
|
||
} else {
|
||
await loadingApi.submit(payload);
|
||
ElMessage.success(this.isRunningEdit ? '保存成功' : '确认成功');
|
||
}
|
||
sessionStorage.setItem(loadingManageListRefreshKey, '1');
|
||
this.closeLoadingDialog();
|
||
this.loadTable();
|
||
} finally {
|
||
this.dialogSaving = '';
|
||
}
|
||
},
|
||
deleteLoading(row) {
|
||
ElMessageBox.confirm(`确定删除配载单 ${row.loadingNo}?`, '提示', { type: 'warning' })
|
||
.then(async () => {
|
||
const res = await loadingApi.remove(row.id);
|
||
const result = res.data?.data || {};
|
||
ElMessage.success(`删除成功 ${result.successCount || 0} 条`);
|
||
this.loadTable();
|
||
})
|
||
.catch(() => {});
|
||
},
|
||
cancelLoading(row) {
|
||
ElMessageBox.confirm(`确定取消配载单 ${row.loadingNo}?`, '提示', { type: 'warning' })
|
||
.then(async () => {
|
||
await loadingApi.cancel(row.id);
|
||
ElMessage.success('取消成功');
|
||
this.loadTable();
|
||
})
|
||
.catch(() => {});
|
||
},
|
||
startLoading(row) {
|
||
ElMessageBox.confirm(`确定将配载单 ${row.loadingNo} 改为进行中?`, '提示', { type: 'warning' })
|
||
.then(async () => {
|
||
await loadingApi.start(row.id);
|
||
ElMessage.success('已改为进行中');
|
||
this.loadTable();
|
||
})
|
||
.catch(() => {});
|
||
},
|
||
completeLoading(row) {
|
||
ElMessageBox.confirm(`确定完成配载单 ${row.loadingNo}?`, '提示', { type: 'warning' })
|
||
.then(async () => {
|
||
await loadingApi.complete(row.id);
|
||
ElMessage.success('完成成功');
|
||
this.loadTable();
|
||
})
|
||
.catch(() => {});
|
||
},
|
||
handleBatchComplete() {
|
||
if (!this.selectionRows.length) {
|
||
ElMessage.warning('请选择需要批量完成的配载单');
|
||
return;
|
||
}
|
||
ElMessageBox.confirm('确定对选中的配载单执行批量完成?', '提示', { type: 'warning' })
|
||
.then(async () => {
|
||
const ids = this.selectionRows.map(row => row.id).join(',');
|
||
const res = await loadingApi.batchComplete(ids);
|
||
const result = res.data?.data || {};
|
||
const summary = `批量完成成功 ${result.successCount || 0} 条,跳过 ${
|
||
result.skippedCount || 0
|
||
} 条`;
|
||
if (result.skippedCount && result.skippedReasons?.length) {
|
||
ElMessage.warning(`${summary};${result.skippedReasons.join(';')}`);
|
||
} else {
|
||
ElMessage.success(summary);
|
||
}
|
||
this.loadTable();
|
||
})
|
||
.catch(() => {});
|
||
},
|
||
handleExport() {
|
||
const ids = this.selectionRows.map(row => row.id).join(',');
|
||
exportBlob(
|
||
loadingApi.exportUrl,
|
||
{
|
||
...this.buildQueryParams(this.searchForm),
|
||
ids,
|
||
exportColumns: JSON.stringify(loadingManageConfig.exportColumns),
|
||
},
|
||
{
|
||
feedback: true,
|
||
}
|
||
).then(res => {
|
||
downloadXls(res.data, `配载管理${dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`);
|
||
});
|
||
},
|
||
handleCarrierTypeChange() {
|
||
this.dialogForm.carrierContractId = '';
|
||
this.dialogForm.carrierPartyId = '';
|
||
this.dialogForm.carrierPartyRole = '';
|
||
this.dialogForm.carrierName = '';
|
||
this.dialogForm.carrierId = '';
|
||
this.clearDialogDriverVehicleFields();
|
||
if (this.dialogForm.carrierType === '承运商') {
|
||
this.dialogForm.trailerVehicleNo = '';
|
||
this.dialogForm.escortName = '';
|
||
this.dialogForm.escortPhone = '';
|
||
}
|
||
if (this.dialogForm.carrierType === '自运') {
|
||
this.dialogForm.carrierContractId = '';
|
||
this.dialogForm.carrierPartyId = '';
|
||
this.dialogForm.carrierPartyRole = '';
|
||
}
|
||
this.loadTaskCarrierOptions();
|
||
},
|
||
handleTaskCarrierChange(selectedValue) {
|
||
const contract = this.taskCarrierOptions.find(
|
||
item =>
|
||
this.carrierOptionValue(item) === String(selectedValue || '') ||
|
||
[item.id, item.carrierName, item.partyB].some(
|
||
value => String(value || '') === String(selectedValue || '')
|
||
)
|
||
);
|
||
this.dialogForm.carrierContractId =
|
||
this.dialogForm.carrierType === '承运商' && contract?.id ? contract.id : '';
|
||
this.dialogForm.carrierPartyId =
|
||
this.dialogForm.carrierType === '承运商' ? contract?.carrierPartyId || '' : '';
|
||
this.dialogForm.carrierPartyRole =
|
||
this.dialogForm.carrierType === '承运商' ? contract?.carrierPartyRole || '' : '';
|
||
this.dialogForm.carrierName = contract?.carrierName || '';
|
||
this.dialogForm.carrierId = contract?.carrierId || contract?.customerId || '';
|
||
this.clearDialogDriverVehicleFields();
|
||
},
|
||
clearDialogDriverVehicleFields() {
|
||
this.dialogForm.driverName = '';
|
||
this.dialogForm.driverPhone = '';
|
||
this.dialogForm.vehicleNo = '';
|
||
this.dialogForm.trailerVehicleNo = '';
|
||
this.dialogForm.escortName = '';
|
||
this.dialogForm.escortPhone = '';
|
||
this.driverOptions = [];
|
||
},
|
||
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.loadDialogDriverOptions(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;
|
||
fetchDriverCandidates({
|
||
size: 20,
|
||
...(keyword ? { driverName: keyword } : {}),
|
||
posts: '押运员',
|
||
})
|
||
.then(records => {
|
||
callback(
|
||
records.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, '');
|
||
},
|
||
normalizeLoadingMileage,
|
||
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, { ...(keyword ? { driverName: keyword } : {}), posts: '司机' })
|
||
);
|
||
} finally {
|
||
this.driverLoading = false;
|
||
}
|
||
},
|
||
async loadDialogDriverOptions(keyword = '') {
|
||
this.driverLoading = true;
|
||
try {
|
||
this.driverOptions = await fetchDriverCandidates({
|
||
size: 20,
|
||
...(keyword ? { driverName: keyword } : {}),
|
||
posts: '司机',
|
||
});
|
||
return this.driverOptions;
|
||
} 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 projectId = this.selectedWaybillRows.map(row => row.projectId).find(Boolean);
|
||
const waybillIds = this.selectedWaybillRows.map(row => row.id).filter(Boolean);
|
||
const response = await loadingApi.getCarrierContracts(
|
||
projectId ? [projectId] : [],
|
||
waybillIds
|
||
);
|
||
if (requestId !== this.taskCarrierRequestId) return [];
|
||
const contracts = response.data?.data || response.data || [];
|
||
if (this.dialogForm.carrierType !== '自运') {
|
||
this.taskCarrierOptions = contracts
|
||
.map(item => ({
|
||
...item,
|
||
carrierName:
|
||
item.carrierName ||
|
||
item.partyB ||
|
||
item.partyBName ||
|
||
item.contractPartyB ||
|
||
item.customerContractPartyB ||
|
||
'',
|
||
carrierId:
|
||
item.customerId ||
|
||
item.partyBId ||
|
||
item.partyBUserId ||
|
||
item.contractPartyBId ||
|
||
item.customerContractPartyBId ||
|
||
item.carrierId ||
|
||
'',
|
||
carrierPartyId: item.carrierPartyId || '',
|
||
carrierPartyRole: item.carrierPartyRole || '',
|
||
}))
|
||
.filter(item => item.carrierName);
|
||
const current =
|
||
this.taskCarrierOptions.find(
|
||
item =>
|
||
this.carrierOptionValue(item) === this.taskCarrierSelectedValue ||
|
||
(String(item.id) === String(this.dialogForm.carrierContractId) &&
|
||
(!this.dialogForm.carrierPartyId ||
|
||
String(item.carrierPartyId) === String(this.dialogForm.carrierPartyId)) &&
|
||
(!this.dialogForm.carrierName ||
|
||
item.carrierName === this.dialogForm.carrierName))
|
||
) ||
|
||
this.taskCarrierOptions.find(item => item.carrierName === this.dialogForm.carrierName);
|
||
if (current) {
|
||
this.dialogForm.carrierContractId = current.id;
|
||
this.dialogForm.carrierPartyId = current.carrierPartyId || '';
|
||
this.dialogForm.carrierPartyRole = current.carrierPartyRole || '';
|
||
this.dialogForm.carrierName = current.carrierName;
|
||
this.dialogForm.carrierId = current.carrierId || '';
|
||
} else {
|
||
this.dialogForm.carrierContractId = '';
|
||
this.dialogForm.carrierPartyId = '';
|
||
this.dialogForm.carrierPartyRole = '';
|
||
this.dialogForm.carrierName = '';
|
||
this.dialogForm.carrierId = '';
|
||
}
|
||
return this.taskCarrierOptions;
|
||
}
|
||
if (this.dialogForm.carrierType === '自运' && contracts.length) {
|
||
this.taskCarrierOptions = contracts
|
||
.map(item => ({
|
||
...item,
|
||
carrierName:
|
||
item.carrierName ||
|
||
item.partyB ||
|
||
item.partyBName ||
|
||
item.contractPartyB ||
|
||
item.customerContractPartyB ||
|
||
'',
|
||
carrierId:
|
||
item.customerId ||
|
||
item.partyBId ||
|
||
item.partyBUserId ||
|
||
item.contractPartyBId ||
|
||
item.customerContractPartyBId ||
|
||
item.carrierId ||
|
||
'',
|
||
carrierPartyId: item.carrierPartyId || '',
|
||
carrierPartyRole: item.carrierPartyRole || '',
|
||
}))
|
||
.filter(item => item.carrierName);
|
||
const current = this.taskCarrierOptions.find(
|
||
item => item.carrierName === this.dialogForm.carrierName
|
||
);
|
||
this.dialogForm.carrierContractId = '';
|
||
this.dialogForm.carrierName =
|
||
current?.carrierName || this.taskCarrierOptions[0]?.carrierName || '';
|
||
this.dialogForm.carrierId =
|
||
current?.carrierId || this.taskCarrierOptions[0]?.carrierId || '';
|
||
return this.taskCarrierOptions;
|
||
}
|
||
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] || '';
|
||
this.dialogForm.carrierId = '';
|
||
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 {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.loading-manage-page__table {
|
||
width: 100%;
|
||
}
|
||
|
||
.loading-manage-page__link {
|
||
padding: 0;
|
||
min-height: auto;
|
||
}
|
||
|
||
.loading-manage-page__sub-nos {
|
||
display: block;
|
||
line-height: 20px;
|
||
white-space: normal;
|
||
overflow-wrap: anywhere;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.loading-manage-page__sub-no-link {
|
||
display: inline;
|
||
white-space: normal;
|
||
line-height: 20px;
|
||
text-align: left;
|
||
}
|
||
|
||
.loading-manage-page__address-cell {
|
||
line-height: 20px;
|
||
white-space: pre-line;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.loading-manage-page__status {
|
||
color: #303133;
|
||
}
|
||
|
||
.loading-manage-page__status.is-running {
|
||
color: #1dbf00;
|
||
}
|
||
|
||
.loading-manage-page__status.is-cancelled {
|
||
color: #909399;
|
||
}
|
||
|
||
.loading-manage-page__actions {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 2px 12px;
|
||
}
|
||
|
||
.loading-manage-page__actions .el-button {
|
||
margin-left: 0;
|
||
padding: 0;
|
||
}
|
||
|
||
.loading-manage-page__pagination {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
padding: 12px 0 0;
|
||
background: #fff;
|
||
}
|
||
|
||
:deep(.el-form-item) {
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
:deep(.el-form-item__label) {
|
||
white-space: nowrap;
|
||
}
|
||
|
||
:deep(.el-input),
|
||
:deep(.el-select),
|
||
:deep(.el-date-editor.el-input),
|
||
:deep(.el-date-editor.el-input__wrapper),
|
||
:deep(.el-date-editor--daterange) {
|
||
width: 100%;
|
||
}
|
||
|
||
:deep(.el-table) {
|
||
--el-table-border-color: #eff1f7;
|
||
--el-table-row-hover-bg-color: #f5f7fa;
|
||
}
|
||
|
||
:deep(.el-table__body tr:nth-child(even) > td) {
|
||
background: #fafafa;
|
||
}
|
||
}
|
||
|
||
.loading-manage-dialog {
|
||
min-height: 12vh;
|
||
.loading-manage-dialog__body {
|
||
max-height: 78vh;
|
||
overflow-y: auto;
|
||
padding-right: 4px;
|
||
}
|
||
|
||
.loading-manage-dialog__grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
gap: 0 24px;
|
||
}
|
||
|
||
.loading-manage-dialog__span-2 {
|
||
grid-column: 1 / -1;
|
||
}
|
||
|
||
.loading-manage-dialog__title-action {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.loading-manage-dialog__candidate-search {
|
||
padding: 0;
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.loading-manage-dialog__waybill-route-layout {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 16px;
|
||
align-items: start;
|
||
//margin-bottom: 12px;
|
||
.section-card {
|
||
min-height: 257px;
|
||
}
|
||
}
|
||
|
||
.loading-manage-dialog__candidate-actions,
|
||
.loading-manage-dialog__collapsed-search,
|
||
.loading-manage-dialog__table-toolbar,
|
||
.loading-manage-dialog__footer {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.loading-manage-dialog__table-toolbar {
|
||
justify-content: space-between;
|
||
margin: 10px 0;
|
||
}
|
||
|
||
.loading-manage-dialog__route-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.loading-manage-dialog__cargo-section {
|
||
margin-top: 16px;
|
||
}
|
||
|
||
@media (max-width: 1200px) {
|
||
.loading-manage-dialog__grid {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
|
||
.loading-manage-dialog__waybill-route-layout {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
|
||
.loading-manage-dialog__route-item {
|
||
display: grid;
|
||
grid-template-columns: 28px 36px minmax(0, 1fr) auto;
|
||
align-items: center;
|
||
gap: 10px;
|
||
min-height: 44px;
|
||
padding: 8px 12px;
|
||
border: 1px solid #eff1f7;
|
||
background: #fff;
|
||
cursor: move;
|
||
|
||
&.is-locked {
|
||
cursor: not-allowed;
|
||
background: #f5f7fa;
|
||
color: #909399;
|
||
}
|
||
}
|
||
|
||
.loading-manage-dialog__route-handle {
|
||
color: #909399;
|
||
}
|
||
|
||
.loading-manage-dialog__route-hint {
|
||
color: #909399;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.loading-manage-dialog__locked-tip {
|
||
color: #909399;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.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: 24px;
|
||
}
|
||
|
||
: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: 12px;
|
||
padding: 4px 0 16px;
|
||
|
||
:deep(.el-tag) {
|
||
height: 28px;
|
||
padding: 0 12px;
|
||
border: 0;
|
||
border-radius: 8px;
|
||
font-size: 13px;
|
||
line-height: 28px;
|
||
}
|
||
}
|
||
.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: minmax(0, 1fr) 220px;
|
||
gap: 24px;
|
||
padding-bottom: 4px;
|
||
align-items: start;
|
||
}
|
||
.loading-detail__task-main {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
min-width: 0;
|
||
}
|
||
.loading-detail__task-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
gap: 16px 24px;
|
||
}
|
||
.loading-detail__attachment-field {
|
||
min-width: 0;
|
||
}
|
||
.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;
|
||
color: #303133;
|
||
word-break: break-all;
|
||
}
|
||
.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-address-text {
|
||
display: inline-block;
|
||
max-width: 100%;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
vertical-align: bottom;
|
||
cursor: default;
|
||
}
|
||
.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: 200px;
|
||
}
|
||
.loading-detail__punch-record {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
.loading-detail__punch-record-title {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
}
|
||
.loading-detail__punch-record-line {
|
||
font-size: 13px;
|
||
color: #606266;
|
||
|
||
span + span {
|
||
margin-left: 12px;
|
||
}
|
||
}
|
||
.loading-detail__driver-upload-grid,
|
||
.loading-detail__voucher-image-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||
gap: 10px;
|
||
margin-top: 4px;
|
||
min-height: 80px;
|
||
}
|
||
.loading-detail__driver-upload-item,
|
||
.loading-detail__voucher-image-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
cursor: pointer;
|
||
|
||
.el-image {
|
||
display: block;
|
||
width: 100%;
|
||
aspect-ratio: 1;
|
||
border: 1px solid #eff1f7;
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
background: #f5f7fa;
|
||
}
|
||
}
|
||
.loading-detail__voucher-image-item--folder .el-image {
|
||
aspect-ratio: auto;
|
||
padding: 12px;
|
||
background: transparent;
|
||
border: none;
|
||
}
|
||
.loading-detail__driver-upload-label,
|
||
.loading-detail__voucher-folder-name {
|
||
font-size: 12px;
|
||
color: #606266;
|
||
line-height: 18px;
|
||
text-align: center;
|
||
word-break: break-all;
|
||
}
|
||
.loading-detail__voucher-folder-toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 8px;
|
||
}
|
||
.loading-detail__map {
|
||
width: 100%;
|
||
min-height: 260px;
|
||
border: 1px solid #eff1f7;
|
||
}
|
||
}
|
||
:global(.loading-route-change-dialog .el-dialog__body) {
|
||
background: #fff;
|
||
}
|
||
.loading-route-change-dialog {
|
||
.loading-route-change-dialog__original {
|
||
color: #a8abb2;
|
||
line-height: 20px;
|
||
margin-bottom: 4px;
|
||
}
|
||
.loading-route-change-dialog__address-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
|
||
.el-input {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
}
|
||
.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;
|
||
|
||
&.is-locked {
|
||
cursor: not-allowed;
|
||
background: #eef0f3;
|
||
color: #909399;
|
||
}
|
||
}
|
||
.loading-route-change-dialog__route-handle-placeholder {
|
||
width: 14px;
|
||
height: 14px;
|
||
}
|
||
.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 {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
line-height: 22px;
|
||
white-space: normal;
|
||
text-align: left;
|
||
}
|
||
.loading-route-change-dialog__change-group {
|
||
padding-left: 8px;
|
||
border-left: 2px solid #409eff;
|
||
}
|
||
.loading-route-change-dialog__change-group-title {
|
||
margin-bottom: 4px;
|
||
color: #409eff;
|
||
font-weight: 500;
|
||
}
|
||
.loading-route-change-dialog__change-group-text {
|
||
color: #606266;
|
||
word-break: break-word;
|
||
overflow-wrap: anywhere;
|
||
}
|
||
.loading-route-change-dialog__change-field {
|
||
display: grid;
|
||
grid-template-columns: 54px minmax(0, 1fr);
|
||
column-gap: 8px;
|
||
row-gap: 2px;
|
||
}
|
||
.loading-route-change-dialog__change-field-label {
|
||
color: #909399;
|
||
white-space: nowrap;
|
||
}
|
||
.loading-route-change-dialog__change-before,
|
||
.loading-route-change-dialog__change-single {
|
||
color: #606266;
|
||
word-break: break-word;
|
||
overflow-wrap: anywhere;
|
||
}
|
||
.loading-route-change-dialog__change-arrow {
|
||
color: #c0c4cc;
|
||
text-align: right;
|
||
}
|
||
.loading-route-change-dialog__change-after {
|
||
color: #409eff;
|
||
word-break: break-word;
|
||
overflow-wrap: anywhere;
|
||
}
|
||
.loading-route-change-dialog__change-flag {
|
||
margin-left: 6px;
|
||
color: #c0c4cc;
|
||
font-size: 12px;
|
||
}
|
||
}
|
||
.loading-route-change-map-dialog {
|
||
.loading-route-change-map-dialog__toolbar {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-bottom: 12px;
|
||
|
||
.el-input {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
}
|
||
.loading-route-change-map-dialog__map {
|
||
width: 100%;
|
||
min-width: 0;
|
||
height: 440px;
|
||
}
|
||
.loading-route-change-map-dialog__status {
|
||
margin-top: 8px;
|
||
color: #606266;
|
||
}
|
||
}
|
||
:global(.loading-manage-form-page) {
|
||
width: 100%;
|
||
min-height: 100%;
|
||
box-sizing: border-box;
|
||
background: #f5f6fa;
|
||
}
|
||
/* 独立页面(新增/编辑/重新派单/详情):内容随 #avue-view 自然滚动,不做内嵌 78vh 滚动盒。
|
||
此处权重必须高于 .loading-manage-dialog .loading-manage-dialog__body,故带上 .loading-manage-page 外壳。 */
|
||
:global(.loading-manage-page .loading-manage-form-dialog.loading-manage-form-page .loading-manage-dialog__body) {
|
||
max-height: none;
|
||
overflow: visible;
|
||
padding-right: 0;
|
||
}
|
||
:global(.loading-manage-form-dialog) {
|
||
width: 100% !important;
|
||
min-height: 100%;
|
||
margin: 0 !important;
|
||
border-radius: 0;
|
||
box-shadow: none;
|
||
}
|
||
:global(.loading-manage-form-dialog .el-dialog__body) {
|
||
max-height: none;
|
||
padding: 20px 24px 96px;
|
||
background: #f5f6fa;
|
||
}
|
||
:global(.el-overlay:has(.loading-manage-form-dialog)) {
|
||
position: fixed !important;
|
||
inset: 92px 0 0 230px !important;
|
||
z-index: 100 !important;
|
||
overflow: auto;
|
||
background: transparent;
|
||
}
|
||
:global(.el-overlay:has(.loading-manage-form-dialog) .el-overlay-dialog) {
|
||
min-height: 100%;
|
||
overflow: visible;
|
||
}
|
||
:global(.loading-manage-form-dialog .el-dialog__header) {
|
||
padding: 18px 24px;
|
||
margin-right: 0;
|
||
background: #fff;
|
||
border-bottom: 1px solid #eff1f7;
|
||
}
|
||
:global(.loading-manage-form-dialog .el-dialog__footer) {
|
||
position: fixed;
|
||
right: 0;
|
||
bottom: 0;
|
||
left: 230px;
|
||
z-index: 1001;
|
||
box-sizing: border-box;
|
||
padding: 12px 24px;
|
||
background: #fff;
|
||
border-top: 1px solid #eff1f7;
|
||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
|
||
}
|
||
:global(.loading-manage-form-page .loading-manage-dialog__footer) {
|
||
position: fixed;
|
||
right: 0;
|
||
bottom: 0;
|
||
left: 230px;
|
||
z-index: 1001;
|
||
box-sizing: border-box;
|
||
min-height: 64px;
|
||
padding: 12px 24px;
|
||
background: #fff;
|
||
border-top: 1px solid #eff1f7;
|
||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
|
||
}
|
||
:global(.avue--collapse .el-overlay:has(.loading-manage-form-dialog)) {
|
||
left: 60px !important;
|
||
}
|
||
:global(.avue-layout--horizontal .el-overlay:has(.loading-manage-form-dialog)) {
|
||
top: 92px !important;
|
||
left: 0 !important;
|
||
}
|
||
:global(.avue--collapse .loading-manage-form-dialog .el-dialog__footer) {
|
||
left: 60px;
|
||
}
|
||
:global(.avue-layout--horizontal .loading-manage-form-dialog .el-dialog__footer) {
|
||
left: 0;
|
||
}
|
||
:global(.avue--collapse .loading-manage-form-page .loading-manage-dialog__footer) {
|
||
left: 60px;
|
||
}
|
||
:global(.avue-layout--horizontal .loading-manage-form-page .loading-manage-dialog__footer) {
|
||
left: 0;
|
||
}
|
||
@media (max-width: 900px) {
|
||
.loading-manage-dialog__grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.loading-detail__task-row,
|
||
.loading-detail__bottom-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.loading-detail__task-grid {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
}
|
||
</style>
|