10531 lines
365 KiB
Vue
10531 lines
365 KiB
Vue
<template>
|
||
<basic-container
|
||
:class="[
|
||
'waybill-manage-page',
|
||
{
|
||
'waybill-manage-page--form-page': formPageLocked,
|
||
'waybill-manage-page--detail-page': detailPageLocked,
|
||
},
|
||
]"
|
||
>
|
||
<component
|
||
v-if="!detailPageLocked"
|
||
:is="crudContainer"
|
||
:option="pageFormOption"
|
||
:form-page-title="formPageLocked ? formPageTitle : undefined"
|
||
:table-loading="loading"
|
||
:data="data"
|
||
v-model:page="page"
|
||
v-model="form"
|
||
:status="formPageLocked ? dialogReadonly : undefined"
|
||
ref="crud"
|
||
:permission="permissionList"
|
||
:before-open="beforeOpen"
|
||
@row-save="rowSave"
|
||
@row-update="rowUpdate"
|
||
@row-del="rowDel"
|
||
@search-change="searchChange"
|
||
@search-reset="searchReset"
|
||
@selection-change="selectionChange"
|
||
@current-change="currentChange"
|
||
@size-change="sizeChange"
|
||
@refresh-change="refreshChange"
|
||
@on-load="onLoad"
|
||
>
|
||
<template #menu-left>
|
||
<el-button
|
||
type="primary"
|
||
:icon="actionButtonLikeSearch ? undefined : 'el-icon-plus'"
|
||
v-if="canCreate"
|
||
@click="openBusinessForm('add')"
|
||
>{{ config.createText || '新增' }}
|
||
</el-button>
|
||
<el-button
|
||
type="primary"
|
||
icon="el-icon-upload"
|
||
plain
|
||
v-if="config.importUrl && hasPermission(`${config.permission}_import`) && !readonlyScope"
|
||
@click="handleImport"
|
||
>{{ config.importText || '批量导入' }}
|
||
</el-button>
|
||
<el-button
|
||
type="primary"
|
||
icon="el-icon-download"
|
||
plain
|
||
v-if="config.templateUrl && hasPermission(`${config.permission}_template`)"
|
||
@click="handleTemplate"
|
||
>下载模板
|
||
</el-button>
|
||
<el-button
|
||
type="primary"
|
||
icon="el-icon-download"
|
||
plain
|
||
v-if="config.exportUrl && hasPermission(`${config.permission}_export`)"
|
||
@click="handleExport"
|
||
>批量导出
|
||
</el-button>
|
||
<el-button
|
||
type="success"
|
||
icon="el-icon-check"
|
||
plain
|
||
v-if="canBatchComplete"
|
||
@click="handleBatchComplete"
|
||
>批量完成
|
||
</el-button>
|
||
<el-button
|
||
type="primary"
|
||
:plain="!actionButtonLikeSearch"
|
||
v-if="canRoadLoading"
|
||
@click="handleRoadLoading"
|
||
>{{ config.roadLoadingText || '公路配载' }}
|
||
</el-button>
|
||
<el-button
|
||
type="danger"
|
||
icon="el-icon-delete"
|
||
plain
|
||
v-if="
|
||
config.batchDelete !== false &&
|
||
hasPermission(`${config.permission}_delete`) &&
|
||
!readonlyScope
|
||
"
|
||
@click="handleDelete"
|
||
>批量删除
|
||
</el-button>
|
||
</template>
|
||
|
||
<template #status="{ row }">
|
||
<el-tag :type="statusTagType(row.status)" class="status-text">
|
||
{{ displayStatus(row, 'status') }}
|
||
</el-tag>
|
||
</template>
|
||
|
||
<template #businessStatus="{ row }">
|
||
<span class="waybill-manage-page__status-cell">
|
||
<el-tag :type="statusTagType(row.businessStatus)" class="status-text">
|
||
{{ displayStatus(row, 'businessStatus') }}
|
||
</el-tag>
|
||
<el-tooltip
|
||
v-if="driverRejectReasonText(row)"
|
||
:content="driverRejectReasonText(row)"
|
||
placement="top"
|
||
>
|
||
<el-icon class="waybill-manage-page__reject-tip-icon">
|
||
<WarnTriangleFilled />
|
||
</el-icon>
|
||
</el-tooltip>
|
||
</span>
|
||
</template>
|
||
|
||
<template #approvalStatus="{ row }">
|
||
<el-tag :type="statusTagType(row.approvalStatus)" class="status-text">
|
||
{{ displayStatus(row, 'approvalStatus') }}
|
||
</el-tag>
|
||
</template>
|
||
|
||
<template #goodsInfo="{ row }">
|
||
<span>{{ formatWaybillGoodsInfo(row) || '-' }}</span>
|
||
</template>
|
||
|
||
<template #waybillNo="{ row }">
|
||
<el-link v-if="row.waybillNo" type="primary" @click.stop="openDetail(row)">
|
||
{{ row.waybillNo }}
|
||
</el-link>
|
||
<span v-else>-</span>
|
||
</template>
|
||
|
||
<template #contractName="{ row }">
|
||
<div class="waybill-manage-page__contract-name">
|
||
<span>{{ row.contractName || '-' }}</span>
|
||
</div>
|
||
</template>
|
||
|
||
<template #loadingNo="{ row }">
|
||
<el-link v-if="row.loadingNo" type="primary" @click.stop="openLoadingDetail(row)">
|
||
{{ row.loadingNo }}
|
||
</el-link>
|
||
<span v-else>-</span>
|
||
</template>
|
||
|
||
<template #departureAddress="{ row }">
|
||
<el-tooltip v-if="row.departureAddress" :content="row.departureAddress" placement="top">
|
||
<span>{{ formatWaybillListAddress(row.departureAddress) }}</span>
|
||
</el-tooltip>
|
||
<span v-else>-</span>
|
||
</template>
|
||
|
||
<template #arrivalAddress="{ row }">
|
||
<el-tooltip v-if="row.arrivalAddress" :content="row.arrivalAddress" placement="top">
|
||
<span>{{ formatWaybillListAddress(row.arrivalAddress) }}</span>
|
||
</el-tooltip>
|
||
<span v-else>-</span>
|
||
</template>
|
||
|
||
<template #basicInfoTitle-form>
|
||
<div class="dialog-section-title">基本信息</div>
|
||
</template>
|
||
|
||
<template v-if="config.enableProjectSelect" #projectName-label>项目</template>
|
||
|
||
<template #relationNo-label>
|
||
<span class="waybill-manage-page__label-with-info">
|
||
<span>关联单号</span>
|
||
<el-tooltip content="仅做关联标记,用于业务中子母单情况" placement="top">
|
||
<el-icon class="waybill-manage-page__info-icon"><InfoFilled /></el-icon>
|
||
</el-tooltip>
|
||
</span>
|
||
</template>
|
||
|
||
<template #projectName-form>
|
||
<el-select
|
||
v-model="selectedProjectId"
|
||
class="waybill-manage-page__field"
|
||
:placeholder="config.projectPlaceholder || '请选择项目名称'"
|
||
filterable
|
||
clearable
|
||
:loading="projectLoading"
|
||
:disabled="dialogReadonly"
|
||
@visible-change="visible => visible && loadProjectOptions()"
|
||
@change="handleProjectChange"
|
||
>
|
||
<el-option
|
||
v-for="item in projectOptions"
|
||
:key="item.id"
|
||
:label="item.projectName"
|
||
:value="item.id"
|
||
/>
|
||
</el-select>
|
||
</template>
|
||
|
||
<template #contractName-form>
|
||
<el-select
|
||
v-if="contractSelectEnabled"
|
||
v-model="form.contractId"
|
||
class="waybill-manage-page__field"
|
||
placeholder="请选择客户合同"
|
||
filterable
|
||
clearable
|
||
:loading="contractLoading"
|
||
:disabled="dialogReadonly || !form.projectId"
|
||
@visible-change="visible => visible && loadContractOptionsForProject()"
|
||
@change="handleContractChange"
|
||
>
|
||
<el-option
|
||
v-for="item in contractOptions"
|
||
:key="item.id"
|
||
:label="item.contractName"
|
||
:value="item.id"
|
||
/>
|
||
</el-select>
|
||
<el-input
|
||
v-else
|
||
v-model="form.contractName"
|
||
class="waybill-manage-page__field"
|
||
placeholder="请输入合同名称"
|
||
clearable
|
||
maxlength="100"
|
||
show-word-limit
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</template>
|
||
|
||
<template #planName-form>
|
||
<el-select
|
||
v-model="form.planId"
|
||
class="waybill-manage-page__field"
|
||
placeholder="请选择计划名称"
|
||
filterable
|
||
clearable
|
||
:loading="shippingPlanLoading"
|
||
:disabled="dialogReadonly || isTemplateCreate"
|
||
@visible-change="visible => visible && loadShippingPlanOptions()"
|
||
@change="handleShippingPlanChange"
|
||
>
|
||
<el-option
|
||
v-for="item in shippingPlanOptions"
|
||
:key="item.id || item.planName"
|
||
:label="item.planName"
|
||
:value="item.id"
|
||
/>
|
||
</el-select>
|
||
</template>
|
||
|
||
<template #shippingInfoTitle-form>
|
||
<div
|
||
class="dialog-section-title dialog-section-title--action waybill-manage-page__shipping-title"
|
||
>
|
||
<span>收发货信息</span>
|
||
<el-link
|
||
v-if="!dialogReadonly"
|
||
class="waybill-manage-page__shipping-route-action"
|
||
type="primary"
|
||
:disabled="transportAddressSelectDisabled"
|
||
@click="openTransportRouteDialog"
|
||
>
|
||
选择线路
|
||
</el-link>
|
||
</div>
|
||
</template>
|
||
|
||
<template #departureAddress-form>
|
||
<div class="waybill-manage-page__route-address-row">
|
||
<el-input
|
||
v-model="form.departureName"
|
||
readonly
|
||
:placeholder="transportStationNamePlaceholder"
|
||
:disabled="transportAddressSelectDisabled"
|
||
@click="openTransportPrimaryAddressPicker('departure')"
|
||
>
|
||
<template v-if="transportStationMode" #suffix>
|
||
<el-icon
|
||
class="waybill-manage-page__map-suffix"
|
||
@click.stop="openTransportSecondaryAddressPicker('departure')"
|
||
>
|
||
<Search />
|
||
</el-icon>
|
||
</template>
|
||
</el-input>
|
||
<el-input
|
||
v-model="form.departureAddress"
|
||
:readonly="transportStationMode || !config.editableRoadAddress"
|
||
:placeholder="transportAddressDetailPlaceholder"
|
||
:suffix-icon="transportStationMode || config.editableRoadAddress ? undefined : Location"
|
||
:disabled="transportAddressSelectDisabled"
|
||
@click="handleTransportSecondaryAddressInputClick('departure')"
|
||
>
|
||
<template v-if="!transportStationMode && config.editableRoadAddress" #suffix>
|
||
<el-icon
|
||
class="waybill-manage-page__map-suffix"
|
||
@click.stop="openTransportMapDialog('departure')"
|
||
>
|
||
<Location />
|
||
</el-icon>
|
||
</template>
|
||
</el-input>
|
||
<el-tooltip content="选择常用地址" placement="top">
|
||
<el-button
|
||
class="waybill-manage-page__address-pick-btn"
|
||
type="primary"
|
||
:icon="OfficeBuilding"
|
||
link
|
||
:disabled="transportAddressSelectDisabled"
|
||
@click="openTransportAddressPicker('departure')"
|
||
/>
|
||
</el-tooltip>
|
||
<span class="waybill-manage-page__route-label">联系人</span>
|
||
<el-input
|
||
v-model="form.departureContact"
|
||
placeholder="请输入"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
<span class="waybill-manage-page__route-label">联系方式</span>
|
||
<el-input v-model="form.departurePhone" placeholder="请输入" :disabled="dialogReadonly" />
|
||
</div>
|
||
</template>
|
||
|
||
<template #arrivalAddress-form>
|
||
<div class="waybill-manage-page__route-address-row">
|
||
<el-input
|
||
v-model="form.arrivalName"
|
||
readonly
|
||
:placeholder="transportStationNamePlaceholder"
|
||
:disabled="transportAddressSelectDisabled"
|
||
@click="openTransportPrimaryAddressPicker('arrival')"
|
||
>
|
||
<template v-if="transportStationMode" #suffix>
|
||
<el-icon
|
||
class="waybill-manage-page__map-suffix"
|
||
@click.stop="openTransportSecondaryAddressPicker('arrival')"
|
||
>
|
||
<Search />
|
||
</el-icon>
|
||
</template>
|
||
</el-input>
|
||
<el-input
|
||
v-model="form.arrivalAddress"
|
||
:readonly="transportStationMode || !config.editableRoadAddress"
|
||
:placeholder="transportAddressDetailPlaceholder"
|
||
:suffix-icon="transportStationMode || config.editableRoadAddress ? undefined : Location"
|
||
:disabled="transportAddressSelectDisabled"
|
||
@click="handleTransportSecondaryAddressInputClick('arrival')"
|
||
>
|
||
<template v-if="!transportStationMode && config.editableRoadAddress" #suffix>
|
||
<el-icon
|
||
class="waybill-manage-page__map-suffix"
|
||
@click.stop="openTransportMapDialog('arrival')"
|
||
>
|
||
<Location />
|
||
</el-icon>
|
||
</template>
|
||
</el-input>
|
||
<el-tooltip content="选择常用地址" placement="top">
|
||
<el-button
|
||
class="waybill-manage-page__address-pick-btn"
|
||
type="primary"
|
||
:icon="OfficeBuilding"
|
||
link
|
||
:disabled="transportAddressSelectDisabled"
|
||
@click="openTransportAddressPicker('arrival')"
|
||
/>
|
||
</el-tooltip>
|
||
<span class="waybill-manage-page__route-label">联系人</span>
|
||
<el-input v-model="form.arrivalContact" placeholder="请输入" :disabled="dialogReadonly" />
|
||
<span class="waybill-manage-page__route-label">联系方式</span>
|
||
<el-input v-model="form.arrivalPhone" placeholder="请输入" :disabled="dialogReadonly" />
|
||
</div>
|
||
</template>
|
||
|
||
<template #taskInfoTitle-form>
|
||
<div class="waybill-manage-page__task-mode-switch">
|
||
<el-segmented
|
||
v-model="form.taskEntryMode"
|
||
:options="taskEntryModeOptions"
|
||
:disabled="dialogReadonly"
|
||
@change="handleTaskEntryModeChange"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<template #taskInfoForm-form>
|
||
<div class="waybill-manage-page__task">
|
||
<div v-if="isTaskFullMode" class="waybill-manage-page__task-full">
|
||
<div class="waybill-manage-page__subsection">
|
||
<div
|
||
class="dialog-section-title dialog-section-title--action waybill-manage-page__goods-title"
|
||
>
|
||
<span>货物信息</span>
|
||
<div v-if="!dialogReadonly" class="waybill-manage-page__section-actions">
|
||
<!-- <el-link type="primary" @click="cargoImportBox = true">导入货物</el-link> -->
|
||
<el-link type="primary" @click="openCommonCargoDialog">常用货物</el-link>
|
||
</div>
|
||
</div>
|
||
<div class="waybill-manage-page__cargo-wrap">
|
||
<el-table
|
||
:data="transportCargoRows"
|
||
border
|
||
class="waybill-manage-page__cargo-table waybill-manage-page__cargo-table--task"
|
||
empty-text="暂无货物信息"
|
||
>
|
||
<el-table-column
|
||
type="index"
|
||
label="序号"
|
||
width="70"
|
||
align="center"
|
||
fixed="left"
|
||
/>
|
||
<el-table-column min-width="220" align="center">
|
||
<template #header>
|
||
<span class="waybill-manage-page__required-column">货物类型</span>
|
||
</template>
|
||
<template #default="{ row }">
|
||
<el-cascader
|
||
v-model="row.cargoTypePath"
|
||
:options="transportCargoTypeOptions"
|
||
:props="transportCargoTypeCascaderProps"
|
||
placeholder="从货物类型获取"
|
||
clearable
|
||
filterable
|
||
:disabled="dialogReadonly"
|
||
:filter-method="filterCargoType"
|
||
@visible-change="visible => visible && ensureCargoTypeOptions()"
|
||
@change="value => handleTransportCargoTypeChange(row, value)"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column min-width="240" align="center">
|
||
<template #header>
|
||
<span class="waybill-manage-page__required-column">货物名称</span>
|
||
</template>
|
||
<template #default="{ row }">
|
||
<el-autocomplete
|
||
v-model="row.cargoName"
|
||
placeholder="请输入"
|
||
clearable
|
||
:debounce="300"
|
||
:fetch-suggestions="
|
||
(queryString, callback) =>
|
||
fetchTaskCargoSuggestions(queryString, callback, row.cargoTypePath)
|
||
"
|
||
:disabled="dialogReadonly"
|
||
:loading="isTaskCargoOptionsLoading(row.cargoTypePath)"
|
||
@select="item => handleTaskCargoRowSelect(row, item)"
|
||
>
|
||
<template #default="{ item }">
|
||
<div class="waybill-manage-page__cargo-autocomplete-item">
|
||
<span>{{ formatCargoTypeLabel(item) }}</span>
|
||
<small v-if="item.cargoCode">{{ item.cargoCode }}</small>
|
||
</div>
|
||
</template>
|
||
</el-autocomplete>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="包装" min-width="130" align="center">
|
||
<template #default="{ row }">
|
||
<el-select v-model="row.packageType" clearable :disabled="dialogReadonly">
|
||
<el-option
|
||
v-for="item in packageOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column min-width="130" align="center">
|
||
<template #header>
|
||
<span class="waybill-manage-page__required-column">数量</span>
|
||
</template>
|
||
<template #default="{ row }">
|
||
<el-input
|
||
v-model="row.quantity"
|
||
placeholder="请输入"
|
||
:disabled="dialogReadonly"
|
||
@input="value => handleTransportCargoQuantityInput(row, value)"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column min-width="140" align="center">
|
||
<template #header>
|
||
<span class="waybill-manage-page__required-column">数量单位</span>
|
||
</template>
|
||
<template #default="{ row }">
|
||
<el-select
|
||
v-model="row.quantityUnit"
|
||
clearable
|
||
:disabled="dialogReadonly"
|
||
@visible-change="visible => visible && loadTaskQuantityUnitOptions()"
|
||
>
|
||
<el-option
|
||
v-for="item in taskQuantityUnitOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="品牌" min-width="130" align="center">
|
||
<template #default="{ row }">
|
||
<el-input
|
||
v-model="row.brand"
|
||
placeholder="请输入"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="规格" min-width="130" align="center">
|
||
<template #default="{ row }">
|
||
<el-input
|
||
v-model="row.specification"
|
||
placeholder="请输入"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="型号" min-width="130" align="center">
|
||
<template #default="{ row }">
|
||
<el-input
|
||
v-model="row.model"
|
||
placeholder="请输入"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="物料编码" min-width="150" align="center">
|
||
<template #default="{ row }">
|
||
<el-input
|
||
v-model="row.materialCode"
|
||
placeholder="请输入"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="设备编码" min-width="150" align="center">
|
||
<template #default="{ row }">
|
||
<el-input
|
||
v-model="row.deviceCode"
|
||
placeholder="请输入"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="备注" min-width="180" align="center">
|
||
<template #default="{ row }">
|
||
<el-input
|
||
v-model="row.remark"
|
||
placeholder="请输入"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="140" align="center" fixed="right">
|
||
<template #default="{ $index }">
|
||
<div class="waybill-manage-page__cargo-actions">
|
||
<el-link
|
||
v-if="!dialogReadonly && $index === transportCargoRows.length - 1"
|
||
type="primary"
|
||
@click="addTransportCargoRow($index)"
|
||
>
|
||
新增
|
||
</el-link>
|
||
<el-link
|
||
v-if="!dialogReadonly"
|
||
type="danger"
|
||
@click="removeTransportCargoRow($index)"
|
||
>
|
||
删除
|
||
</el-link>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
</div>
|
||
<div class="dialog-section-title waybill-manage-page__task-content-title">任务信息</div>
|
||
<el-form
|
||
:model="form"
|
||
label-position="right"
|
||
label-width="auto"
|
||
class="waybill-manage-page__task-form waybill-manage-page__task-form--full"
|
||
>
|
||
<div class="waybill-manage-page__task-row">
|
||
<el-form-item
|
||
label="承运类型"
|
||
required
|
||
class="waybill-manage-page__task-carrier-type waybill-manage-page__task-span-4"
|
||
>
|
||
<el-radio-group
|
||
v-model="form.carrierType"
|
||
:disabled="dialogReadonly"
|
||
@change="handleTaskCarrierTypeChange"
|
||
>
|
||
<el-radio-button
|
||
v-for="item in waybillTaskCarrierTypes"
|
||
:key="item"
|
||
:label="item"
|
||
:value="item"
|
||
/>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
</div>
|
||
<div
|
||
v-if="hasTransportType"
|
||
class="waybill-manage-page__subsection waybill-manage-page__carrier-section"
|
||
>
|
||
<div class="waybill-manage-page__subsection-head">
|
||
<span>承运信息</span>
|
||
</div>
|
||
<div class="waybill-manage-page__task-row">
|
||
<el-form-item
|
||
:label="taskCarrierLabel"
|
||
:required="taskCarrierRequired"
|
||
class="waybill-manage-page__task-span-1"
|
||
>
|
||
<el-select
|
||
v-model="taskCarrierValue"
|
||
:placeholder="`请选择${taskCarrierLabel}`"
|
||
filterable
|
||
clearable
|
||
:suffix-icon="Search"
|
||
:loading="taskCarrierLoading"
|
||
:disabled="dialogReadonly || !form.projectId"
|
||
@visible-change="visible => visible && loadTaskCarrierOptions()"
|
||
>
|
||
<el-option
|
||
v-for="item in taskCarrierOptions"
|
||
:key="
|
||
item.id ||
|
||
item.customerName ||
|
||
item.carrierName ||
|
||
item.fullName ||
|
||
item.name ||
|
||
item.customerCode
|
||
"
|
||
:label="
|
||
item.label ||
|
||
item.customerName ||
|
||
item.carrierName ||
|
||
item.fullName ||
|
||
item.name
|
||
"
|
||
:value="taskCarrierOptionValue(item)"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item
|
||
v-if="!isNonRoadTransport"
|
||
label="司机"
|
||
class="waybill-manage-page__task-span-1"
|
||
>
|
||
<el-autocomplete
|
||
v-model="form.driverName"
|
||
placeholder="请输入"
|
||
clearable
|
||
:debounce="300"
|
||
:fetch-suggestions="fetchTaskDriverSuggestions"
|
||
:loading="taskDriverLoading"
|
||
:disabled="dialogReadonly"
|
||
@select="handleTaskDriverSelect"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item
|
||
v-if="!isNonRoadTransport"
|
||
label="手机号"
|
||
class="waybill-manage-page__task-span-1"
|
||
>
|
||
<el-input
|
||
v-model="form.driverPhone"
|
||
placeholder="请输入"
|
||
clearable
|
||
maxlength="20"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item
|
||
:label="transportVehicleNoLabel"
|
||
required
|
||
class="waybill-manage-page__task-span-1"
|
||
>
|
||
<el-input
|
||
v-model="form.vehicleNo"
|
||
:placeholder="`请输入${transportVehicleNoLabel}`"
|
||
clearable
|
||
maxlength="30"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item
|
||
v-if="isNonRoadTransport"
|
||
label="船长"
|
||
class="waybill-manage-page__task-span-1"
|
||
>
|
||
<el-input
|
||
v-model="form.captainName"
|
||
placeholder="请输入"
|
||
clearable
|
||
maxlength="20"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item
|
||
v-if="isNonRoadTransport"
|
||
label="联系电话"
|
||
class="waybill-manage-page__task-span-1"
|
||
>
|
||
<el-input
|
||
v-model="form.driverPhone"
|
||
placeholder="请输入"
|
||
clearable
|
||
maxlength="20"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item
|
||
v-if="isNonRoadTransport"
|
||
label="箱号"
|
||
class="waybill-manage-page__task-span-1"
|
||
>
|
||
<el-input
|
||
v-model="form.containerNo"
|
||
placeholder="请输入"
|
||
clearable
|
||
maxlength="30"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item
|
||
v-if="isNonRoadTransport"
|
||
label="舱位"
|
||
class="waybill-manage-page__task-span-1"
|
||
>
|
||
<el-input
|
||
v-model="form.cabinNo"
|
||
placeholder="请输入"
|
||
clearable
|
||
maxlength="30"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</el-form-item>
|
||
</div>
|
||
<div v-if="!isNonRoadTransport" class="waybill-manage-page__task-row">
|
||
<el-form-item
|
||
v-if="showRoadEscortFields"
|
||
label="挂车车牌号"
|
||
class="waybill-manage-page__task-span-1"
|
||
>
|
||
<el-input
|
||
v-model="form.trailerVehicleNo"
|
||
placeholder="请输入"
|
||
clearable
|
||
maxlength="30"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item
|
||
v-if="showRoadEscortFields"
|
||
label="押运人"
|
||
class="waybill-manage-page__task-span-1"
|
||
>
|
||
<el-autocomplete
|
||
v-model="form.escortName"
|
||
placeholder="请输入"
|
||
clearable
|
||
:debounce="300"
|
||
:fetch-suggestions="fetchTaskEscortSuggestions"
|
||
:loading="taskEscortLoading"
|
||
:disabled="dialogReadonly"
|
||
@select="handleTaskEscortSelect"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item
|
||
v-if="showRoadEscortFields"
|
||
label="押运人手机号"
|
||
class="waybill-manage-page__task-span-1"
|
||
>
|
||
<el-input
|
||
v-model="form.escortPhone"
|
||
placeholder="请输入"
|
||
clearable
|
||
maxlength="20"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item
|
||
v-if="!isNonRoadTransport"
|
||
label="里程(km)"
|
||
class="waybill-manage-page__task-span-1"
|
||
>
|
||
<el-input
|
||
v-model="form.mileage"
|
||
placeholder="请输入"
|
||
clearable
|
||
maxlength="10"
|
||
inputmode="numeric"
|
||
:disabled="dialogReadonly"
|
||
@input="handleTaskMileageInput"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item
|
||
:label="isRoadTransport ? '计划发货日期' : '预计发货日期'"
|
||
class="waybill-manage-page__task-span-1"
|
||
>
|
||
<el-date-picker
|
||
v-model="form.estimatedStartTime"
|
||
type="date"
|
||
placeholder="默认填充当天"
|
||
format="YYYY-MM-DD"
|
||
value-format="YYYY-MM-DD"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item
|
||
:label="isRoadTransport ? '计划完成日期' : '预计完成日期'"
|
||
class="waybill-manage-page__task-span-1"
|
||
>
|
||
<el-date-picker
|
||
v-model="form.estimatedEndTime"
|
||
type="date"
|
||
placeholder="请输入"
|
||
format="YYYY-MM-DD"
|
||
value-format="YYYY-MM-DD"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</el-form-item>
|
||
</div>
|
||
<div class="waybill-manage-page__task-row">
|
||
<el-form-item
|
||
label="备注"
|
||
class="waybill-manage-page__task-note waybill-manage-page__task-span-4"
|
||
>
|
||
<el-input
|
||
v-model="form.taskRemark"
|
||
type="textarea"
|
||
:rows="2"
|
||
maxlength="200"
|
||
show-word-limit
|
||
:disabled="dialogReadonly"
|
||
placeholder="请输入,200字以内"
|
||
/>
|
||
</el-form-item>
|
||
</div>
|
||
</div>
|
||
<template v-else-if="hasTransportType">
|
||
<div v-if="isRoadTransport" class="waybill-manage-page__task-row">
|
||
<el-form-item
|
||
label="司机"
|
||
:required="isRoadTransport"
|
||
class="waybill-manage-page__task-span-1"
|
||
>
|
||
<el-autocomplete
|
||
v-model="form.driverName"
|
||
placeholder="请输入"
|
||
clearable
|
||
:debounce="300"
|
||
:fetch-suggestions="fetchTaskDriverSuggestions"
|
||
:loading="taskDriverLoading"
|
||
:disabled="dialogReadonly"
|
||
@select="handleTaskDriverSelect"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="手机号" required class="waybill-manage-page__task-span-1">
|
||
<el-input
|
||
v-model="form.driverPhone"
|
||
placeholder="请输入"
|
||
clearable
|
||
maxlength="20"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="车牌号" required class="waybill-manage-page__task-span-1">
|
||
<el-input
|
||
v-model="form.vehicleNo"
|
||
placeholder="请输入"
|
||
clearable
|
||
maxlength="30"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="挂车车牌号" class="waybill-manage-page__task-span-1">
|
||
<el-input
|
||
v-model="form.trailerVehicleNo"
|
||
placeholder="请输入"
|
||
clearable
|
||
maxlength="30"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</el-form-item>
|
||
</div>
|
||
<div v-if="isRoadTransport" class="waybill-manage-page__task-row">
|
||
<el-form-item label="押运人" class="waybill-manage-page__task-span-1">
|
||
<el-autocomplete
|
||
v-model="form.escortName"
|
||
placeholder="请输入"
|
||
clearable
|
||
:debounce="300"
|
||
:fetch-suggestions="fetchTaskEscortSuggestions"
|
||
:loading="taskEscortLoading"
|
||
:disabled="dialogReadonly"
|
||
@select="handleTaskEscortSelect"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="押运人手机号" class="waybill-manage-page__task-span-1">
|
||
<el-input
|
||
v-model="form.escortPhone"
|
||
placeholder="请输入"
|
||
clearable
|
||
maxlength="20"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="里程(km)" class="waybill-manage-page__task-span-1">
|
||
<el-input
|
||
v-model="form.mileage"
|
||
placeholder="请输入"
|
||
clearable
|
||
maxlength="10"
|
||
inputmode="numeric"
|
||
:disabled="dialogReadonly"
|
||
@input="handleTaskMileageInput"
|
||
/>
|
||
</el-form-item>
|
||
</div>
|
||
<div v-if="isRoadTransport" class="waybill-manage-page__task-row">
|
||
<el-form-item
|
||
:label="isRoadTransport ? '计划发货日期' : '预计发货日期'"
|
||
class="waybill-manage-page__task-span-1"
|
||
>
|
||
<el-date-picker
|
||
v-model="form.estimatedStartTime"
|
||
type="date"
|
||
placeholder="默认填充当天"
|
||
format="YYYY-MM-DD"
|
||
value-format="YYYY-MM-DD"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item
|
||
:label="isRoadTransport ? '计划完成日期' : '预计完成日期'"
|
||
class="waybill-manage-page__task-span-1"
|
||
>
|
||
<el-date-picker
|
||
v-model="form.estimatedEndTime"
|
||
type="date"
|
||
placeholder="请输入"
|
||
format="YYYY-MM-DD"
|
||
value-format="YYYY-MM-DD"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</el-form-item>
|
||
</div>
|
||
<div v-if="isRoadTransport" class="waybill-manage-page__task-row">
|
||
<el-form-item
|
||
label="备注"
|
||
class="waybill-manage-page__task-note waybill-manage-page__task-span-4"
|
||
>
|
||
<el-input
|
||
v-model="form.taskRemark"
|
||
type="textarea"
|
||
:rows="2"
|
||
maxlength="200"
|
||
show-word-limit
|
||
:disabled="dialogReadonly"
|
||
placeholder="请输入,200字以内"
|
||
/>
|
||
</el-form-item>
|
||
</div>
|
||
<template v-if="isNonRoadTransport">
|
||
<div class="waybill-manage-page__task-row">
|
||
<el-form-item
|
||
:label="transportVehicleNoLabel"
|
||
required
|
||
class="waybill-manage-page__task-span-1"
|
||
>
|
||
<el-input
|
||
v-model="form.vehicleNo"
|
||
:placeholder="`请输入${transportVehicleNoLabel}`"
|
||
clearable
|
||
maxlength="30"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="船长" class="waybill-manage-page__task-span-1">
|
||
<el-input
|
||
v-model="form.captainName"
|
||
placeholder="请输入"
|
||
clearable
|
||
maxlength="20"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="联系电话" class="waybill-manage-page__task-span-1">
|
||
<el-input
|
||
v-model="form.driverPhone"
|
||
placeholder="请输入"
|
||
clearable
|
||
maxlength="20"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="箱号" class="waybill-manage-page__task-span-1">
|
||
<el-input
|
||
v-model="form.containerNo"
|
||
placeholder="请输入"
|
||
clearable
|
||
maxlength="30"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</el-form-item>
|
||
</div>
|
||
<div class="waybill-manage-page__task-row">
|
||
<el-form-item label="舱位" class="waybill-manage-page__task-span-1">
|
||
<el-input
|
||
v-model="form.cabinNo"
|
||
placeholder="请输入"
|
||
clearable
|
||
maxlength="30"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</el-form-item>
|
||
</div>
|
||
<div class="waybill-manage-page__task-row">
|
||
<el-form-item
|
||
label="备注"
|
||
class="waybill-manage-page__task-note waybill-manage-page__task-span-4"
|
||
>
|
||
<el-input
|
||
v-model="form.remark"
|
||
type="textarea"
|
||
:rows="2"
|
||
maxlength="200"
|
||
show-word-limit
|
||
:disabled="dialogReadonly"
|
||
placeholder="请输入,200字以内"
|
||
/>
|
||
</el-form-item>
|
||
</div>
|
||
</template>
|
||
</template>
|
||
</el-form>
|
||
<div class="waybill-manage-page__subsection">
|
||
<div class="waybill-manage-page__subsection-head">
|
||
<span class="waybill-manage-page__label-with-info">
|
||
<span>运费信息</span>
|
||
<el-tooltip content="可选填写,仅做记录,不影响应付结算" placement="top">
|
||
<el-icon class="waybill-manage-page__info-icon"><InfoFilled /></el-icon>
|
||
</el-tooltip>
|
||
</span>
|
||
</div>
|
||
<el-form
|
||
:model="form"
|
||
label-position="right"
|
||
label-width="auto"
|
||
class="waybill-manage-page__freight-form waybill-manage-page__freight-form--road"
|
||
>
|
||
<template v-for="group in taskFreightGroups" :key="group.key">
|
||
<el-form-item label="单价">
|
||
<el-input
|
||
:model-value="taskFullFreightGroupUnitPrice(group)"
|
||
placeholder="请输入"
|
||
clearable
|
||
:disabled="dialogReadonly"
|
||
@input="
|
||
value => handleTaskFullFreightGroupNumberInput(group, 'unitPrice', value)
|
||
"
|
||
@clear="() => handleTaskFullFreightGroupNumberInput(group, 'unitPrice', '')"
|
||
>
|
||
<template #append>
|
||
<el-select
|
||
:model-value="taskFullFreightGroupPriceUnit(group)"
|
||
class="waybill-manage-page__append-select waybill-manage-page__append-select--wide"
|
||
placeholder="单位"
|
||
:disabled="dialogReadonly"
|
||
@change="value => handleTaskFullFreightGroupPriceUnitChange(group, value)"
|
||
@visible-change="visible => visible && loadTaskFeeUnitOptions()"
|
||
>
|
||
<el-option
|
||
v-for="item in taskFeeUnitOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
<el-form-item label="数量合计">
|
||
<el-input :model-value="taskFullFreightGroupQuantity(group)" disabled>
|
||
<template #append>
|
||
<el-select
|
||
:model-value="group.quantityUnit"
|
||
class="waybill-manage-page__append-select waybill-manage-page__append-select--wide"
|
||
placeholder="单位"
|
||
disabled
|
||
>
|
||
<el-option :label="fixedQuantityUnit" :value="fixedQuantityUnit" />
|
||
</el-select>
|
||
</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
<el-form-item label="运费">
|
||
<el-input
|
||
:model-value="taskFullFreightGroupAmount(group)"
|
||
placeholder="请输入运费"
|
||
:disabled="dialogReadonly"
|
||
@input="value => handleTaskFullFreightGroupAmountInput(group, value)"
|
||
@clear="() => handleTaskFullFreightGroupAmountInput(group, '')"
|
||
>
|
||
<template #suffix>{{ taskFreightCurrencyLabel }}</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
</template>
|
||
<el-form-item label="运费合计">
|
||
<el-input :model-value="taskFullFreightTotal" disabled>
|
||
<template #suffix>{{ taskFreightCurrencyLabel }}</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
<el-form-item label="其他运费合计">
|
||
<el-input
|
||
v-model="form.otherFeeTotal"
|
||
placeholder="请输入"
|
||
clearable
|
||
:disabled="dialogReadonly"
|
||
@input="value => handleTaskNumberInput('otherFeeTotal', value)"
|
||
>
|
||
<template #suffix>{{ taskFreightCurrencyLabel }}</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
</div>
|
||
<div
|
||
v-if="!isTaskFullMode"
|
||
class="dialog-section-title waybill-manage-page__task-content-title"
|
||
>
|
||
任务信息
|
||
</div>
|
||
<el-form
|
||
v-if="!isTaskFullMode"
|
||
:model="form"
|
||
label-position="right"
|
||
label-width="auto"
|
||
class="waybill-manage-page__task-form"
|
||
>
|
||
<el-form-item
|
||
label="承运类型"
|
||
required
|
||
class="waybill-manage-page__task-carrier-type waybill-manage-page__task-span-4"
|
||
>
|
||
<el-radio-group
|
||
v-model="form.carrierType"
|
||
:disabled="dialogReadonly"
|
||
@change="handleTaskCarrierTypeChange"
|
||
>
|
||
<el-radio-button
|
||
v-for="item in waybillTaskCarrierTypes"
|
||
:key="item"
|
||
:label="item"
|
||
:value="item"
|
||
/>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
<template v-if="hasTransportType">
|
||
<el-form-item :label="taskCarrierLabel" :required="taskCarrierRequired">
|
||
<el-select
|
||
v-model="taskCarrierValue"
|
||
:placeholder="`请选择${taskCarrierLabel}`"
|
||
filterable
|
||
clearable
|
||
:loading="taskCarrierLoading"
|
||
:disabled="dialogReadonly || !form.projectId"
|
||
@visible-change="visible => visible && loadTaskCarrierOptions()"
|
||
>
|
||
<el-option
|
||
v-for="item in taskCarrierOptions"
|
||
:key="
|
||
item.id ||
|
||
item.customerName ||
|
||
item.carrierName ||
|
||
item.fullName ||
|
||
item.name ||
|
||
item.customerCode
|
||
"
|
||
:label="
|
||
item.label ||
|
||
item.customerName ||
|
||
item.carrierName ||
|
||
item.fullName ||
|
||
item.name
|
||
"
|
||
:value="taskCarrierOptionValue(item)"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item
|
||
v-if="isRoadTransport"
|
||
label="司机"
|
||
:required="form.carrierType === '自运'"
|
||
>
|
||
<el-autocomplete
|
||
v-model="form.driverName"
|
||
placeholder="请输入司机"
|
||
clearable
|
||
:debounce="300"
|
||
:fetch-suggestions="fetchTaskDriverSuggestions"
|
||
:loading="taskDriverLoading"
|
||
:disabled="dialogReadonly"
|
||
@select="handleTaskDriverSelect"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item
|
||
v-if="isRoadTransport"
|
||
label="手机号"
|
||
:required="form.carrierType === '自运'"
|
||
>
|
||
<el-input
|
||
v-model="form.driverPhone"
|
||
placeholder="请输入手机号"
|
||
clearable
|
||
maxlength="20"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item :label="transportVehicleNoLabel" required>
|
||
<el-input
|
||
v-model="form.vehicleNo"
|
||
:placeholder="`请输入${transportVehicleNoLabel}`"
|
||
clearable
|
||
maxlength="30"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item v-if="isNonRoadTransport" label="船长">
|
||
<el-input
|
||
v-model="form.captainName"
|
||
placeholder="请输入船长"
|
||
clearable
|
||
maxlength="20"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item v-if="isNonRoadTransport && isCarrierMode" label="联系电话">
|
||
<el-input
|
||
v-model="form.driverPhone"
|
||
placeholder="请输入联系电话"
|
||
clearable
|
||
maxlength="20"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item v-if="isNonRoadTransport" label="箱号">
|
||
<el-input
|
||
v-model="form.containerNo"
|
||
placeholder="请输入箱号"
|
||
clearable
|
||
maxlength="30"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item v-if="isNonRoadTransport" label="舱位">
|
||
<el-input
|
||
v-model="form.cabinNo"
|
||
placeholder="请输入舱位"
|
||
clearable
|
||
maxlength="30"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item v-if="showRoadEscortFields" label="挂车车牌号">
|
||
<el-input
|
||
v-model="form.trailerVehicleNo"
|
||
placeholder="请输入挂车车牌号"
|
||
clearable
|
||
maxlength="30"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item v-if="showRoadEscortFields" label="押运人">
|
||
<el-autocomplete
|
||
v-model="form.escortName"
|
||
placeholder="请输入押运人"
|
||
clearable
|
||
:debounce="300"
|
||
:fetch-suggestions="fetchTaskEscortSuggestions"
|
||
:loading="taskEscortLoading"
|
||
:disabled="dialogReadonly"
|
||
@select="handleTaskEscortSelect"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item v-if="showRoadEscortFields" label="押运人手机号">
|
||
<el-input
|
||
v-model="form.escortPhone"
|
||
placeholder="请输入押运人手机号"
|
||
clearable
|
||
maxlength="20"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="货物类型" required>
|
||
<el-cascader
|
||
v-model="form.cargoTypePath"
|
||
:options="transportCargoTypeOptions"
|
||
:props="transportCargoTypeCascaderProps"
|
||
placeholder="请选择到二级"
|
||
clearable
|
||
filterable
|
||
:disabled="dialogReadonly"
|
||
:filter-method="filterCargoType"
|
||
@visible-change="visible => visible && ensureCargoTypeOptions()"
|
||
@change="handleTaskCargoTypeChange"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="货物名称">
|
||
<el-autocomplete
|
||
v-model="form.cargoName"
|
||
placeholder="请输入货物名称"
|
||
clearable
|
||
:debounce="300"
|
||
:fetch-suggestions="
|
||
(queryString, callback) =>
|
||
fetchTaskCargoSuggestions(queryString, callback, form.cargoTypePath)
|
||
"
|
||
:disabled="dialogReadonly"
|
||
:loading="taskCargoLoading"
|
||
@select="handleTaskCargoSelect"
|
||
>
|
||
<template #default="{ item }">
|
||
<div class="waybill-manage-page__cargo-autocomplete-item">
|
||
<span>{{ formatCargoTypeLabel(item) }}</span>
|
||
<small v-if="item.cargoCode">{{ item.cargoCode }}</small>
|
||
</div>
|
||
</template>
|
||
</el-autocomplete>
|
||
</el-form-item>
|
||
<el-form-item label="规格">
|
||
<el-input
|
||
v-model="form.specification"
|
||
placeholder="选择货物自动回填"
|
||
clearable
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="型号">
|
||
<el-input
|
||
v-model="form.model"
|
||
placeholder="选择货物自动回填"
|
||
clearable
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="数量">
|
||
<el-input
|
||
v-model="form.quantity"
|
||
placeholder="请输入数量"
|
||
clearable
|
||
:disabled="dialogReadonly"
|
||
@input="value => handleTaskNumberInput('quantity', value)"
|
||
>
|
||
<template #append>
|
||
<el-select
|
||
v-model="form.quantityUnit"
|
||
class="waybill-manage-page__append-select waybill-manage-page__append-select--wide"
|
||
placeholder="单位"
|
||
:disabled="dialogReadonly"
|
||
@visible-change="visible => visible && loadTaskQuantityUnitOptions()"
|
||
>
|
||
<el-option
|
||
v-for="item in taskQuantityUnitOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
<el-form-item label="里程(km)">
|
||
<el-input
|
||
v-model="form.mileage"
|
||
placeholder="请输入里程"
|
||
clearable
|
||
maxlength="10"
|
||
inputmode="numeric"
|
||
:disabled="dialogReadonly"
|
||
@input="handleTaskMileageInput"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item :label="isRoadTransport ? '计划发货日期' : '预计发货日期'">
|
||
<el-date-picker
|
||
v-model="form.estimatedStartTime"
|
||
type="date"
|
||
placeholder="请选择预计发货日期"
|
||
format="YYYY-MM-DD"
|
||
value-format="YYYY-MM-DD"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item :label="isRoadTransport ? '计划完成日期' : '预计完成日期'">
|
||
<el-date-picker
|
||
v-model="form.estimatedEndTime"
|
||
type="date"
|
||
placeholder="请选择预计完成日期"
|
||
format="YYYY-MM-DD"
|
||
value-format="YYYY-MM-DD"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="单价">
|
||
<el-input
|
||
v-model="form.unitPrice"
|
||
placeholder="请输入单价"
|
||
clearable
|
||
:disabled="dialogReadonly"
|
||
@input="value => handleTaskNumberInput('unitPrice', value)"
|
||
>
|
||
<template #append>
|
||
<el-select
|
||
v-model="form.priceUnit"
|
||
class="waybill-manage-page__append-select waybill-manage-page__append-select--wide"
|
||
placeholder="单位"
|
||
:disabled="dialogReadonly"
|
||
@visible-change="visible => visible && loadTaskFeeUnitOptions()"
|
||
>
|
||
<el-option
|
||
v-for="item in taskFeeUnitOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
<el-form-item label="其他费用合计">
|
||
<el-input
|
||
v-model="form.otherFeeTotal"
|
||
placeholder="请输入其他费用合计"
|
||
clearable
|
||
:disabled="dialogReadonly"
|
||
@input="value => handleTaskNumberInput('otherFeeTotal', value)"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="备注" class="waybill-manage-page__task-remark">
|
||
<el-input
|
||
v-model="form.taskRemark"
|
||
type="textarea"
|
||
:rows="2"
|
||
maxlength="200"
|
||
show-word-limit
|
||
placeholder="请输入备注,200字以内"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</el-form-item>
|
||
</template>
|
||
</el-form>
|
||
</div>
|
||
</template>
|
||
|
||
<template #attachmentTitle-form>
|
||
<div class="dialog-section-title">{{ config.attachmentTitle || '其它附件' }}</div>
|
||
</template>
|
||
|
||
<template #attachmentsJson-form>
|
||
<div class="waybill-manage-page__attachment">
|
||
<div class="waybill-manage-page__attachment-head">
|
||
<el-button
|
||
type="primary"
|
||
:disabled="!attachmentRows.length"
|
||
@click="handleBatchDownload"
|
||
>
|
||
批量下载
|
||
</el-button>
|
||
</div>
|
||
<el-table
|
||
:data="attachmentRows"
|
||
border
|
||
class="waybill-manage-page__attachment-table"
|
||
@selection-change="handleAttachmentSelectionChange"
|
||
>
|
||
<el-table-column type="selection" width="55" align="center" />
|
||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||
<el-table-column label="文件名" min-width="200" align="left" show-overflow-tooltip>
|
||
<template #default="{ row }">
|
||
<el-link type="primary" @click="previewAttachment(row)">
|
||
{{ row.originalName || row.name }}
|
||
</el-link>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="附件描述" min-width="220" align="center">
|
||
<template #default="{ row }">
|
||
<el-input
|
||
v-model="row.description"
|
||
placeholder="请输入附件描述"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="文件大小" width="110" align="center">
|
||
<template #default="{ row }">{{ formatFileSize(row.size) }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="uploadUserName" label="上传人" width="140" align="center" />
|
||
<el-table-column
|
||
prop="uploadTime"
|
||
label="上传时间"
|
||
width="160"
|
||
align="center"
|
||
sortable
|
||
/>
|
||
<el-table-column label="操作" width="100" align="center" fixed="right">
|
||
<template #default="{ row, $index }">
|
||
<el-link v-if="!dialogReadonly" type="danger" @click="removeAttachment($index)">
|
||
删除
|
||
</el-link>
|
||
<el-link v-else type="primary" @click="downloadAttachment(row)"> 下载 </el-link>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div class="waybill-manage-page__attachment-upload">
|
||
<vehicle-attachment-upload
|
||
v-model="attachmentRows"
|
||
:readonly="dialogReadonly"
|
||
:file-types="attachmentFileTypes"
|
||
:max-size="50"
|
||
:show-file-list="false"
|
||
button-text="上传附件"
|
||
@change="handleAttachmentChange"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<template #menu-form>
|
||
<div v-if="!dialogReadonly" class="waybill-manage-page__freight-footer-summary">
|
||
<span>
|
||
运费合计:<strong>¥{{ formatFooterFreightTotal }}</strong>
|
||
</span>
|
||
<el-link v-if="hasProjectProcessConfig" type="primary" @click="openWaybillProcessConfig">
|
||
查看过程配置
|
||
</el-link>
|
||
</div>
|
||
<div
|
||
v-if="formPageLocked"
|
||
class="waybill-manage-page__standalone-menu-actions"
|
||
>
|
||
<el-button class="waybill-manage-page__form-close" @click="closeCrudDialog">
|
||
关闭
|
||
</el-button>
|
||
<el-button
|
||
v-if="config.enableDraftSave && !dialogReadonly"
|
||
class="waybill-manage-page__form-draft"
|
||
type="primary"
|
||
plain
|
||
:loading="draftSaveLoading"
|
||
@click="handleDraftSave"
|
||
>
|
||
{{ config.draftSaveText || '保存' }}
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
|
||
<template #menu="{ row, index }">
|
||
<div class="waybill-manage-page__row-actions">
|
||
<el-link type="primary" v-if="showDetailButton(row)" @click="openDetail(row)"
|
||
>详情</el-link
|
||
>
|
||
<el-link
|
||
type="primary"
|
||
v-if="hasPermission(`${config.permission}_view`) && !showDetailButton(row)"
|
||
@click="$refs.crud.rowView(row, index)"
|
||
>
|
||
查看
|
||
</el-link>
|
||
<el-link type="primary" v-if="canEdit(row)" @click="openBusinessForm('edit', row)">
|
||
编辑
|
||
</el-link>
|
||
<el-link type="primary" v-if="canReassign(row)" @click="openReassignDrawer(row)">
|
||
重新派单
|
||
</el-link>
|
||
<el-link type="primary" v-if="canCopy(row)" @click="handleCopy(row)"> 复制 </el-link>
|
||
<el-link type="primary" v-if="canEnable(row)" @click="handleAction('enable', row)">
|
||
启用
|
||
</el-link>
|
||
<el-link type="primary" v-if="canDisable(row)" @click="handleAction('disable', row)">
|
||
停用
|
||
</el-link>
|
||
<el-link type="primary" v-if="canCancel(row)" @click="handleAction('cancel', row)">
|
||
取消
|
||
</el-link>
|
||
<el-link type="primary" v-if="canComplete(row)" @click="handleAction('complete', row)">
|
||
{{ completeActionLabel }}
|
||
</el-link>
|
||
<el-link type="primary" v-if="canMaintainMileage(row)" @click="openMileageDialog(row)">
|
||
维护里程
|
||
</el-link>
|
||
<el-link
|
||
v-for="operation in customOperations(row)"
|
||
:key="operation.action"
|
||
:type="operation.type || 'primary'"
|
||
@click="handleCustomOperation(operation, row)"
|
||
>
|
||
{{ operation.label }}
|
||
</el-link>
|
||
<el-link type="danger" v-if="canDelete(row)" @click="rowDel(row)"> 删除 </el-link>
|
||
</div>
|
||
</template>
|
||
</component>
|
||
|
||
<el-drawer
|
||
v-model="reassignDrawer.visible"
|
||
title="重新派单"
|
||
direction="btt"
|
||
size="280px"
|
||
append-to-body
|
||
destroy-on-close
|
||
class="waybill-manage-page__reassign-drawer"
|
||
@closed="resetReassignDrawer"
|
||
>
|
||
<el-form
|
||
ref="reassignFormRef"
|
||
:model="reassignForm"
|
||
:rules="reassignRules"
|
||
label-width="72px"
|
||
class="waybill-manage-page__reassign-form"
|
||
>
|
||
<div class="waybill-manage-page__reassign-row">
|
||
<div class="waybill-manage-page__reassign-meta">
|
||
<span class="waybill-manage-page__reassign-meta-label">运单号</span>
|
||
<span class="waybill-manage-page__reassign-meta-value">{{
|
||
reassignDrawer.row?.waybillNo || '-'
|
||
}}</span>
|
||
</div>
|
||
<div class="waybill-manage-page__reassign-meta waybill-manage-page__reassign-meta--wide">
|
||
<span class="waybill-manage-page__reassign-meta-label">拒绝原因</span>
|
||
<span class="waybill-manage-page__reassign-meta-value">{{
|
||
driverRejectReasonText(reassignDrawer.row) || '-'
|
||
}}</span>
|
||
</div>
|
||
</div>
|
||
<div class="waybill-manage-page__reassign-row">
|
||
<el-form-item label="司机" prop="driverName" class="waybill-manage-page__reassign-span-1">
|
||
<el-autocomplete
|
||
v-model="reassignForm.driverName"
|
||
placeholder="请输入"
|
||
clearable
|
||
:debounce="300"
|
||
:fetch-suggestions="fetchReassignDriverSuggestions"
|
||
:loading="reassignDrawer.driverLoading"
|
||
style="width: 100%"
|
||
@select="handleReassignDriverSelect"
|
||
@change="handleReassignDriverChange"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item
|
||
label="手机号"
|
||
prop="driverPhone"
|
||
class="waybill-manage-page__reassign-span-1"
|
||
>
|
||
<el-input
|
||
v-model="reassignForm.driverPhone"
|
||
placeholder="请输入"
|
||
clearable
|
||
maxlength="20"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="车牌号" prop="vehicleNo" class="waybill-manage-page__reassign-span-1">
|
||
<el-input
|
||
v-model="reassignForm.vehicleNo"
|
||
placeholder="请输入车牌号"
|
||
clearable
|
||
maxlength="30"
|
||
/>
|
||
</el-form-item>
|
||
</div>
|
||
</el-form>
|
||
<template #footer>
|
||
<div class="waybill-manage-page__reassign-footer">
|
||
<el-button @click="reassignDrawer.visible = false">取消</el-button>
|
||
<el-button
|
||
type="primary"
|
||
:loading="reassignDrawer.submitting"
|
||
@click="submitReassign"
|
||
>
|
||
确认派单
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
</el-drawer>
|
||
|
||
<el-dialog
|
||
v-model="mileageDialog.visible"
|
||
width="520px"
|
||
append-to-body
|
||
:close-on-click-modal="false"
|
||
class="waybill-manage-page__mileage-dialog"
|
||
@closed="resetMileageDialog"
|
||
>
|
||
<template #header>
|
||
<div class="dialog-section-title">维护里程</div>
|
||
</template>
|
||
<el-form
|
||
ref="mileageFormRef"
|
||
:model="mileageForm"
|
||
:rules="mileageRules"
|
||
label-position="right"
|
||
label-width="auto"
|
||
class="waybill-manage-page__mileage-form"
|
||
>
|
||
<el-form-item label="里程(公里)" prop="mileage">
|
||
<el-input
|
||
:model-value="mileageForm.mileage"
|
||
inputmode="numeric"
|
||
maxlength="10"
|
||
placeholder="请输入里程"
|
||
@input="handleMaintainMileageInput"
|
||
>
|
||
<template #append>公里</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
<el-form-item label="备注" prop="mileageRemark">
|
||
<el-input
|
||
v-model="mileageForm.mileageRemark"
|
||
type="textarea"
|
||
:rows="3"
|
||
maxlength="200"
|
||
show-word-limit
|
||
placeholder="请输入里程维护备注"
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="mileageDialog.visible = false">取消</el-button>
|
||
<el-button
|
||
type="primary"
|
||
:loading="mileageDialog.submitting"
|
||
@click="submitMileageMaintenance"
|
||
>
|
||
提交
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<empty-pagination
|
||
v-show="!formPageLocked && !detailPageLocked"
|
||
:page="page"
|
||
@size-change="sizeChange"
|
||
@current-change="currentChange"
|
||
@load="onLoad(page, query)"
|
||
/>
|
||
|
||
<component
|
||
:is="detailContainer"
|
||
v-model="detailBox"
|
||
:title="`${config.title}详情`"
|
||
:append-to-body="!detailPageLocked"
|
||
top="10px"
|
||
width="96%"
|
||
show-close
|
||
:class="[
|
||
'waybill-manage-page__detail-dialog',
|
||
{ 'waybill-manage-page__detail-page': detailPageLocked },
|
||
$attrs.option?.dialogCustomClass,
|
||
]"
|
||
>
|
||
<div v-loading="detailLoading" class="waybill-manage-page__detail-content">
|
||
<template v-if="detailBox || detailPageLocked">
|
||
<section-card class="waybill-manage-page__waybill-detail-summary">
|
||
<div class="waybill-manage-page__waybill-heading">
|
||
<strong>运单详情</strong>
|
||
<span>{{ detailRow.waybillNo || '-' }}</span>
|
||
<span class="detail-status-text status-text-success">{{
|
||
displayStatus(detailRow, 'businessStatus')
|
||
}}</span>
|
||
<span class="detail-status-text detail-transport-type">
|
||
{{ getTransportTypeLabel(waybillTransportMode(detailRow)) || '-' }}
|
||
</span>
|
||
<el-link
|
||
v-if="detailRow.id"
|
||
class="waybill-manage-page__waybill-route-change-link"
|
||
type="primary"
|
||
@click="openWaybillRouteChangeDialog"
|
||
>
|
||
变更运输路线
|
||
</el-link>
|
||
</div>
|
||
<div class="waybill-manage-page__waybill-summary-grid">
|
||
<div
|
||
v-for="item in waybillDetailSummaryFields"
|
||
:key="item[0]"
|
||
class="waybill-manage-page__waybill-summary-item"
|
||
:class="{ 'is-plan': item[0] === 'planName' }"
|
||
>
|
||
<span class="waybill-manage-page__waybill-label">{{ item[1] }}</span>
|
||
<span :class="['waybill-manage-page__waybill-value', { 'is-link': item[2] }]">
|
||
{{ formatDetailValue(detailRow, item[0]) }}
|
||
</span>
|
||
</div>
|
||
<div class="waybill-manage-page__waybill-route">
|
||
<div class="waybill-manage-page__waybill-route-item is-start">
|
||
<div class="waybill-manage-page__waybill-route-marker">起</div>
|
||
<div class="waybill-manage-page__waybill-route-detail">
|
||
<strong class="waybill-manage-page__waybill-route-name">
|
||
{{
|
||
formatWaybillRouteCityDistrict(
|
||
detailRow.departureAddress || detailRow.departureName
|
||
)
|
||
}}
|
||
</strong>
|
||
<el-tooltip
|
||
:content="detailRow.departureAddress || detailRow.departureName || '-'"
|
||
placement="top"
|
||
>
|
||
<span class="waybill-manage-page__waybill-route-addr">
|
||
{{ detailRow.departureAddress || detailRow.departureName || '-' }}
|
||
</span>
|
||
</el-tooltip>
|
||
<div class="waybill-manage-page__waybill-route-contact">
|
||
<span>联系人:{{ detailRow.departureContact || '-' }}</span>
|
||
<span>联系方式:{{ detailRow.departurePhone || '-' }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<span class="waybill-manage-page__waybill-route-line" aria-hidden="true" />
|
||
<div class="waybill-manage-page__waybill-route-item is-end">
|
||
<div class="waybill-manage-page__waybill-route-marker">终</div>
|
||
<div class="waybill-manage-page__waybill-route-detail">
|
||
<strong class="waybill-manage-page__waybill-route-name">
|
||
{{
|
||
formatWaybillRouteCityDistrict(
|
||
detailRow.arrivalAddress || detailRow.arrivalName
|
||
)
|
||
}}
|
||
</strong>
|
||
<el-tooltip
|
||
:content="detailRow.arrivalAddress || detailRow.arrivalName || '-'"
|
||
placement="top"
|
||
>
|
||
<span class="waybill-manage-page__waybill-route-addr">
|
||
{{ detailRow.arrivalAddress || detailRow.arrivalName || '-' }}
|
||
</span>
|
||
</el-tooltip>
|
||
<div class="waybill-manage-page__waybill-route-contact">
|
||
<span>联系人:{{ detailRow.arrivalContact || '-' }}</span>
|
||
<span>联系方式:{{ detailRow.arrivalPhone || '-' }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="waybill-manage-page__waybill-attachments">
|
||
<span class="waybill-manage-page__waybill-label">附件</span>
|
||
<div>
|
||
<el-link
|
||
v-for="file in attachmentRows"
|
||
:key="file.url || file.name || file.originalName"
|
||
type="primary"
|
||
@click="previewAttachment(file)"
|
||
>
|
||
{{ file.originalName || file.name || '-' }}
|
||
</el-link>
|
||
<span v-if="!attachmentRows.length">-</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section-card>
|
||
<section-card title="货物信息">
|
||
<el-table
|
||
:data="
|
||
transportCargoRows.length ? transportCargoRows : parseJsonArray(detailRow.goodsJson)
|
||
"
|
||
border
|
||
>
|
||
<el-table-column type="index" label="序号" width="70" />
|
||
<el-table-column prop="cargoName" label="货物名称" min-width="150" />
|
||
<el-table-column prop="cargoType" label="货物类型" min-width="130" />
|
||
<el-table-column prop="packageType" label="包装" min-width="100" />
|
||
<el-table-column prop="quantity" label="数量" min-width="100" />
|
||
<el-table-column prop="quantityUnit" label="数量单位" min-width="110" />
|
||
<el-table-column prop="materialCode" label="物料编码" min-width="130" />
|
||
<el-table-column prop="deviceCode" label="设备编码" min-width="130" />
|
||
<el-table-column prop="brand" label="品牌" min-width="110" />
|
||
<el-table-column label="规格型号" min-width="140"
|
||
><template #default="{ row }">{{
|
||
[row.specification, row.model].filter(Boolean).join('/') || '-'
|
||
}}</template></el-table-column
|
||
>
|
||
<el-table-column prop="remark" label="备注" min-width="180" />
|
||
</el-table>
|
||
</section-card>
|
||
<section-card title="承运信息">
|
||
<div class="waybill-manage-page__waybill-carrier-grid">
|
||
<div class="waybill-manage-page__waybill-detail-field">
|
||
<span>承运类型</span><strong>{{ detailRow.carrierType || '-' }}</strong>
|
||
</div>
|
||
<div class="waybill-manage-page__waybill-detail-field">
|
||
<span>承运商</span><strong>{{ detailRow.carrierName || '-' }}</strong>
|
||
</div>
|
||
<template v-if="waybillIsRoadTransport(detailRow)">
|
||
<div class="waybill-manage-page__waybill-detail-field">
|
||
<span>司机</span
|
||
><strong
|
||
>{{
|
||
[detailRow.driverName, detailRow.driverPhone].filter(Boolean).join(' ') || '-'
|
||
}}</strong
|
||
>
|
||
</div>
|
||
<div class="waybill-manage-page__waybill-detail-field">
|
||
<span>车牌号</span><strong>{{ detailRow.vehicleNo || '-' }}</strong>
|
||
</div>
|
||
<div class="waybill-manage-page__waybill-detail-field">
|
||
<span>挂车车牌</span><strong>{{ detailRow.trailerVehicleNo || '-' }}</strong>
|
||
</div>
|
||
<div class="waybill-manage-page__waybill-detail-field">
|
||
<span>押运人</span
|
||
><strong
|
||
>{{
|
||
[detailRow.escortName, detailRow.escortPhone].filter(Boolean).join(' ') || '-'
|
||
}}</strong
|
||
>
|
||
</div>
|
||
<div
|
||
v-if="!waybillIsCarrier(detailRow)"
|
||
class="waybill-manage-page__waybill-detail-field"
|
||
>
|
||
<span>里程(km)</span><strong>{{ detailRow.mileage || '-' }}</strong>
|
||
</div>
|
||
</template>
|
||
<template v-else-if="waybillIsNonRoadTransport(detailRow)">
|
||
<div class="waybill-manage-page__waybill-detail-field">
|
||
<span>船长</span
|
||
><strong
|
||
>{{
|
||
[detailRow.captainName, detailRow.driverPhone].filter(Boolean).join(' ') || '-'
|
||
}}</strong
|
||
>
|
||
</div>
|
||
<div class="waybill-manage-page__waybill-detail-field">
|
||
<span>航班号/船次号/班列号</span>
|
||
<strong>{{ detailRow.vehicleNo || '-' }}</strong>
|
||
</div>
|
||
<div class="waybill-manage-page__waybill-detail-field">
|
||
<span>箱号</span><strong>{{ detailRow.containerNo || '-' }}</strong>
|
||
</div>
|
||
<div class="waybill-manage-page__waybill-detail-field">
|
||
<span>仓位</span><strong>{{ detailRow.cabinNo || '-' }}</strong>
|
||
</div>
|
||
</template>
|
||
<template v-else>
|
||
<div class="waybill-manage-page__waybill-detail-field">
|
||
<span>{{ waybillVehicleNoLabel(detailRow) }}</span>
|
||
<strong>{{ detailRow.vehicleNo || '-' }}</strong>
|
||
</div>
|
||
</template>
|
||
<div
|
||
class="waybill-manage-page__waybill-detail-field"
|
||
:class="{
|
||
'waybill-manage-page__waybill-detail-field--remark': waybillIsRoadTransport(detailRow),
|
||
'waybill-manage-page__waybill-detail-field--remark-full': waybillIsNonRoadTransport(detailRow),
|
||
}"
|
||
>
|
||
<span>备注</span
|
||
><strong>{{ detailRow.taskRemark || detailRow.remark || '-' }}</strong>
|
||
</div>
|
||
</div>
|
||
</section-card>
|
||
<section-card title="运费信息">
|
||
<div
|
||
class="waybill-manage-page__waybill-carrier-grid waybill-manage-page__waybill-fee-grid"
|
||
>
|
||
<div class="waybill-manage-page__waybill-detail-field">
|
||
<span>单价</span>
|
||
<strong>
|
||
<template v-if="waybillUnitPriceWithCurrency(detailRow).value !== '-'">
|
||
<span class="waybill-manage-page__waybill-fee-value">
|
||
{{ waybillUnitPriceWithCurrency(detailRow).value }}
|
||
</span>
|
||
<span v-if="waybillUnitPriceWithCurrency(detailRow).unit">
|
||
{{ ' ' + waybillUnitPriceWithCurrency(detailRow).unit }}
|
||
</span>
|
||
</template>
|
||
<template v-else>-</template>
|
||
</strong>
|
||
</div>
|
||
<div class="waybill-manage-page__waybill-detail-field">
|
||
<span>运费</span>
|
||
<strong>
|
||
<template v-if="waybillAmountWithCurrency(detailRow, 'freight').value !== '-'">
|
||
<span class="waybill-manage-page__waybill-fee-value">{{
|
||
waybillAmountWithCurrency(detailRow, 'freight').value
|
||
}}</span
|
||
>元
|
||
</template>
|
||
<template v-else>-</template>
|
||
</strong>
|
||
</div>
|
||
<div class="waybill-manage-page__waybill-detail-field">
|
||
<span>其他费用合计</span>
|
||
<strong>
|
||
<template
|
||
v-if="waybillAmountWithCurrency(detailRow, 'otherFeeTotal').value !== '-'"
|
||
>
|
||
<span class="waybill-manage-page__waybill-fee-value">{{
|
||
waybillAmountWithCurrency(detailRow, 'otherFeeTotal').value
|
||
}}</span
|
||
>元
|
||
</template>
|
||
<template v-else>-</template>
|
||
</strong>
|
||
</div>
|
||
<div class="waybill-manage-page__waybill-detail-field">
|
||
<span>运费合计</span>
|
||
<strong>
|
||
<template
|
||
v-if="waybillAmountWithCurrency(detailRow, 'freightTotal').value !== '-'"
|
||
>
|
||
<span class="waybill-manage-page__waybill-fee-value">{{
|
||
waybillAmountWithCurrency(detailRow, 'freightTotal').value
|
||
}}</span
|
||
>元
|
||
</template>
|
||
<template v-else>-</template>
|
||
</strong>
|
||
</div>
|
||
</div>
|
||
</section-card>
|
||
<div class="waybill-manage-page__waybill-detail-bottom">
|
||
<section-card title="执行详情">
|
||
<el-tabs v-model="waybillProcessDetailTab" type="border-card">
|
||
<el-tab-pane label="打卡详情" name="punch">
|
||
<div v-loading="waybillPunchRecordsLoading">
|
||
<el-timeline v-if="waybillPunchRecords.length">
|
||
<el-timeline-item
|
||
v-for="(record, index) in orderedWaybillPunchRecords"
|
||
: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="waybill-manage-page__punch-record">
|
||
<div class="waybill-manage-page__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.type === 'enroute'"
|
||
size="small"
|
||
type="info"
|
||
effect="plain"
|
||
>在途</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="waybill-manage-page__punch-record-line"
|
||
>
|
||
地点:{{ record.address }}
|
||
</div>
|
||
<div
|
||
v-if="record.weight || record.volume || record.quantity"
|
||
class="waybill-manage-page__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="waybill-manage-page__punch-record-line"
|
||
>
|
||
备注:{{ record.remark }}
|
||
</div>
|
||
<div
|
||
v-if="record.photos && record.photos.length"
|
||
class="waybill-manage-page__punch-record-photos"
|
||
>
|
||
<div
|
||
v-for="(photo, pi) in record.photos"
|
||
:key="`${record.nodeCode || index}-${pi}`"
|
||
class="waybill-manage-page__driver-upload-item"
|
||
@click="previewDriverUploadPhoto(record.photos, pi)"
|
||
>
|
||
<el-image :src="photo.url" fit="cover" lazy />
|
||
<div class="waybill-manage-page__driver-upload-label">
|
||
{{ photo.label || `${record.nodeName}-凭证` }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<div v-else class="waybill-manage-page__punch-record-line">暂未打卡</div>
|
||
</div>
|
||
</el-timeline-item>
|
||
</el-timeline>
|
||
<el-empty
|
||
v-else-if="!waybillPunchRecordsLoading"
|
||
description="暂无打卡记录"
|
||
:image-size="50"
|
||
/>
|
||
</div>
|
||
</el-tab-pane>
|
||
<el-tab-pane
|
||
v-if="waybillHasRelatedVoucher"
|
||
label="批量补录"
|
||
name="batchSupplement"
|
||
>
|
||
<div
|
||
v-if="waybillVoucherFolderView"
|
||
class="waybill-manage-page__voucher-folder-toolbar"
|
||
>
|
||
<el-button text type="primary" @click="backToWaybillVoucherFolders">
|
||
返回文件夹
|
||
</el-button>
|
||
<span>{{ waybillVoucherFolderName(waybillVoucherFolder) }}</span>
|
||
</div>
|
||
<div
|
||
v-loading="waybillVoucherImagesLoading"
|
||
class="waybill-manage-page__voucher-image-grid"
|
||
>
|
||
<div
|
||
v-for="(image, index) in waybillVoucherImages"
|
||
:key="
|
||
image.id ||
|
||
image.objectKey ||
|
||
(isWaybillVoucherFolder(image)
|
||
? `${image.voucherId || 'folder'}-${image.folderName || image.name || index}`
|
||
: index)
|
||
"
|
||
:class="[
|
||
'waybill-manage-page__voucher-image-item',
|
||
{
|
||
'waybill-manage-page__voucher-image-item--folder':
|
||
isWaybillVoucherFolder(image),
|
||
},
|
||
]"
|
||
:title="
|
||
isWaybillVoucherFolder(image)
|
||
? waybillVoucherFolderName(image)
|
||
: image.imageName || image.name
|
||
"
|
||
@click="
|
||
isWaybillVoucherFolder(image)
|
||
? openWaybillVoucherFolder(image)
|
||
: previewWaybillVoucherImage(index)
|
||
"
|
||
>
|
||
<el-image
|
||
:src="
|
||
isWaybillVoucherFolder(image)
|
||
? image.icon || '/img/文件夹.png'
|
||
: image.url
|
||
"
|
||
:fit="isWaybillVoucherFolder(image) ? 'contain' : 'cover'"
|
||
lazy
|
||
/>
|
||
<div
|
||
v-if="isWaybillVoucherFolder(image)"
|
||
class="waybill-manage-page__voucher-folder-name"
|
||
>
|
||
{{ waybillVoucherFolderName(image) }}
|
||
</div>
|
||
</div>
|
||
<el-empty
|
||
v-if="!waybillVoucherImagesLoading && !waybillVoucherImages.length"
|
||
description="暂无凭证图片"
|
||
:image-size="50"
|
||
/>
|
||
</div>
|
||
</el-tab-pane>
|
||
<el-tab-pane label="司机上传" name="driverUpload">
|
||
<div
|
||
v-loading="waybillPunchRecordsLoading"
|
||
class="waybill-manage-page__driver-upload-grid"
|
||
>
|
||
<div
|
||
v-for="(photo, index) in waybillDriverUploads"
|
||
:key="`${photo.url}-${index}`"
|
||
class="waybill-manage-page__driver-upload-item"
|
||
:title="photo.label"
|
||
@click="previewDriverUploadPhoto(waybillDriverUploads, index)"
|
||
>
|
||
<el-image :src="photo.url" fit="cover" lazy />
|
||
<div class="waybill-manage-page__driver-upload-label">
|
||
{{ photo.label || '凭证' }}
|
||
</div>
|
||
</div>
|
||
<el-empty
|
||
v-if="!waybillPunchRecordsLoading && !waybillDriverUploads.length"
|
||
description="暂无司机上传数据"
|
||
:image-size="50"
|
||
/>
|
||
</div>
|
||
</el-tab-pane>
|
||
</el-tabs>
|
||
</section-card>
|
||
<section-card title="物流轨迹">
|
||
<template #extra>
|
||
<div class="waybill-manage-page__waybill-track-actions">
|
||
<el-button
|
||
plain
|
||
:type="waybillTrackMode === 'playback' ? 'primary' : undefined"
|
||
:loading="waybillTrackLoading"
|
||
@click="handleWaybillTrackAction('playback')"
|
||
>轨迹回放</el-button
|
||
>
|
||
<el-button
|
||
plain
|
||
:type="waybillTrackMode === 'locate' ? 'primary' : undefined"
|
||
:loading="waybillLocateLoading"
|
||
@click="handleWaybillTrackAction('locate')"
|
||
>实时定位</el-button
|
||
>
|
||
</div>
|
||
</template>
|
||
<div
|
||
v-if="waybillTrackMode === 'playback'"
|
||
class="waybill-manage-page__waybill-track-toolbar"
|
||
>
|
||
<span>时间段</span>
|
||
<el-date-picker
|
||
v-model="waybillTrackDateRange"
|
||
type="daterange"
|
||
range-separator="至"
|
||
start-placeholder="开始日期"
|
||
end-placeholder="结束日期"
|
||
value-format="YYYY-MM-DD"
|
||
:clearable="false"
|
||
:disabled="waybillTrackLoading"
|
||
/>
|
||
<el-button
|
||
type="primary"
|
||
:loading="waybillTrackLoading"
|
||
@click="loadWaybillTrack"
|
||
>查询</el-button
|
||
>
|
||
<span class="waybill-manage-page__waybill-track-total"
|
||
>轨迹点 {{ waybillTrackInfo?.total || 0 }} 个</span
|
||
>
|
||
</div>
|
||
<div
|
||
v-if="waybillTrackMode === 'locate'"
|
||
class="waybill-manage-page__waybill-locate"
|
||
>
|
||
<div v-if="waybillLocateInfo" class="waybill-manage-page__waybill-locate-meta">
|
||
<div>
|
||
<span>车牌号</span><strong>{{ waybillLocateInfo.vehicleNo || '-' }}</strong>
|
||
</div>
|
||
<div>
|
||
<span>定位时间</span><strong>{{ waybillLocateInfo.locateTime || '-' }}</strong>
|
||
</div>
|
||
<div>
|
||
<span>速度</span><strong>{{ waybillLocateInfo.speed || '-' }}</strong>
|
||
</div>
|
||
<div>
|
||
<span>方向</span><strong>{{ waybillLocateInfo.direction || '-' }}</strong>
|
||
</div>
|
||
<div class="waybill-manage-page__waybill-locate-address">
|
||
<span>地址</span><strong>{{ waybillLocateInfo.address || '-' }}</strong>
|
||
</div>
|
||
<div>
|
||
<span>坐标</span
|
||
><strong
|
||
>{{
|
||
waybillLocateInfo.longitude != null && waybillLocateInfo.latitude != null
|
||
? `${waybillLocateInfo.longitude}, ${waybillLocateInfo.latitude}`
|
||
: '-'
|
||
}}
|
||
</strong>
|
||
</div>
|
||
</div>
|
||
<div
|
||
ref="waybillLocateMap"
|
||
class="waybill-manage-page__waybill-locate-map"
|
||
></div>
|
||
<div
|
||
v-if="!waybillLocateInfo"
|
||
class="waybill-manage-page__waybill-map-tip"
|
||
>
|
||
{{ waybillLocateHint || '暂无数据' }}
|
||
</div>
|
||
</div>
|
||
<div
|
||
v-else-if="waybillTrackMode === 'playback'"
|
||
class="waybill-manage-page__waybill-locate"
|
||
>
|
||
<div
|
||
ref="waybillTrackMap"
|
||
class="waybill-manage-page__waybill-locate-map waybill-manage-page__waybill-locate-map--track"
|
||
></div>
|
||
<div
|
||
v-if="!(waybillTrackInfo?.points || []).length"
|
||
class="waybill-manage-page__waybill-map-tip"
|
||
>
|
||
{{ waybillTrackHint || '暂无数据' }}
|
||
</div>
|
||
</div>
|
||
<div v-else class="waybill-manage-page__waybill-map-empty">暂无数据</div>
|
||
</section-card>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
<div v-if="detailPageLocked" class="waybill-manage-page__detail-footer">
|
||
<el-button type="primary" @click="closeDetail">关闭</el-button>
|
||
</div>
|
||
<template v-if="!detailPageLocked" #footer>
|
||
<el-button type="primary" @click="closeDetail">关闭</el-button>
|
||
</template>
|
||
</component>
|
||
|
||
<el-dialog
|
||
v-model="attachmentDocumentPreviewVisible"
|
||
:title="attachmentPreviewFile.name || '附件预览'"
|
||
append-to-body
|
||
destroy-on-close
|
||
width="90%"
|
||
top="4vh"
|
||
>
|
||
<open-file-viewer
|
||
v-if="attachmentDocumentPreviewVisible && attachmentPreviewFile.url"
|
||
:file="attachmentPreviewFile.url"
|
||
:file-name="attachmentPreviewFile.name"
|
||
:mime-type="attachmentPreviewFile.mimeType"
|
||
width="100%"
|
||
height="72vh"
|
||
fit="contain"
|
||
theme="auto"
|
||
locale="zh-CN"
|
||
:toolbar="attachmentViewerToolbar"
|
||
:plugins="attachmentViewerPlugins"
|
||
@unsupported="handleAttachmentPreviewUnsupported"
|
||
@error="handleAttachmentPreviewError"
|
||
/>
|
||
</el-dialog>
|
||
|
||
<el-image-viewer
|
||
v-if="attachmentImagePreviewVisible"
|
||
:url-list="attachmentImagePreviewUrls"
|
||
:initial-index="attachmentImagePreviewIndex"
|
||
@close="attachmentImagePreviewVisible = false"
|
||
/>
|
||
|
||
<el-dialog
|
||
v-model="waybillRouteChangeBox"
|
||
title="变更运输路线"
|
||
append-to-body
|
||
destroy-on-close
|
||
width="78%"
|
||
class="waybill-manage-page__route-change-dialog"
|
||
>
|
||
<el-tabs type="border-card" v-model="waybillRouteChangeTab">
|
||
<el-tab-pane label="变更路线" name="change">
|
||
<section-card title="运单地址">
|
||
<el-table :data="waybillRouteChangeRows" border>
|
||
<el-table-column type="index" label="序号" width="70" />
|
||
<el-table-column prop="waybillNo" label="运单号" min-width="150">
|
||
<template #default="{ row }">
|
||
<el-link type="primary">{{ row.waybillNo || '-' }}</el-link>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="projectName" label="项目名称" min-width="160" />
|
||
<el-table-column label="发货地" min-width="310">
|
||
<template #default="{ row }">
|
||
<div class="waybill-manage-page__route-change-original">
|
||
原:{{ row.originalDepartureAddress || '-' }}
|
||
</div>
|
||
<el-input
|
||
v-model="row.departureAddress"
|
||
placeholder="请选择或输入地址"
|
||
@input="updateWaybillRouteChangeNodes"
|
||
>
|
||
<template #suffix>
|
||
<el-icon
|
||
class="waybill-manage-page__route-change-location"
|
||
@click="openWaybillCommonAddressDialog(row, 'departure')"
|
||
>
|
||
<Location />
|
||
</el-icon>
|
||
</template>
|
||
</el-input>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="到货地" min-width="310">
|
||
<template #default="{ row }">
|
||
<div class="waybill-manage-page__route-change-original">
|
||
原:{{ row.originalArrivalAddress || '-' }}
|
||
</div>
|
||
<el-input
|
||
v-model="row.arrivalAddress"
|
||
placeholder="请选择或输入地址"
|
||
@input="updateWaybillRouteChangeNodes"
|
||
>
|
||
<template #suffix>
|
||
<el-icon
|
||
class="waybill-manage-page__route-change-location"
|
||
@click="openWaybillCommonAddressDialog(row, 'arrival')"
|
||
>
|
||
<Location />
|
||
</el-icon>
|
||
</template>
|
||
</el-input>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</section-card>
|
||
<section-card title="路线编辑">
|
||
<template #extra>
|
||
<span class="waybill-manage-page__route-change-hint">拖拽调整顺序</span>
|
||
</template>
|
||
<div
|
||
v-if="waybillRouteChangeNodes.length"
|
||
class="waybill-manage-page__route-change-list"
|
||
>
|
||
<div
|
||
v-for="(node, index) in waybillRouteChangeNodes"
|
||
:key="node.key || index"
|
||
class="waybill-manage-page__route-change-item"
|
||
draggable="true"
|
||
@dragstart="handleWaybillRouteChangeDragStart(index)"
|
||
@dragover.prevent
|
||
@drop="handleWaybillRouteChangeDrop(index)"
|
||
>
|
||
<span class="waybill-manage-page__route-change-type">
|
||
{{ waybillRouteChangeTypeText(index) }}
|
||
</span>
|
||
<span>{{ node.address }}</span>
|
||
<span class="waybill-manage-page__route-change-tags">
|
||
<el-tag v-for="tag in node.tags || []" :key="tag" size="small">{{ tag }}</el-tag>
|
||
</span>
|
||
<el-icon><Rank /></el-icon>
|
||
</div>
|
||
</div>
|
||
<el-empty v-else description="暂无路线" :image-size="60" />
|
||
</section-card>
|
||
<el-form label-width="auto">
|
||
<el-form-item label="变更备注" style="margin-top: 8px">
|
||
<el-input
|
||
v-model="waybillRouteChangeRemark"
|
||
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="waybillRouteChangeRecords" border>
|
||
<el-table-column prop="changeTime" label="变更时间" min-width="170" />
|
||
<el-table-column prop="changeUser" label="变更人" min-width="120" />
|
||
<el-table-column prop="content" label="变更内容" min-width="360">
|
||
<template #default="{ row }">
|
||
<div class="waybill-manage-page__route-change-record-content">
|
||
{{ row.content || '-' }}
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="remark" label="备注" min-width="200" show-overflow-tooltip />
|
||
</el-table>
|
||
<el-empty
|
||
v-if="!waybillRouteChangeRecords.length"
|
||
description="暂无变更记录"
|
||
:image-size="60"
|
||
/>
|
||
</el-tab-pane>
|
||
</el-tabs>
|
||
<template #footer>
|
||
<el-button @click="waybillRouteChangeBox = false">关闭</el-button>
|
||
<el-button
|
||
v-if="waybillRouteChangeTab === 'change'"
|
||
type="primary"
|
||
:loading="waybillRouteChangeSaving"
|
||
@click="confirmWaybillRouteChange"
|
||
>
|
||
确认变更
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog
|
||
v-model="waybillCommonAddressBox"
|
||
title="选择常用地址"
|
||
append-to-body
|
||
width="900px"
|
||
class="waybill-manage-page__common-address-dialog"
|
||
>
|
||
<div class="waybill-manage-page__common-address-toolbar">
|
||
<el-input
|
||
v-model="waybillCommonAddressQuery.addressName"
|
||
clearable
|
||
placeholder="请输入地址名称"
|
||
@keyup.enter="loadWaybillCommonAddresses"
|
||
/>
|
||
<el-button type="primary" @click="loadWaybillCommonAddresses">查询</el-button>
|
||
</div>
|
||
<el-table
|
||
v-loading="waybillCommonAddressLoading"
|
||
:data="waybillCommonAddressRows"
|
||
border
|
||
@row-dblclick="selectWaybillCommonAddress"
|
||
>
|
||
<el-table-column prop="addressName" label="地址名称" min-width="180" />
|
||
<el-table-column prop="regionName" label="区域" min-width="180" />
|
||
<el-table-column prop="detailAddress" label="详细地址" min-width="260" />
|
||
<el-table-column label="操作" width="90">
|
||
<template #default="{ row }">
|
||
<el-link type="primary" @click="selectWaybillCommonAddress(row)">选择</el-link>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<template #footer>
|
||
<el-pagination
|
||
v-model:current-page="waybillCommonAddressPage.currentPage"
|
||
v-model:page-size="waybillCommonAddressPage.pageSize"
|
||
layout="total, prev, pager, next"
|
||
:total="waybillCommonAddressPage.total"
|
||
@current-change="loadWaybillCommonAddresses"
|
||
/>
|
||
<el-button @click="waybillCommonAddressBox = false">关闭</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<flow-design
|
||
v-if="website.design.designMode"
|
||
is-dialog
|
||
v-model:is-display="flowBox"
|
||
:process-instance-id="processInstanceId"
|
||
/>
|
||
<el-dialog v-else title="流程图" append-to-body v-model="flowBox" :fullscreen="true">
|
||
<iframe
|
||
:src="flowUrl"
|
||
width="100%"
|
||
height="700"
|
||
title="流程图"
|
||
frameBorder="no"
|
||
border="0"
|
||
marginWidth="0"
|
||
marginHeight="0"
|
||
scrolling="no"
|
||
allowTransparency="yes"
|
||
/>
|
||
<template #footer>
|
||
<el-button @click="flowBox = false">关闭</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog
|
||
title="选择线路"
|
||
append-to-body
|
||
v-model="transportRouteBox"
|
||
width="1280px"
|
||
@opened="loadTransportRouteList"
|
||
>
|
||
<div class="waybill-manage-page__dialog-search">
|
||
<el-form :model="transportRouteQuery" label-position="right" label-width="86px">
|
||
<el-row :gutter="16">
|
||
<el-col :span="6">
|
||
<el-form-item label="线路名称">
|
||
<el-input v-model="transportRouteQuery.routeName" clearable placeholder="请输入" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<el-form-item label="发货地址">
|
||
<el-input
|
||
v-model="transportRouteQuery.departureAddress"
|
||
clearable
|
||
placeholder="请输入"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<el-form-item label="收货地址">
|
||
<el-input
|
||
v-model="transportRouteQuery.arrivalAddress"
|
||
clearable
|
||
placeholder="请输入"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="6" class="waybill-manage-page__dialog-search-actions">
|
||
<el-button type="primary" @click="handleTransportRouteSearch">查询</el-button>
|
||
<el-button @click="handleTransportRouteReset">重置</el-button>
|
||
</el-col>
|
||
</el-row>
|
||
</el-form>
|
||
</div>
|
||
<el-table border height="320" :data="transportRouteRows" v-loading="transportRouteLoading">
|
||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||
<el-table-column prop="routeName" label="线路名称" min-width="150" />
|
||
<el-table-column prop="routeCode" label="线路编号" min-width="140" />
|
||
<el-table-column prop="departureName" label="发货地" min-width="140" />
|
||
<el-table-column
|
||
prop="departureAddress"
|
||
label="发货地址"
|
||
min-width="220"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column prop="departureContact" label="发货联系人" min-width="120" />
|
||
<el-table-column prop="departurePhone" label="发货联系方式" min-width="140" />
|
||
<el-table-column prop="arrivalName" label="收货地" min-width="140" />
|
||
<el-table-column
|
||
prop="arrivalAddress"
|
||
label="收货地址"
|
||
min-width="220"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column prop="arrivalContact" label="收货联系人" min-width="120" />
|
||
<el-table-column prop="arrivalPhone" label="收货联系方式" min-width="140" />
|
||
<el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip />
|
||
<el-table-column prop="deptName" label="组织" min-width="150" show-overflow-tooltip />
|
||
<el-table-column
|
||
prop="updateUserName"
|
||
label="更新人"
|
||
min-width="120"
|
||
:formatter="formatTransportRouteUpdateUserName"
|
||
/>
|
||
<el-table-column prop="updateTime" label="更新时间" min-width="160" sortable />
|
||
<el-table-column prop="createTime" label="创建时间" min-width="160" sortable />
|
||
<el-table-column label="操作" width="110" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-link type="primary" @click.stop="selectTransportRouteRow(row)"> 选择 </el-link>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div class="waybill-manage-page__dialog-pagination">
|
||
<el-pagination
|
||
v-model:current-page="transportRoutePage.currentPage"
|
||
v-model:page-size="transportRoutePage.pageSize"
|
||
:page-sizes="[10, 20, 50, 100]"
|
||
layout="total, prev, pager, next, sizes"
|
||
:total="transportRoutePage.total"
|
||
@size-change="handleTransportRouteSizeChange"
|
||
@current-change="handleTransportRouteCurrentChange"
|
||
/>
|
||
</div>
|
||
<template #footer>
|
||
<el-button @click="transportRouteBox = false">关闭</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog
|
||
title="选择常用地址"
|
||
append-to-body
|
||
v-model="transportAddressBox"
|
||
width="1280px"
|
||
@opened="loadTransportAddressList"
|
||
>
|
||
<div class="waybill-manage-page__dialog-search">
|
||
<el-form :model="transportAddressQuery" label-position="right" label-width="86px">
|
||
<el-row :gutter="16">
|
||
<el-col :span="6">
|
||
<el-form-item label="地址名称">
|
||
<el-input
|
||
v-model="transportAddressQuery.addressName"
|
||
clearable
|
||
placeholder="请输入"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<el-form-item label="地址类型">
|
||
<el-select
|
||
v-model="transportAddressQuery.addressType"
|
||
clearable
|
||
filterable
|
||
placeholder="请选择"
|
||
:disabled="!!getTransportFixedAddressType()"
|
||
>
|
||
<el-option
|
||
v-for="item in addressTypeOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<el-form-item label="详细地址">
|
||
<el-input
|
||
v-model="transportAddressQuery.detailAddress"
|
||
clearable
|
||
placeholder="请输入"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="6" class="waybill-manage-page__dialog-search-actions">
|
||
<el-button type="primary" @click="handleTransportAddressSearch">查询</el-button>
|
||
<el-button @click="handleTransportAddressReset">重置</el-button>
|
||
</el-col>
|
||
</el-row>
|
||
</el-form>
|
||
</div>
|
||
<el-table
|
||
border
|
||
height="320"
|
||
:data="transportAddressRows"
|
||
v-loading="transportAddressLoading"
|
||
>
|
||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||
<el-table-column prop="addressName" label="地址名称" min-width="150" />
|
||
<el-table-column prop="addressCode" label="地址编号" min-width="130" />
|
||
<el-table-column prop="addressType" label="类型" min-width="120" />
|
||
<el-table-column
|
||
prop="siteCodeDisplay"
|
||
label="站点编码"
|
||
min-width="120"
|
||
:formatter="formatTransportAddressSiteCode"
|
||
/>
|
||
<el-table-column
|
||
prop="detailAddress"
|
||
label="详细地址"
|
||
min-width="260"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column prop="longitude" label="经度" min-width="120" />
|
||
<el-table-column prop="latitude" label="纬度" min-width="120" />
|
||
<el-table-column prop="regionName" label="行政区划" min-width="150" show-overflow-tooltip />
|
||
<el-table-column prop="contactName" label="联系人" min-width="120" />
|
||
<el-table-column prop="contactPhone" label="联系方式" min-width="140" />
|
||
<el-table-column prop="deptName" label="组织" min-width="150" show-overflow-tooltip />
|
||
<el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip />
|
||
<el-table-column prop="updateTime" label="更新时间" min-width="160" sortable />
|
||
<el-table-column prop="createTime" label="创建时间" min-width="160" sortable />
|
||
<el-table-column label="操作" width="110" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-link type="primary" @click.stop="selectTransportAddressRow(row)"> 选择 </el-link>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div class="waybill-manage-page__dialog-pagination">
|
||
<el-pagination
|
||
v-model:current-page="transportAddressPage.currentPage"
|
||
v-model:page-size="transportAddressPage.pageSize"
|
||
:page-sizes="[10, 20, 50, 100]"
|
||
layout="total, prev, pager, next, sizes"
|
||
:total="transportAddressPage.total"
|
||
@size-change="handleTransportAddressSizeChange"
|
||
@current-change="handleTransportAddressCurrentChange"
|
||
/>
|
||
</div>
|
||
<template #footer>
|
||
<el-button @click="transportAddressBox = false">关闭</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog
|
||
:title="`选择${transportStationTypeLabel || '站点'}`"
|
||
append-to-body
|
||
v-model="transportStationBox"
|
||
width="1180px"
|
||
@opened="loadTransportStationList"
|
||
>
|
||
<div class="waybill-manage-page__dialog-search">
|
||
<el-form :model="transportStationQuery" label-position="right" label-width="86px">
|
||
<el-row :gutter="16">
|
||
<el-col :span="6">
|
||
<el-form-item label="站点名称">
|
||
<el-input v-model="transportStationQuery.name" clearable placeholder="请输入" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<el-form-item label="地址类型">
|
||
<el-input :model-value="transportStationTypeLabel" disabled />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<el-form-item label="详细地址">
|
||
<el-input
|
||
v-model="transportStationQuery.detailAddress"
|
||
clearable
|
||
placeholder="请输入"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="6" class="waybill-manage-page__dialog-search-actions">
|
||
<el-button type="primary" @click="handleTransportStationSearch">查询</el-button>
|
||
<el-button @click="handleTransportStationReset">重置</el-button>
|
||
</el-col>
|
||
</el-row>
|
||
</el-form>
|
||
</div>
|
||
<el-table
|
||
border
|
||
height="320"
|
||
:data="transportStationRows"
|
||
v-loading="transportStationLoading"
|
||
>
|
||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||
<el-table-column prop="stationName" label="站点名" min-width="160" />
|
||
<el-table-column prop="stationType" label="类型" min-width="120" />
|
||
<el-table-column prop="stationCode" label="站点编码" min-width="130" />
|
||
<el-table-column
|
||
prop="detailAddress"
|
||
label="详细地址"
|
||
min-width="260"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column prop="longitude" label="经度" min-width="120" />
|
||
<el-table-column prop="latitude" label="纬度" min-width="120" />
|
||
<el-table-column prop="regionName" label="行政区划" min-width="160" show-overflow-tooltip />
|
||
<el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip />
|
||
<el-table-column prop="updateTime" label="更新时间" min-width="160" sortable />
|
||
<el-table-column prop="createTime" label="创建时间" min-width="160" sortable />
|
||
<el-table-column label="操作" width="110" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-link type="primary" @click.stop="selectTransportStationRow(row)"> 选择 </el-link>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div class="waybill-manage-page__dialog-pagination">
|
||
<el-pagination
|
||
v-model:current-page="transportStationPage.currentPage"
|
||
v-model:page-size="transportStationPage.pageSize"
|
||
:page-sizes="[10, 20, 50, 100]"
|
||
layout="total, prev, pager, next, sizes"
|
||
:total="transportStationPage.total"
|
||
@size-change="handleTransportStationSizeChange"
|
||
@current-change="handleTransportStationCurrentChange"
|
||
/>
|
||
</div>
|
||
<template #footer>
|
||
<el-button @click="transportStationBox = false">关闭</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog
|
||
title="地图选择地址"
|
||
append-to-body
|
||
v-model="transportMapBox"
|
||
width="920px"
|
||
@opened="initTransportAmap"
|
||
>
|
||
<div class="waybill-manage-page__map-toolbar">
|
||
<el-input
|
||
v-model="transportMapKeyword"
|
||
clearable
|
||
placeholder="输入地址关键词"
|
||
@keyup.enter="searchTransportMapKeyword"
|
||
/>
|
||
<el-button
|
||
type="primary"
|
||
:icon="Search"
|
||
:loading="transportMapLoading"
|
||
@click="searchTransportMapKeyword"
|
||
>
|
||
搜索
|
||
</el-button>
|
||
</div>
|
||
<div class="map-picker-content">
|
||
<map-search-results :results="transportMapSearchResults" @select="selectTransportMapSearchResult" />
|
||
<div ref="transportAmap" class="waybill-manage-page__map"></div>
|
||
</div>
|
||
<div class="waybill-manage-page__map-info">
|
||
<span>{{ transportMapStatus }}</span>
|
||
<span v-if="transportMapSelected.regionName"
|
||
>行政区划:{{ transportMapSelected.regionName }}</span
|
||
>
|
||
</div>
|
||
<template #footer>
|
||
<el-button @click="transportMapBox = false">取消</el-button>
|
||
<el-button
|
||
type="primary"
|
||
:disabled="!transportMapSelected.longitude"
|
||
@click="confirmTransportMapPick"
|
||
>
|
||
确定
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog
|
||
title="选择常用货物"
|
||
append-to-body
|
||
v-model="commonCargoBox"
|
||
width="1280px"
|
||
@opened="loadCommonCargoList"
|
||
>
|
||
<div class="waybill-manage-page__dialog-search">
|
||
<el-form :model="commonCargoQuery" label-position="right" label-width="100px">
|
||
<el-row :gutter="16">
|
||
<el-col :span="6">
|
||
<el-form-item label="货物名称">
|
||
<el-input v-model="commonCargoQuery.cargoName" clearable placeholder="请输入" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<el-form-item label="货物类型">
|
||
<el-cascader
|
||
v-model="commonCargoQuery.cargoTypePath"
|
||
:options="cargoTypeOptions"
|
||
:props="cargoTypeCascaderProps"
|
||
placeholder="请选择"
|
||
clearable
|
||
filterable
|
||
:filter-method="filterCargoType"
|
||
@visible-change="visible => visible && ensureCargoTypeOptions()"
|
||
@change="handleCommonCargoTypeChange"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<el-form-item label="规格型号">
|
||
<el-input
|
||
v-model="commonCargoQuery.specificationModel"
|
||
clearable
|
||
placeholder="请输入"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="6" class="waybill-manage-page__dialog-search-actions">
|
||
<el-button type="primary" @click="handleCommonCargoSearch">查询</el-button>
|
||
<el-button @click="handleCommonCargoReset">重置</el-button>
|
||
</el-col>
|
||
</el-row>
|
||
</el-form>
|
||
</div>
|
||
<el-table
|
||
border
|
||
height="320"
|
||
:data="commonCargoRows"
|
||
v-loading="commonCargoLoading"
|
||
@selection-change="handleCommonCargoSelectionChange"
|
||
>
|
||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||
<el-table-column prop="cargoName" label="货物名称" min-width="150" />
|
||
<el-table-column prop="cargoCode" label="货物编号" min-width="130" />
|
||
<el-table-column prop="firstCargoTypeName" label="一级货物类型" min-width="150" />
|
||
<el-table-column prop="secondCargoTypeName" label="二级货物类型" min-width="150" />
|
||
<el-table-column prop="secondCargoTypeCode" label="二级货物编码" min-width="140" />
|
||
<el-table-column prop="packageType" label="包装" min-width="100" />
|
||
<el-table-column prop="brand" label="品牌" min-width="120" />
|
||
<el-table-column prop="specification" label="规格" min-width="120" />
|
||
<el-table-column prop="model" label="型号" min-width="120" />
|
||
<el-table-column prop="cargoValue" label="单价" min-width="120" />
|
||
<el-table-column prop="priceUnit" label="计价单位" min-width="120" />
|
||
<el-table-column
|
||
prop="cargoSize"
|
||
label="尺寸"
|
||
min-width="120"
|
||
:formatter="formatCommonCargoSize"
|
||
/>
|
||
<el-table-column
|
||
prop="descriptionOne"
|
||
label="其他说明1"
|
||
min-width="160"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column
|
||
prop="descriptionTwo"
|
||
label="其他说明2"
|
||
min-width="160"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip />
|
||
<el-table-column prop="deptName" label="组织" min-width="150" show-overflow-tooltip />
|
||
<el-table-column prop="updateTime" label="更新时间" min-width="160" sortable />
|
||
<el-table-column prop="createTime" label="创建时间" min-width="160" sortable />
|
||
<el-table-column label="操作" width="110" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-link type="primary" @click.stop="selectCommonCargoRow(row)"> 选择 </el-link>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div class="waybill-manage-page__dialog-pagination">
|
||
<el-pagination
|
||
v-model:current-page="commonCargoPage.currentPage"
|
||
v-model:page-size="commonCargoPage.pageSize"
|
||
:page-sizes="[10, 20, 50, 100]"
|
||
layout="total, prev, pager, next, sizes"
|
||
:total="commonCargoPage.total"
|
||
@size-change="handleCommonCargoSizeChange"
|
||
@current-change="handleCommonCargoCurrentChange"
|
||
/>
|
||
</div>
|
||
<template #footer>
|
||
<el-button @click="commonCargoBox = false">关闭</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog
|
||
title="导入货物"
|
||
append-to-body
|
||
v-model="cargoImportBox"
|
||
width="560px"
|
||
class="waybill-manage-page__cargo-import-dialog"
|
||
@closed="cargoImportRows = []"
|
||
>
|
||
<div class="waybill-manage-page__cargo-import">
|
||
<div class="waybill-manage-page__cargo-import-card">
|
||
<el-upload
|
||
action="#"
|
||
accept=".xls,.xlsx"
|
||
:auto-upload="false"
|
||
:show-file-list="false"
|
||
:disabled="cargoImportLoading"
|
||
:on-change="handleCargoImportFileChange"
|
||
>
|
||
<el-button type="primary" :loading="cargoImportLoading">上传</el-button>
|
||
<template #tip>
|
||
<div class="el-upload__tip">支持.xlsx、.xls的文件格式,单个文件大小限制50M</div>
|
||
</template>
|
||
</el-upload>
|
||
<el-button type="primary" @click="handleCargoImportTemplate">点击下载模板</el-button>
|
||
</div>
|
||
</div>
|
||
<template #footer>
|
||
<el-button :disabled="cargoImportLoading" @click="closeCargoImportDialog">关闭</el-button>
|
||
<el-button
|
||
type="primary"
|
||
:loading="cargoImportLoading"
|
||
:disabled="!cargoImportRows.length"
|
||
@click="confirmCargoImport"
|
||
>
|
||
确定
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog
|
||
v-model="waybillProcessConfigBox"
|
||
title="过程配置"
|
||
append-to-body
|
||
width="980px"
|
||
class="waybill-manage-page__process-config-dialog"
|
||
>
|
||
<div v-loading="waybillProcessConfigLoading">
|
||
<el-empty
|
||
v-if="!waybillProcessConfigRows.length"
|
||
description="当前项目暂无过程配置"
|
||
:image-size="72"
|
||
/>
|
||
<section
|
||
v-for="row in waybillProcessConfigRows"
|
||
:key="row.id || row.configCode"
|
||
class="waybill-manage-page__process-config-section"
|
||
>
|
||
<div class="waybill-manage-page__process-config-meta">
|
||
<span>{{ row.configName || '过程配置' }}</span>
|
||
<span v-if="row.defaultFinishDays">默认{{ row.defaultFinishDays }}天后完成运输</span>
|
||
</div>
|
||
<div
|
||
v-if="getProcessConfigEnabledNodes(row).length"
|
||
class="waybill-manage-page__process-timeline"
|
||
>
|
||
<div class="waybill-manage-page__process-timeline-line"></div>
|
||
<div
|
||
v-for="node in getProcessConfigEnabledNodes(row)"
|
||
:key="node.key"
|
||
class="waybill-manage-page__process-timeline-item"
|
||
>
|
||
<span class="waybill-manage-page__process-timeline-dot"></span>
|
||
<div class="waybill-manage-page__process-timeline-name">{{ node.name }}</div>
|
||
<div class="waybill-manage-page__process-timeline-desc">
|
||
<div v-for="text in processConfigTimelineDescriptions(node)" :key="text">
|
||
{{ text }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<el-empty v-else description="未配置现行过程节点" :image-size="56" />
|
||
</section>
|
||
</div>
|
||
<template #footer>
|
||
<el-button type="primary" @click="waybillProcessConfigBox = false">关闭</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog :title="`${config.title}导入`" append-to-body v-model="excelBox" width="555px">
|
||
<avue-form :option="excelOptionConfig" v-model="excelForm">
|
||
<template #excelTemplate>
|
||
<el-button type="primary" @click="handleTemplate">
|
||
点击下载<i class="el-icon-download el-icon--right"></i>
|
||
</el-button>
|
||
</template>
|
||
</avue-form>
|
||
</el-dialog>
|
||
</basic-container>
|
||
</template>
|
||
|
||
<script>
|
||
import { exportBlob, importBlob } from '@/api/common';
|
||
import { getList as getCommonAddressList } from '@/api/base/common-address';
|
||
import { getList as getAirportMasterList } from '@/api/base/airport-master';
|
||
import { getList as getCargoTypeList } from '@/api/base/cargo-type';
|
||
import { getList as getPortTerminalList } from '@/api/base/port-terminal';
|
||
import { getList as getRailwayStationList } from '@/api/base/railway-station';
|
||
import { getList as getCommonCargoList } from '@/api/business/common-cargo';
|
||
import {
|
||
getDetail as getContractDetail,
|
||
getList as getContractList,
|
||
} from '@/api/business/contract-manage';
|
||
import {
|
||
getDetail as getProjectDetail,
|
||
getList as getProjectList,
|
||
} from '@/api/business/project-apply';
|
||
import { getList as getLoadingList } from '@/api/business/loading-manage';
|
||
import { getList as getCommonRouteList } from '@/api/business/common-route';
|
||
import {
|
||
getDetail as getShippingPlanDetail,
|
||
getList as getShippingPlanList,
|
||
} from '@/api/business/transport-plan';
|
||
import {
|
||
getDetail as getProcessConfigDetail,
|
||
getList as getProcessConfigList,
|
||
getVoucherImages as getProcessConfigVoucherImages,
|
||
} from '@/api/business/process-config';
|
||
import { getPunchRecords as getWaybillPunchRecords, locateVehicle, trackVehicle } from '@/api/business/waybill-manage';
|
||
import { getList as getDriverList } from '@/api/transportCapacity/driver';
|
||
import { getDictionary } from '@/api/system/dictbiz';
|
||
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
|
||
import { addressTypeOptions } from '@/option/base/common-address';
|
||
import { packageOptions } from '@/option/business/common';
|
||
import { formatUpdateUserName } from '@/utils/audit';
|
||
import { getToken } from '@/utils/auth';
|
||
import { openImportDialog } from '@/utils/import-excel';
|
||
import { applyTableMenuWidth } from '@/utils/table-menu';
|
||
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||
import { isMobile } from '@/utils/validate';
|
||
import { InfoFilled, Location, OfficeBuilding, Rank, Search, WarnTriangleFilled } from '@element-plus/icons-vue';
|
||
import { ElImageViewer } from 'element-plus';
|
||
import { OpenFileViewer } from '@open-file-viewer/vue';
|
||
import {
|
||
fallbackPlugin,
|
||
imagePlugin,
|
||
officePlugin,
|
||
pdfPlugin,
|
||
textPlugin,
|
||
} from '@open-file-viewer/core';
|
||
import '@open-file-viewer/core/style.css';
|
||
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
|
||
import { h, resolveComponent } from 'vue';
|
||
import { mapGetters } from 'vuex';
|
||
import NProgress from 'nprogress';
|
||
import 'nprogress/nprogress.css';
|
||
|
||
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
|
||
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
|
||
let amapLoader;
|
||
const standaloneWaybillFormRoute = '/business/waybill-manage/form';
|
||
const standaloneWaybillFormPaths = ['/business/waybill-manage', standaloneWaybillFormRoute];
|
||
const standaloneWaybillDetailRoute = '/business/waybill-manage/detail';
|
||
const loadingCreateWaybillsStorageKey = 'loading-manage-create-waybills';
|
||
|
||
const attachmentViewerPlugins = [
|
||
imagePlugin(),
|
||
pdfPlugin({ workerSrc: pdfWorkerSrc, useFetchData: true }),
|
||
officePlugin({ pdf: { workerSrc: pdfWorkerSrc, useFetchData: true } }),
|
||
textPlugin(),
|
||
fallbackPlugin(),
|
||
];
|
||
|
||
const PageAvueForm = {
|
||
inheritAttrs: false,
|
||
props: {
|
||
formPageTitle: { type: String, default: '' },
|
||
},
|
||
render() {
|
||
const attrs = { ...this.$attrs };
|
||
const rowSave = attrs.onRowSave;
|
||
const rowUpdate = attrs.onRowUpdate;
|
||
const boxType = attrs.option?.boxType;
|
||
delete attrs.onRowSave;
|
||
delete attrs.onRowUpdate;
|
||
attrs.onSubmit = (row, hide) => {
|
||
if (boxType === 'edit') {
|
||
// Avue form passes its state-reset callback as the second argument. Keep it
|
||
// available as the failure callback as well, otherwise a rejected request
|
||
// leaves the standalone form permanently disabled.
|
||
rowUpdate?.(row, undefined, hide, hide);
|
||
} else {
|
||
rowSave?.(row, hide, hide);
|
||
}
|
||
};
|
||
const slots = Object.fromEntries(
|
||
Object.entries(this.$slots).flatMap(([name, slot]) => {
|
||
if (name === 'menu-form' || name === 'menu-form-before') return [[name, slot]];
|
||
const prop = name.replace(/-form$/, '');
|
||
return [
|
||
[prop, slot],
|
||
[`${prop}-header`, slot],
|
||
];
|
||
})
|
||
);
|
||
const children = [];
|
||
if (this.formPageTitle) {
|
||
children.push(h('div', { class: 'archive-page-form__title' }, this.formPageTitle));
|
||
}
|
||
children.push(h(getPlainAvueForm(resolveComponent('avue-form')), attrs, slots));
|
||
return h(
|
||
'div',
|
||
{ class: attrs.option?.dialogCustomClass || 'waybill-manage-form-page-dialog' },
|
||
children
|
||
);
|
||
},
|
||
};
|
||
|
||
const PageDetail = {
|
||
inheritAttrs: false,
|
||
render() {
|
||
return h('div', { class: this.$attrs.class }, [
|
||
...(this.$slots.default?.() || []),
|
||
...(this.$slots.footer?.() || []),
|
||
]);
|
||
},
|
||
};
|
||
|
||
const estimateMenuButtonCount = config => {
|
||
const rowActions = (config.actions || []).filter(action => action !== 'batchComplete');
|
||
return 3 + rowActions.length + (config.operations || []).length;
|
||
};
|
||
|
||
const extractRecords = res => {
|
||
const data = res?.data?.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 WAYBILL_QUANTITY_UNIT = '吨';
|
||
|
||
const defaultTransportCargo = () => ({
|
||
cargoName: '',
|
||
cargoType: '',
|
||
cargoTypeCode: '',
|
||
cargoTypePath: [],
|
||
quantity: '',
|
||
quantityUnit: '',
|
||
unitPrice: '',
|
||
priceUnit: '元/吨',
|
||
freightAmount: '',
|
||
packageType: '',
|
||
brand: '',
|
||
specification: '',
|
||
model: '',
|
||
materialCode: '',
|
||
deviceCode: '',
|
||
remark: '',
|
||
});
|
||
|
||
const taskCarrierTypes = ['承运商', '自运'];
|
||
|
||
// Avue 默认分组使用折叠容器。独立运单表单需要始终展示各业务分区,
|
||
// 因此仅替换本页面的分组渲染器,保留 avue-form 的字段渲染和校验能力。
|
||
const PlainAvueGroup = {
|
||
name: 'PlainAvueGroup',
|
||
inheritAttrs: false,
|
||
props: {
|
||
display: { type: Boolean, default: true },
|
||
header: { type: Boolean, default: true },
|
||
label: { type: String, default: '' },
|
||
icon: { type: String, default: '' },
|
||
},
|
||
render() {
|
||
if (!this.display) return null;
|
||
const children = [];
|
||
if (this.$slots.tabs) children.push(...this.$slots.tabs());
|
||
if (this.header && (this.$slots.header || this.label || this.icon)) {
|
||
children.push(
|
||
h('div', { class: 'avue-group__header' }, [
|
||
this.$slots.header
|
||
? this.$slots.header()
|
||
: h('h1', { class: 'avue-group__title' }, this.label),
|
||
])
|
||
);
|
||
}
|
||
if (this.$slots.default) children.push(...this.$slots.default());
|
||
return h('div', { class: 'avue-group' }, children);
|
||
},
|
||
};
|
||
|
||
const plainAvueFormCache = new WeakMap();
|
||
const getPlainAvueForm = avueForm => {
|
||
if (!avueForm || typeof avueForm !== 'object') return avueForm;
|
||
if (!plainAvueFormCache.has(avueForm)) {
|
||
plainAvueFormCache.set(avueForm, {
|
||
...avueForm,
|
||
components: {
|
||
...(avueForm.components || {}),
|
||
avueGroup: PlainAvueGroup,
|
||
'avue-group': PlainAvueGroup,
|
||
},
|
||
});
|
||
}
|
||
return plainAvueFormCache.get(avueForm);
|
||
};
|
||
|
||
export default {
|
||
components: {
|
||
PageAvueForm,
|
||
PageDetail,
|
||
InfoFilled,
|
||
WarnTriangleFilled,
|
||
Rank,
|
||
ElImageViewer,
|
||
OpenFileViewer,
|
||
},
|
||
name: 'WaybillManagePage',
|
||
props: {
|
||
api: {
|
||
type: Object,
|
||
required: true,
|
||
},
|
||
config: {
|
||
type: Object,
|
||
required: true,
|
||
},
|
||
crudOption: {
|
||
type: Object,
|
||
required: true,
|
||
},
|
||
crudExcelOption: {
|
||
type: Object,
|
||
default: () => ({ column: [] }),
|
||
},
|
||
// 可选:固定操作列宽度;不传则按按钮数量自动估算(沿用既有逻辑)
|
||
menuWidth: {
|
||
type: Number,
|
||
default: null,
|
||
},
|
||
detailId: {
|
||
type: [String, Number],
|
||
default: null,
|
||
},
|
||
standaloneFormPage: {
|
||
type: Boolean,
|
||
default: false,
|
||
},
|
||
standaloneDetailPage: {
|
||
type: Boolean,
|
||
default: false,
|
||
},
|
||
},
|
||
data() {
|
||
return {
|
||
Location,
|
||
OfficeBuilding,
|
||
Search,
|
||
form: {},
|
||
suppressTransportTypeClear: false,
|
||
query: {},
|
||
loading: true,
|
||
data: [],
|
||
allDept: 0,
|
||
dialogReadonly: false,
|
||
detailBox: false,
|
||
detailLoading: false,
|
||
detailRow: {},
|
||
waybillDetailProcessNodes: [],
|
||
waybillPunchRecords: [],
|
||
waybillDriverUploads: [],
|
||
waybillPunchRecordsLoading: false,
|
||
waybillProcessDetailTab: 'punch',
|
||
waybillHasRelatedVoucher: false,
|
||
waybillVoucherImages: [],
|
||
waybillVoucherFolders: [],
|
||
waybillVoucherFolder: null,
|
||
waybillVoucherFolderView: false,
|
||
waybillVoucherImagesLoading: false,
|
||
waybillVoucherImagesLoaded: false,
|
||
waybillRouteChangeBox: false,
|
||
waybillRouteChangeTab: 'change',
|
||
waybillRouteChangeRows: [],
|
||
waybillRouteChangeNodes: [],
|
||
waybillRouteChangeRemark: '',
|
||
waybillRouteChangeRecords: [],
|
||
waybillRouteChangeDragIndex: -1,
|
||
waybillRouteChangeSaving: false,
|
||
waybillLocateLoading: false,
|
||
waybillLocateInfo: null,
|
||
waybillLocateHint: '',
|
||
waybillLocateMapInstance: null,
|
||
waybillLocateMarker: null,
|
||
waybillLocateInfoWindow: null,
|
||
waybillTrackMode: '',
|
||
waybillTrackLoading: false,
|
||
waybillTrackInfo: null,
|
||
waybillTrackHint: '',
|
||
waybillTrackDateRange: [],
|
||
waybillTrackMapInstance: null,
|
||
waybillTrackPolyline: null,
|
||
waybillTrackStartMarker: null,
|
||
waybillTrackEndMarker: null,
|
||
mileageDialog: {
|
||
visible: false,
|
||
submitting: false,
|
||
row: null,
|
||
},
|
||
reassignDrawer: {
|
||
visible: false,
|
||
submitting: false,
|
||
driverLoading: false,
|
||
row: null,
|
||
},
|
||
reassignForm: {
|
||
id: '',
|
||
driverId: '',
|
||
driverName: '',
|
||
driverPhone: '',
|
||
vehicleNo: '',
|
||
},
|
||
reassignDriverOptions: [],
|
||
reassignRules: {
|
||
driverName: [{ required: true, message: '请选择司机', trigger: 'change' }],
|
||
driverPhone: [
|
||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||
{
|
||
validator: (rule, value, callback) => {
|
||
if (!value) {
|
||
callback();
|
||
return;
|
||
}
|
||
if (!isMobile(value)) {
|
||
callback(new Error('请输入正确的手机号'));
|
||
return;
|
||
}
|
||
callback();
|
||
},
|
||
trigger: 'blur',
|
||
},
|
||
],
|
||
vehicleNo: [{ required: true, message: '请输入车牌号', trigger: 'blur' }],
|
||
},
|
||
mileageForm: {
|
||
id: '',
|
||
mileage: '',
|
||
mileageRemark: '',
|
||
},
|
||
mileageRules: {
|
||
mileage: [
|
||
{ required: true, message: '请输入里程', trigger: 'blur' },
|
||
{
|
||
pattern: /^[1-9]\d{0,9}$/,
|
||
message: '里程必须为不超过10位的正整数',
|
||
trigger: 'blur',
|
||
},
|
||
],
|
||
},
|
||
waybillCommonAddressBox: false,
|
||
waybillCommonAddressLoading: false,
|
||
waybillCommonAddressQuery: {
|
||
addressName: '',
|
||
},
|
||
waybillCommonAddressRows: [],
|
||
waybillCommonAddressTarget: null,
|
||
waybillCommonAddressPage: {
|
||
pageSize: 10,
|
||
currentPage: 1,
|
||
total: 0,
|
||
},
|
||
draftSaveLoading: false,
|
||
crudDialogType: '',
|
||
defaultDialogButtons: null,
|
||
option: (() => {
|
||
const opt = this.cloneOption(this.crudOption, estimateMenuButtonCount(this.config));
|
||
if (this.menuWidth != null) opt.menuWidth = this.menuWidth;
|
||
if (this.standaloneFormPage && standaloneWaybillFormPaths.includes(this.$route.path)) {
|
||
opt.dialogAppendToBody = false;
|
||
opt.dialogModal = false;
|
||
opt.dialogCustomClass = 'waybill-manage-form-page-dialog';
|
||
opt.dialogTop = '0';
|
||
opt.dialogWidth = '100%';
|
||
} else if (!opt.dialogCustomClass) {
|
||
// 普通弹窗模式:灰底 body + 白卡表单(与独立表单页风格一致)
|
||
opt.dialogCustomClass = 'waybill-manage-form-dialog';
|
||
}
|
||
return opt;
|
||
})(),
|
||
selectedProjectId: '',
|
||
projectOptions: [],
|
||
projectLoading: false,
|
||
contractOptions: [],
|
||
contractLoading: false,
|
||
shippingPlanOptions: [],
|
||
shippingPlanLoading: false,
|
||
addressTypeOptions,
|
||
flowBox: false,
|
||
flowUrl: '',
|
||
processInstanceId: '',
|
||
attachmentRows: [],
|
||
selectedAttachmentRows: [],
|
||
attachmentImagePreviewVisible: false,
|
||
attachmentImagePreviewUrls: [],
|
||
attachmentImagePreviewIndex: 0,
|
||
attachmentDocumentPreviewVisible: false,
|
||
attachmentPreviewFile: {},
|
||
attachmentViewerPlugins,
|
||
attachmentViewerToolbar: {
|
||
download: true,
|
||
fullscreen: true,
|
||
print: true,
|
||
rotate: true,
|
||
zoom: true,
|
||
},
|
||
transportCargoRows: [],
|
||
taskFreightCurrency: 'RMB',
|
||
freightCurrencyOptions: [],
|
||
freightCurrencyLoading: false,
|
||
waybillProcessConfigBox: false,
|
||
waybillProcessConfigLoading: false,
|
||
waybillProcessConfigRows: [],
|
||
hasProjectProcessConfig: false,
|
||
transportRouteBox: false,
|
||
transportRouteLoading: false,
|
||
transportRouteQuery: {},
|
||
transportRouteRows: [],
|
||
transportRouteSelected: null,
|
||
transportRoutePage: {
|
||
pageSize: 10,
|
||
currentPage: 1,
|
||
total: 0,
|
||
},
|
||
transportAddressBox: false,
|
||
transportAddressTarget: '',
|
||
transportAddressLoading: false,
|
||
transportAddressQuery: {},
|
||
transportTypeOptions: [],
|
||
transportTypeOptionsLoading: false,
|
||
transportAddressRows: [],
|
||
transportAddressSelected: null,
|
||
transportAddressPage: {
|
||
pageSize: 10,
|
||
currentPage: 1,
|
||
total: 0,
|
||
},
|
||
transportStationBox: false,
|
||
transportStationTarget: '',
|
||
transportStationLoading: false,
|
||
transportStationQuery: {},
|
||
transportStationRows: [],
|
||
transportStationPage: {
|
||
pageSize: 10,
|
||
currentPage: 1,
|
||
total: 0,
|
||
},
|
||
transportMapBox: false,
|
||
transportMapTarget: '',
|
||
transportMapLoading: false,
|
||
transportMapKeyword: '',
|
||
transportMapStatus: '可搜索地址或点击地图选点',
|
||
transportMapSelected: {},
|
||
transportMapSearchResults: [],
|
||
transportAmap: null,
|
||
transportAmapMarker: null,
|
||
transportAmapGeocoder: null,
|
||
commonCargoBox: false,
|
||
commonCargoLoading: false,
|
||
commonCargoQuery: {},
|
||
commonCargoRows: [],
|
||
commonCargoSelected: [],
|
||
commonCargoPage: {
|
||
pageSize: 10,
|
||
currentPage: 1,
|
||
total: 0,
|
||
},
|
||
taskCarrierTypes,
|
||
taskCarrierOptions: [],
|
||
taskCarrierLoading: false,
|
||
taskCarrierRequestId: 0,
|
||
waybillCarrierContractsLoaded: false,
|
||
waybillHasCarrierContracts: null,
|
||
waybillCarrierContractRequestId: 0,
|
||
taskDriverOptions: [],
|
||
taskDriverLoading: false,
|
||
taskEscortOptions: [],
|
||
taskEscortLoading: false,
|
||
taskCargoOptionsMap: {},
|
||
taskCargoLoadingMap: {},
|
||
taskQuantityUnitOptions: [],
|
||
taskQuantityUnitLoading: false,
|
||
taskFeeUnitOptions: [],
|
||
taskFeeUnitLoading: false,
|
||
cargoImportBox: false,
|
||
cargoImportLoading: false,
|
||
cargoImportRows: [],
|
||
cargoTypeOptions: [],
|
||
cargoTypeFlatOptions: [],
|
||
cargoTypeLoading: false,
|
||
cargoTypeRequest: null,
|
||
packageOptions,
|
||
quantityUnitOptions: ['吨', '千克', '立方米', '件', '车', '箱', '托盘'],
|
||
attachmentFileTypes: [
|
||
'pdf',
|
||
'bmp',
|
||
'jpeg',
|
||
'png',
|
||
'jpg',
|
||
'doc',
|
||
'docx',
|
||
'ppt',
|
||
'pptx',
|
||
'xlsx',
|
||
'xls',
|
||
'eml',
|
||
'msg',
|
||
'zip',
|
||
],
|
||
excelBox: false,
|
||
excelForm: {},
|
||
excelOptionConfig: this.cloneOption(this.crudExcelOption),
|
||
page: {
|
||
pageSize: 10,
|
||
pageSizes: [10, 20, 50, 100],
|
||
currentPage: 1,
|
||
total: 0,
|
||
},
|
||
selectionList: [],
|
||
attachmentUploadMode: false,
|
||
standaloneFormKey: '',
|
||
// 实例所属路由的路径快照:用于判断当前 $route 是否还是「本实例自己的路由」。
|
||
// 必须在 data 里初始化,不能放 created —— Vue 的 Options API 执行顺序是
|
||
// data → computed → watch(immediate) → created,放 created 会被首个
|
||
// immediate watcher 读到空值,把本该执行的首次加载也挡掉。
|
||
routePathLocked: this.$route.path,
|
||
// 页面形态(列表 / 独立表单页 / 独立详情页)在实例创建时锁定一次,之后不再随 $route 变化。
|
||
// 原因:标签页 keep-alive 只会让实例 deactivate,实例仍会随 $route(全局响应式)重新渲染;
|
||
// 若形态跟着路由翻回列表/弹窗态,缓存实例会渲染出 append-to-body 的 el-dialog —— 弹窗被
|
||
// Teleport 到 body 后,KeepAlive.deactivate 的 move 搬不动它,DOM 会永久残留在页面上,
|
||
// 表现为“从运单详情/独立表单离开后点其它菜单,详情弹窗又冒出来”。
|
||
formPageLocked: false,
|
||
detailPageLocked: false,
|
||
// 同上:表单模式(add / edit)也一次性锁定,避免缓存实例随 $route 变 query 导致 option 变形
|
||
formModeLocked: '',
|
||
};
|
||
},
|
||
created() {
|
||
this.formPageLocked = this.isStandaloneWaybillFormPage;
|
||
this.detailPageLocked = this.isStandaloneWaybillDetailPage;
|
||
this.formModeLocked = this.$route.query.mode || '';
|
||
if (this.config.enableAllDept && this.isAdmin) {
|
||
this.allDept = 1;
|
||
}
|
||
if (this.config.enableProjectSelect) {
|
||
this.loadProjectOptions();
|
||
}
|
||
if (this.config.enableTaskInfoForm) {
|
||
this.loadTaskQuantityUnitOptions();
|
||
this.loadTaskFeeUnitOptions();
|
||
this.ensureCargoTypeOptions();
|
||
}
|
||
if (this.config.enableWaybillFooterFreightSummary) this.loadFreightCurrencyOptions();
|
||
if (this.shippingInfoFormEnabled) this.loadTransportTypeOptions();
|
||
if (this.isStandaloneWaybillFormPage) {
|
||
this.$nextTick(() => {
|
||
this.initStandaloneFormPage();
|
||
// 先完成独立表单初始化,再回填模板数据,避免被新增表单默认值覆盖。
|
||
this.$nextTick(() => this.consumeTemplateCreatePayload());
|
||
});
|
||
} else {
|
||
this.consumeTemplateCreatePayload();
|
||
}
|
||
},
|
||
// 标签切走(实例被 keep-alive 缓存)或销毁时,收起本页所有 append-to-body 的弹窗。
|
||
// 否则 Teleport 出去的弹窗 DOM 会一直留在 body 上,盖住后续打开的页面。
|
||
deactivated() {
|
||
this.closeInnerDialogs();
|
||
},
|
||
beforeUnmount() {
|
||
this.closeInnerDialogs();
|
||
},
|
||
computed: {
|
||
...mapGetters(['permission', 'userInfo']),
|
||
permissionList() {
|
||
return {
|
||
addBtn: this.canCreate,
|
||
};
|
||
},
|
||
// 打卡时间轴按打卡时间先后展示:已打卡记录按打卡时间升序在前,
|
||
// 未打卡记录保持流程节点原顺序排在其后。
|
||
orderedWaybillPunchRecords() {
|
||
const records = Array.isArray(this.waybillPunchRecords) ? this.waybillPunchRecords : [];
|
||
return records
|
||
.map((record, index) => ({
|
||
record,
|
||
index,
|
||
punchedAt: this.waybillPunchRecordTimestamp(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);
|
||
},
|
||
isStandaloneWaybillPage() {
|
||
return this.standaloneFormPage && standaloneWaybillFormPaths.includes(this.$route.path);
|
||
},
|
||
isStandaloneWaybillFormPage() {
|
||
return this.isStandaloneWaybillPage && ['add', 'edit'].includes(this.$route.query.mode);
|
||
},
|
||
isStandaloneWaybillDetailPage() {
|
||
return this.standaloneDetailPage && this.$route.path === standaloneWaybillDetailRoute;
|
||
},
|
||
// 容器形态读实例创建时锁定的标志,不随 $route 翻转(详见 data 中 formPageLocked 的说明)
|
||
// 当前路由是否仍属于本实例(tab.js 按 fullPath 建实例,一个实例只对应一个 path)。
|
||
// 被 keep-alive 缓存后如果 $route 已经跑到别的页面上,就不该再用「当前页面的 id」
|
||
// 去触发本实例的请求或跳转,否则会拿别的单据 id 查自己的接口。
|
||
isOwnedRouteActive() {
|
||
return this.$route.path === this.routePathLocked;
|
||
},
|
||
detailContainer() {
|
||
return this.detailPageLocked ? 'PageDetail' : 'el-dialog';
|
||
},
|
||
formPageTitle() {
|
||
if (this.isStandaloneWaybillFormPage) {
|
||
return (
|
||
this.$route.query.name ||
|
||
`${this.$route.query.mode === 'edit' ? '编辑' : '新增'}${this.config.title || ''}`
|
||
);
|
||
}
|
||
return '';
|
||
},
|
||
crudContainer() {
|
||
return this.formPageLocked ? 'PageAvueForm' : 'avue-crud';
|
||
},
|
||
pageFormOption() {
|
||
if (!this.formPageLocked) return this.option;
|
||
const base = {
|
||
...this.option,
|
||
boxType: this.formModeLocked === 'edit' ? 'edit' : 'add',
|
||
menuBtn: true,
|
||
submitBtn: true,
|
||
emptyBtn: false,
|
||
};
|
||
return this.buildFormGroupOption(base);
|
||
},
|
||
waybillDetailSummaryFields() {
|
||
return [
|
||
['customerName', '客户', true],
|
||
['contractNo', '合同编号', true],
|
||
['projectName', '项目', true],
|
||
['originalNo', '原始单号'],
|
||
['planName', '计划名称'],
|
||
['planNo', '计划单号', true],
|
||
['masterNo', '总单号', true],
|
||
['loadingNo', '配载单号', true],
|
||
];
|
||
},
|
||
isTemplateCreate() {
|
||
return this.config.permission === 'waybill_manage' && Boolean(this.$route.query.fromTemplate);
|
||
},
|
||
completeActionLabel() {
|
||
return '完成';
|
||
},
|
||
isRoadTransport() {
|
||
return this.transportMode === 'road';
|
||
},
|
||
isNonRoadTransport() {
|
||
return !this.isRoadTransport;
|
||
},
|
||
transportVehicleNoLabel() {
|
||
return this.isRoadTransport ? '车牌号' : '船/航/班列号';
|
||
},
|
||
actionButtonLikeSearch() {
|
||
return this.config.actionButtonLikeSearch === true;
|
||
},
|
||
contractSelectEnabled() {
|
||
return this.config.enableContractSelect === true;
|
||
},
|
||
shippingPlanSelectEnabled() {
|
||
return this.config.enableShippingPlanSelect === true;
|
||
},
|
||
shippingInfoFormEnabled() {
|
||
return this.config.enableShippingInfoForm === true;
|
||
},
|
||
taskInfoFormEnabled() {
|
||
return this.config.enableTaskInfoForm === true;
|
||
},
|
||
taskEntryModeOptions() {
|
||
const locked = this.hasMultipleConfiguredGoods(this.form, this.transportCargoRows);
|
||
return [
|
||
{ label: '精简录入', value: 'simple', disabled: locked },
|
||
{ label: '完整录入', value: 'full' },
|
||
];
|
||
},
|
||
isTaskFullMode() {
|
||
return this.form.taskEntryMode === 'full';
|
||
},
|
||
isCarrierMode() {
|
||
return this.form.carrierType === '承运商';
|
||
},
|
||
taskCarrierRequired() {
|
||
return this.isTaskCarrierRequired(this.form.carrierType);
|
||
},
|
||
waybillTaskCarrierTypes() {
|
||
if (this.waybillCarrierContractsLoaded && this.waybillHasCarrierContracts === false) {
|
||
return ['自运'];
|
||
}
|
||
return this.taskCarrierTypes;
|
||
},
|
||
taskCarrierLabel() {
|
||
return '承运商';
|
||
},
|
||
taskCarrierValue: {
|
||
get() {
|
||
return this.form.carrierType === '自运'
|
||
? this.form.carrierName || ''
|
||
: this.form.carrierContractId || this.form.carrierName || '';
|
||
},
|
||
set(value) {
|
||
this.handleTaskCarrierChange(value);
|
||
},
|
||
},
|
||
showRoadEscortFields() {
|
||
return this.isRoadTransport && !this.isCarrierMode;
|
||
},
|
||
taskCargoOptions() {
|
||
return this.getTaskCargoOptionsByPath(this.form.cargoTypePath);
|
||
},
|
||
taskCargoLoading() {
|
||
return this.isTaskCargoOptionsLoading(this.form.cargoTypePath);
|
||
},
|
||
taskCargoQuantityTotal() {
|
||
const total = this.transportCargoRows.reduce((sum, row) => {
|
||
const value = Number(row.quantity);
|
||
return Number.isFinite(value) ? sum + value : sum;
|
||
}, 0);
|
||
if (!total) return '';
|
||
return Number.isInteger(total) ? String(total) : String(Number(total.toFixed(3)));
|
||
},
|
||
fixedQuantityUnit() {
|
||
return WAYBILL_QUANTITY_UNIT;
|
||
},
|
||
taskFreightAmount() {
|
||
const quantity = Number(this.taskCargoQuantityTotal || this.form.quantity || 0);
|
||
const unitPrice = Number(this.form.unitPrice || 0);
|
||
if (!quantity || !unitPrice) return '';
|
||
const amount = quantity * unitPrice;
|
||
return Number.isInteger(amount) ? String(amount) : String(Number(amount.toFixed(2)));
|
||
},
|
||
taskFreightGroups() {
|
||
// 运单统一按吨计价,运费录入只保留一行汇总所有货物。
|
||
return [
|
||
{
|
||
key: '__all__',
|
||
quantityUnit: WAYBILL_QUANTITY_UNIT,
|
||
rows: this.transportCargoRows || [],
|
||
},
|
||
];
|
||
},
|
||
taskFullFreightTotal() {
|
||
const total = this.taskFreightGroups.reduce(
|
||
(sum, group) => sum + Number(this.taskFullFreightGroupAmount(group) || 0),
|
||
0
|
||
);
|
||
return this.formatTaskFullFreightNumber(total);
|
||
},
|
||
taskFreightCurrencyLabel() {
|
||
const currency = this.taskFreightCurrency;
|
||
if (!currency) return '';
|
||
return this.currencyRemark(currency);
|
||
},
|
||
isAdmin() {
|
||
const authority = this.userInfo && this.userInfo.authority;
|
||
return Array.isArray(authority)
|
||
? authority.includes('admin')
|
||
: String(authority || '').includes('admin');
|
||
},
|
||
canViewAllDept() {
|
||
return this.isAdmin;
|
||
},
|
||
readonlyScope() {
|
||
return (
|
||
this.config.enableAllDept && this.config.allDeptReadonly !== false && this.allDept === 1
|
||
);
|
||
},
|
||
canCreate() {
|
||
return this.hasPermission(`${this.config.permission}_add`) && !this.readonlyScope;
|
||
},
|
||
cargoTypeCascaderProps() {
|
||
return {
|
||
label: 'cargoName',
|
||
value: 'id',
|
||
children: 'children',
|
||
disabled: (data, node) => node.level === 1,
|
||
leaf: 'leaf',
|
||
checkStrictly: true,
|
||
emitPath: true,
|
||
};
|
||
},
|
||
transportCargoTypeOptions() {
|
||
return this.cargoTypeOptions.filter(item => item.children?.length);
|
||
},
|
||
transportCargoTypeCascaderProps() {
|
||
return {
|
||
label: 'cargoName',
|
||
value: 'id',
|
||
children: 'children',
|
||
emitPath: true,
|
||
};
|
||
},
|
||
canBatchComplete() {
|
||
return (
|
||
this.hasAction('batchComplete') &&
|
||
this.hasPermission(`${this.config.permission}_batch_complete`) &&
|
||
!this.readonlyScope
|
||
);
|
||
},
|
||
canRoadLoading() {
|
||
const permission =
|
||
this.config.roadLoadingPermission || `${this.config.permission}_road_loading`;
|
||
return this.config.roadLoading && this.hasPermission(permission) && !this.readonlyScope;
|
||
},
|
||
ids() {
|
||
return this.selectionList.map(item => item.id).join(',');
|
||
},
|
||
waybillFooterFreightTotal() {
|
||
const freightAmount = this.isTaskFullMode
|
||
? Number(this.taskFullFreightTotal || 0)
|
||
: Number(this.taskFreightAmount || 0);
|
||
return freightAmount + Number(this.form.otherFeeTotal || 0);
|
||
},
|
||
footerFreightTotal() {
|
||
return this.waybillFooterFreightTotal;
|
||
},
|
||
formatFooterFreightTotal() {
|
||
return this.footerFreightTotal.toLocaleString('zh-CN', {
|
||
minimumFractionDigits: 2,
|
||
maximumFractionDigits: 2,
|
||
});
|
||
},
|
||
transportAddressSelectDisabled() {
|
||
return this.dialogReadonly || this.isBillingFieldEmpty(this.form.transportType);
|
||
},
|
||
transportMode() {
|
||
return this.resolveTransportMode(this.form.transportType);
|
||
},
|
||
hasTransportType() {
|
||
return !this.isBillingFieldEmpty(this.form.transportType);
|
||
},
|
||
transportStationMode() {
|
||
return ['water', 'rail', 'air'].includes(this.transportMode);
|
||
},
|
||
transportStationTypeLabel() {
|
||
const labelMap = {
|
||
water: '港口/码头',
|
||
rail: '铁路车站',
|
||
air: '空港机场',
|
||
};
|
||
return labelMap[this.transportMode] || '';
|
||
},
|
||
transportStationNamePlaceholder() {
|
||
if (!this.transportStationMode) return '行政区划自动带出';
|
||
const labelMap = {
|
||
water: '请选择港口/码头',
|
||
rail: '请选择车站',
|
||
air: '请选择空港',
|
||
};
|
||
return labelMap[this.transportMode] || '请选择站点';
|
||
},
|
||
transportAddressDetailPlaceholder() {
|
||
return this.transportStationMode ? '选择站点后带出详细地址' : '请输入';
|
||
},
|
||
},
|
||
watch: {
|
||
$route(to, from) {
|
||
if (!this.isStandaloneWaybillFormPage) return;
|
||
if (
|
||
to.query.mode !== from?.query?.mode ||
|
||
String(to.query.id || '') !== String(from?.query?.id || '')
|
||
) {
|
||
this.$nextTick(() => this.initStandaloneFormPage());
|
||
}
|
||
},
|
||
detailId: {
|
||
immediate: true,
|
||
handler(id) {
|
||
// 宿主 view 传的是全局 $route.query.id / detailId,实例被缓存后仍会被重渲染,
|
||
// 必须确认当前路由还是自己的:否则跳到别的详情页时会把「别的单据 id」灌进来,
|
||
// 触发 openDetail 用错 id 查运单 → 报「运单管理不存在」。
|
||
if (!this.isOwnedRouteActive) return;
|
||
if (id !== undefined && id !== null && id !== '') {
|
||
this.$nextTick(() => this.openDetail({ id }));
|
||
}
|
||
},
|
||
},
|
||
waybillProcessDetailTab(tab) {
|
||
if (tab === 'batchSupplement') this.loadWaybillVoucherImages();
|
||
if (tab === 'punch' || tab === 'driverUpload') this.loadWaybillPunchRecords();
|
||
},
|
||
'form.transportType'(value, oldValue) {
|
||
if (!this.shippingInfoFormEnabled || this.suppressTransportTypeClear) return;
|
||
if (String(value ?? '') === String(oldValue ?? '')) return;
|
||
this.handleTransportTypeChange(value);
|
||
},
|
||
},
|
||
methods: {
|
||
// 页面被 keep-alive 缓存/卸载时调用,关闭本页所有 append-to-body 的弹窗,
|
||
// 避免 Teleport 出去的 DOM 残留在 body 上盖住后续页面(详见 data 里的说明)。
|
||
closeInnerDialogs() {
|
||
this.detailBox = false;
|
||
this.mileageDialog.visible = false;
|
||
this.attachmentDocumentPreviewVisible = false;
|
||
this.attachmentImagePreviewVisible = false;
|
||
this.waybillRouteChangeBox = false;
|
||
this.waybillCommonAddressBox = false;
|
||
this.flowBox = false;
|
||
this.transportRouteBox = false;
|
||
this.transportAddressBox = false;
|
||
this.transportStationBox = false;
|
||
this.transportMapBox = false;
|
||
this.commonCargoBox = false;
|
||
this.cargoImportBox = false;
|
||
this.waybillProcessConfigBox = false;
|
||
this.excelBox = false;
|
||
},
|
||
initStandaloneFormPage() {
|
||
if (!this.isStandaloneWaybillFormPage) return;
|
||
const mode = this.$route.query.mode === 'edit' ? 'edit' : 'add';
|
||
const id = this.$route.query.id || '';
|
||
const copyId = this.$route.query.copyId || '';
|
||
const key = `${mode}:${id}:${copyId}`;
|
||
if (this.standaloneFormKey === key) return;
|
||
this.standaloneFormKey = key;
|
||
if (mode === 'edit') {
|
||
this.form = { id };
|
||
} else if (mode === 'add' && copyId) {
|
||
// 复制模式:加载原数据
|
||
this.form = { id: copyId };
|
||
}
|
||
this.beforeOpen(() => {}, mode);
|
||
},
|
||
buildFormGroupOption(option) {
|
||
const sectionTitleProps = [
|
||
'basicInfoTitle',
|
||
'shippingInfoTitle',
|
||
'taskInfoTitle',
|
||
'attachmentTitle',
|
||
];
|
||
const columns = [...(option.column || [])].sort((a, b) => (b.order || 0) - (a.order || 0));
|
||
const groups = [];
|
||
let current = [];
|
||
columns.forEach(col => {
|
||
if (sectionTitleProps.includes(col.prop)) {
|
||
if (current.length) groups.push(current);
|
||
current = [col];
|
||
} else {
|
||
current.push(col);
|
||
}
|
||
});
|
||
if (current.length) groups.push(current);
|
||
if (groups.length <= 1) return option;
|
||
|
||
const [mainColumn, ...groupColumns] = groups;
|
||
return {
|
||
...option,
|
||
column: mainColumn,
|
||
group: groupColumns.map((columns, index) => {
|
||
const title = columns.find(column => sectionTitleProps.includes(column.prop));
|
||
return {
|
||
prop: title?.prop || `waybillManageGroup${index + 1}`,
|
||
label: '',
|
||
arrow: false,
|
||
column: title ? columns.filter(column => column.prop !== title.prop) : columns,
|
||
};
|
||
}),
|
||
};
|
||
},
|
||
openBusinessForm(mode, row = {}) {
|
||
if (this.isStandaloneWaybillPage) {
|
||
this.$router.push({
|
||
path: standaloneWaybillFormRoute,
|
||
query: {
|
||
mode,
|
||
...(mode === 'edit' && row.id ? { id: row.id } : {}),
|
||
name: `${mode === 'edit' ? '编辑' : '新增'}${this.config.title || ''}`,
|
||
},
|
||
});
|
||
return;
|
||
}
|
||
this.$refs.crud?.[mode === 'edit' ? 'rowEdit' : 'rowAdd'](row);
|
||
},
|
||
cloneOption(option, menuButtonCount) {
|
||
const nextOption = {
|
||
...option,
|
||
column: (option.column || []).map(column => ({ ...column })),
|
||
};
|
||
const tableOrder = this.config.tableColumnOrder || [];
|
||
if (tableOrder.length) {
|
||
const orderMap = new Map(tableOrder.map((prop, index) => [prop, index]));
|
||
nextOption.column.sort((left, right) => {
|
||
const leftOrder = orderMap.has(left.prop) ? orderMap.get(left.prop) : tableOrder.length;
|
||
const rightOrder = orderMap.has(right.prop)
|
||
? orderMap.get(right.prop)
|
||
: tableOrder.length;
|
||
return leftOrder - rightOrder;
|
||
});
|
||
nextOption.column.forEach(column => {
|
||
if (column.prop) column.hide = !orderMap.has(column.prop);
|
||
});
|
||
}
|
||
return applyTableMenuWidth(nextOption, menuButtonCount);
|
||
},
|
||
formatWaybillGoodsInfo(row = {}) {
|
||
// 列表接口的货物明细字段存在多种兼容命名,优先使用完整的 goodsJson。
|
||
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 this.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.parseQuantity(
|
||
item.quantity ?? item.cargoQuantity ?? item.goodsQuantity
|
||
);
|
||
return result;
|
||
}, {});
|
||
return Object.values(groups)
|
||
.map(group => {
|
||
const quantity = this.formatQuantity(group.quantity);
|
||
return `${group.typeName}等${group.count}种货物 | ${quantity}${
|
||
group.unit ? ` ${group.unit}` : ''
|
||
}`;
|
||
})
|
||
.join('; ');
|
||
},
|
||
formatWaybillProvinceCityDistrict(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;
|
||
},
|
||
formatWaybillRouteCityDistrict(value) {
|
||
const text = String(value || '').trim();
|
||
if (!text) return '-';
|
||
const provinceMatch = text.match(/^.+?(?:省|自治区|特别行政区)/);
|
||
const cityRegion = provinceMatch ? text.slice(provinceMatch[0].length) : text;
|
||
const cityMatch = cityRegion.match(/^(.+?市)/);
|
||
if (!cityMatch) return this.formatWaybillProvinceCityDistrict(text);
|
||
const city = cityMatch[1];
|
||
const remainder = cityRegion.slice(city.length);
|
||
const districtMatch = remainder.match(
|
||
/^(.+?(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗){1,2})/
|
||
);
|
||
const district = districtMatch ? districtMatch[1] : '';
|
||
return [city, district].filter(Boolean).join(' ');
|
||
},
|
||
waybillDetailRouteName(row = {}, field) {
|
||
const address = String(row[`${field}Address`] || '').trim();
|
||
const name = String(row[`${field}Name`] || '').trim();
|
||
if (!address) return name || '-';
|
||
if (!name || address.includes(name)) {
|
||
return name || this.formatWaybillProvinceCityDistrict(address);
|
||
}
|
||
return this.formatWaybillProvinceCityDistrict(address);
|
||
},
|
||
formatWaybillListAddress(value) {
|
||
const text = String(value || '')
|
||
.trim()
|
||
.replace(/[\\/||、,,\s]+/g, '');
|
||
if (!text) return '-';
|
||
const districtPattern = '(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗)';
|
||
const parts = [];
|
||
let remainder = text;
|
||
const province = text.match(/^(.+?(?:省|自治区|特别行政区))/);
|
||
if (province) {
|
||
parts.push(province[1]);
|
||
remainder = text.slice(province[1].length);
|
||
}
|
||
const city = remainder.match(/^(.+?市)/);
|
||
if (city) {
|
||
parts.push(city[1]);
|
||
remainder = remainder.slice(city[1].length);
|
||
}
|
||
const district = remainder.match(new RegExp(`^(.+?${districtPattern})`));
|
||
if (district) parts.push(district[1]);
|
||
return parts.length ? parts.join('/') : text;
|
||
},
|
||
hasPermission(code) {
|
||
return this.isAdmin || this.validData(this.permission && this.permission[code], false);
|
||
},
|
||
hasAction(action) {
|
||
return (this.config.actions || []).includes(action);
|
||
},
|
||
statusValue(row) {
|
||
return row[this.config.statusProp || 'status'];
|
||
},
|
||
inStatus(row, statusList) {
|
||
if (!statusList || !statusList.length) {
|
||
return true;
|
||
}
|
||
const status = this.statusValue(row);
|
||
const statusText = this.config.statusTextProp ? row[this.config.statusTextProp] : '';
|
||
return statusList.includes(status) || statusList.includes(statusText);
|
||
},
|
||
isReadonlyEditAllowed(row) {
|
||
return this.inStatus(row, this.config.ignoreReadonlyEditStatus);
|
||
},
|
||
isReadonlyDeleteAllowed(row) {
|
||
return this.inStatus(row, this.config.ignoreReadonlyDeleteStatus);
|
||
},
|
||
canEdit(row) {
|
||
return (
|
||
this.hasPermission(`${this.config.permission}_edit`) &&
|
||
!this.readonlyScope &&
|
||
(!row.readonly || this.isReadonlyEditAllowed(row)) &&
|
||
this.inStatus(row, this.config.editStatus) &&
|
||
(typeof this.config.canEdit !== 'function' || this.config.canEdit(row))
|
||
);
|
||
},
|
||
canDelete(row) {
|
||
return (
|
||
this.hasPermission(`${this.config.permission}_delete`) &&
|
||
!this.readonlyScope &&
|
||
(!row.readonly || this.isReadonlyDeleteAllowed(row)) &&
|
||
this.inStatus(row, this.config.deleteStatus)
|
||
);
|
||
},
|
||
canCopy(row) {
|
||
return (
|
||
this.hasAction('copy') &&
|
||
this.hasPermission(`${this.config.permission}_copy`) &&
|
||
!this.readonlyScope &&
|
||
!row.readonly
|
||
);
|
||
},
|
||
canEnable(row) {
|
||
return (
|
||
this.hasAction('enable') &&
|
||
this.hasPermission(`${this.config.permission}_enable`) &&
|
||
!this.readonlyScope &&
|
||
!row.readonly &&
|
||
[0, 2].includes(row.status)
|
||
);
|
||
},
|
||
canDisable(row) {
|
||
return (
|
||
this.hasAction('disable') &&
|
||
this.hasPermission(`${this.config.permission}_disable`) &&
|
||
!this.readonlyScope &&
|
||
!row.readonly &&
|
||
row.status === 1
|
||
);
|
||
},
|
||
canCancel(row) {
|
||
if (
|
||
!this.hasAction('cancel') ||
|
||
!this.hasPermission(`${this.config.permission}_cancel`) ||
|
||
this.readonlyScope ||
|
||
row.readonly
|
||
) {
|
||
return false;
|
||
}
|
||
const status = this.statusValue(row);
|
||
return ['pending', 'processing', 'running'].includes(status);
|
||
},
|
||
canComplete(row) {
|
||
if (
|
||
!this.hasAction('complete') ||
|
||
!this.hasPermission(`${this.config.permission}_complete`) ||
|
||
this.readonlyScope ||
|
||
row.readonly
|
||
) {
|
||
return false;
|
||
}
|
||
const status = this.statusValue(row);
|
||
return status === 'processing' || status === 'running';
|
||
},
|
||
canMaintainMileage(row) {
|
||
return (
|
||
this.config.permission === 'waybill_manage' &&
|
||
this.hasPermission(`${this.config.permission}_mileage`) &&
|
||
typeof this.api.maintainMileage === 'function' &&
|
||
this.statusValue(row) === 'completed' &&
|
||
row.mileageMaintainable === true
|
||
);
|
||
},
|
||
canReassign(row) {
|
||
return (
|
||
this.hasAction('reassign') &&
|
||
this.hasPermission(`${this.config.permission}_reassign`) &&
|
||
!this.readonlyScope &&
|
||
!row.readonly &&
|
||
this.statusValue(row) === 'pending' &&
|
||
(typeof this.config.canReassign !== 'function' || this.config.canReassign(row))
|
||
);
|
||
},
|
||
customOperations(row) {
|
||
return (this.config.operations || []).filter(operation => this.canOperation(operation, row));
|
||
},
|
||
consumeTemplateCreatePayload() {
|
||
const target = this.config.templateCreateTarget;
|
||
if (!target) return;
|
||
const storageKey = 'business-template-create';
|
||
const raw = sessionStorage.getItem(storageKey);
|
||
if (!raw) return;
|
||
let payload;
|
||
try {
|
||
payload = JSON.parse(raw);
|
||
} catch (error) {
|
||
sessionStorage.removeItem(storageKey);
|
||
return;
|
||
}
|
||
if (payload.target !== target) return;
|
||
sessionStorage.removeItem(storageKey);
|
||
this.$nextTick(() => {
|
||
// 独立表单页已经由路由控制新增状态,不能再调用 rowAdd 打开弹窗。
|
||
if (!this.isStandaloneWaybillFormPage) this.$refs.crud?.rowAdd?.();
|
||
this.$nextTick(() => this.applyTemplateCreatePayload(payload.data));
|
||
});
|
||
},
|
||
applyTemplateCreatePayload(data) {
|
||
// 模板主键不能带入目标业务单据,避免提交时被后端误判为更新。
|
||
const row = { ...(data || {}) };
|
||
[
|
||
'id',
|
||
'createUser',
|
||
'createUserName',
|
||
'createDept',
|
||
'createTime',
|
||
'updateUser',
|
||
'updateUserName',
|
||
'updateTime',
|
||
].forEach(key => delete row[key]);
|
||
this.suppressTransportTypeClear = true;
|
||
Object.assign(this.form, row);
|
||
this.form.dataSource = '手工创建';
|
||
this.applyWaybillTemplateTaskData(row);
|
||
this.$nextTick(() => {
|
||
this.suppressTransportTypeClear = false;
|
||
});
|
||
this.form.planName = this.form.planName || this.form.templateName || '';
|
||
this.selectedProjectId = this.form.projectId || '';
|
||
this.fillCustomerNameFromProject();
|
||
this.attachmentRows = this.parseJsonArray(this.form.attachmentsJson);
|
||
this.selectedAttachmentRows = [];
|
||
if (this.contractSelectEnabled) {
|
||
this.syncCurrentContractOption();
|
||
this.loadContractOptionsForProject();
|
||
this.syncTaskFreightCurrencyFromContract();
|
||
}
|
||
if (this.config.enableProjectSelect) this.syncCurrentProjectOption();
|
||
this.loadProjectProcessConfigState();
|
||
this.initWaybillCargoRows();
|
||
if (this.taskInfoFormEnabled) this.initTaskInfoForm();
|
||
},
|
||
fillCustomerNameFromProject(projectId = this.form.projectId) {
|
||
if (this.form.customerName || !projectId) return Promise.resolve(this.form.customerName);
|
||
const project = this.projectOptions.find(item => String(item.id) === String(projectId));
|
||
const customerName =
|
||
project?.customerNames || project?.customerName || project?.customer || '';
|
||
if (customerName) {
|
||
this.form.customerName = customerName;
|
||
return Promise.resolve(customerName);
|
||
}
|
||
return getProjectDetail(projectId)
|
||
.then(res => {
|
||
const detail = res?.data?.data || res?.data || {};
|
||
const name = detail.customerNames || detail.customerName || detail.customer || '';
|
||
if (!this.form.customerName && name) this.form.customerName = name;
|
||
return name;
|
||
})
|
||
.catch(() => '');
|
||
},
|
||
applyWaybillTemplateTaskData(template = {}) {
|
||
if (this.config.permission !== 'waybill_manage') return;
|
||
const goodsRows = this.parseJsonArray(template.goodsJson);
|
||
if (!goodsRows.length) return;
|
||
const freight = this.parseJsonObject(template.freightJson);
|
||
const freightItems = Array.isArray(freight.freightItems) ? freight.freightItems : [];
|
||
const enrichedGoodsRows = goodsRows.map((goods, index) => {
|
||
const freightItem =
|
||
freightItems[index] ||
|
||
freightItems.find(
|
||
item => String(item.quantityUnit || '') === String(goods.quantityUnit || '')
|
||
) ||
|
||
{};
|
||
return {
|
||
...goods,
|
||
unitPrice: freightItem.unitPrice ?? goods.unitPrice ?? '',
|
||
priceUnit: freightItem.priceUnit || goods.priceUnit || '',
|
||
};
|
||
});
|
||
this.form.goodsJson = JSON.stringify(enrichedGoodsRows);
|
||
this.taskFreightCurrency = freight.currency || this.taskFreightCurrency || 'RMB';
|
||
this.syncTaskFreightCurrencyFromContract();
|
||
this.form.otherFeeTotal =
|
||
freight.otherFreightAmount ?? freight.otherFeeTotal ?? this.form.otherFeeTotal ?? '';
|
||
|
||
if (enrichedGoodsRows.length > 1) {
|
||
this.form.taskEntryMode = 'full';
|
||
return;
|
||
}
|
||
|
||
const [goods] = enrichedGoodsRows;
|
||
this.form.taskEntryMode = 'simple';
|
||
Object.assign(this.form, {
|
||
cargoType: goods.cargoType || goods.goodsType || '',
|
||
cargoTypeCode: goods.cargoTypeCode || goods.goodsTypeCode || '',
|
||
cargoName: goods.cargoName || goods.goodsName || '',
|
||
specification: goods.specification || goods.spec || '',
|
||
model: goods.model || goods.modelName || '',
|
||
quantity: goods.quantity ?? goods.cargoQuantity ?? goods.goodsQuantity ?? '',
|
||
quantityUnit: goods.quantityUnit || goods.goodsQuantityUnit || goods.unit || '吨',
|
||
unitPrice: goods.unitPrice,
|
||
priceUnit: goods.priceUnit || '元/吨',
|
||
});
|
||
},
|
||
canOperation(operation, row) {
|
||
const permission = operation.permission || `${this.config.permission}_${operation.action}`;
|
||
if (!this.hasPermission(permission) || this.readonlyScope || row.readonly) {
|
||
return false;
|
||
}
|
||
const prop = operation.statusProp || this.config.statusProp || 'status';
|
||
const statusList = operation.status || [];
|
||
if (statusList.length && !statusList.includes(row[prop])) {
|
||
return false;
|
||
}
|
||
return true;
|
||
},
|
||
showDetailButton(row) {
|
||
if (!this.config.detailButton || !this.hasPermission(`${this.config.permission}_view`))
|
||
return false;
|
||
const status = this.statusValue(row);
|
||
return status !== 'draft';
|
||
},
|
||
formatDetailValue(row, prop) {
|
||
if (!row) return '-';
|
||
if (prop === 'businessStatus') return this.displayStatus(row, prop);
|
||
if (prop === 'feeGenerationMode') {
|
||
return row[prop] === 'manual' ? '账单导入生成' : '系统生成';
|
||
}
|
||
const column = this.findColumn(this.option.column, prop);
|
||
if (column?.formatter) return column.formatter(row, {}, row[prop]);
|
||
if (column?.dicData) {
|
||
const item = column.dicData.find(dic => String(dic.value) === String(row[prop]));
|
||
if (item) return item.label;
|
||
}
|
||
const value = row[prop];
|
||
if (value && typeof value === 'object') return JSON.stringify(value);
|
||
if (typeof value === 'string' && /^[\[{]/.test(value.trim())) {
|
||
try {
|
||
const parsed = JSON.parse(value);
|
||
return typeof parsed === 'object' ? JSON.stringify(parsed) : parsed;
|
||
} catch (error) {
|
||
// 非 JSON 文本按原值展示。
|
||
}
|
||
}
|
||
return value === undefined || value === null || value === '' ? '-' : value;
|
||
},
|
||
waybillTransportMode(row = {}) {
|
||
return (
|
||
row.transportModeName ||
|
||
row.transportMode ||
|
||
row.transportTypeName ||
|
||
row.transportType ||
|
||
''
|
||
);
|
||
},
|
||
waybillIsRoadTransport(row = {}) {
|
||
return this.waybillTransportModeCode(row) === 'road';
|
||
},
|
||
waybillTransportModeCode(row = {}) {
|
||
const text = [
|
||
row.transportType,
|
||
row.transportTypeName,
|
||
row.transportMode,
|
||
row.transportModeName,
|
||
this.getTransportTypeLabel(row.transportType),
|
||
]
|
||
.filter(Boolean)
|
||
.join(' ')
|
||
.toLowerCase();
|
||
if (/公路|道路|汽车|陆运|road|highway/.test(text) || text === 'gl') return 'road';
|
||
if (/水路|水运|海运|港|water|ship|sea/.test(text) || text === 'sl') return 'water';
|
||
if (/航空|空运|空港|air|airport/.test(text) || text === 'hk') return 'air';
|
||
if (/铁路|rail|train/.test(text) || text === 'tl') return 'rail';
|
||
return '';
|
||
},
|
||
waybillIsNonRoadTransport(row = {}) {
|
||
return ['water', 'rail', 'air'].includes(this.waybillTransportModeCode(row));
|
||
},
|
||
waybillIsCarrier(row = {}) {
|
||
return String(row.carrierTypeName || row.carrierType || '').trim() === '承运商';
|
||
},
|
||
waybillVehicleNoLabel(row = {}) {
|
||
return this.waybillIsRoadTransport(row) ? '车牌号' : '船/航/班列号';
|
||
},
|
||
waybillUnitPrice(row = {}) {
|
||
const price = row.unitPrice || row.price || '';
|
||
const unit = row.priceUnit || row.billingUnit || '';
|
||
if (price === '' || Number(price) === -1) return '';
|
||
return unit ? `${price}(${unit})` : price;
|
||
},
|
||
waybillCurrencyRemark(row = {}) {
|
||
const freight = this.parseJsonObject(row.freightJson);
|
||
const value = row.currency || row.freightCurrency || freight.currency || '';
|
||
if (!value) return '';
|
||
const normalized = String(value).toUpperCase();
|
||
const option = this.freightCurrencyOptions.find(item => {
|
||
const itemValue = String(item.value || '').toUpperCase();
|
||
return itemValue === normalized || (normalized === 'CNY' && itemValue === 'RMB');
|
||
});
|
||
const remark = String(option?.remark || '').trim();
|
||
if ((normalized === 'CNY' || normalized === 'RMB') && remark === '元') return '¥';
|
||
return remark;
|
||
},
|
||
waybillUnitPriceWithCurrency(row = {}) {
|
||
const text = this.waybillUnitPrice(row);
|
||
if (!text) return { value: '-', unit: '' };
|
||
const match = String(text).match(/^(.*)\((.*)\)$/);
|
||
const price = match ? match[1] : text;
|
||
const unit = match ? match[2] : '';
|
||
return {
|
||
value: `${this.waybillCurrencyRemark(row)}${price}`,
|
||
unit,
|
||
};
|
||
},
|
||
waybillAmountWithCurrency(row = {}, prop) {
|
||
const value = this.formatDetailValue(row, prop);
|
||
if (value === '-') return { value: '-' };
|
||
const number = Number(value);
|
||
const display = Number.isFinite(number) ? number.toFixed(2) : value;
|
||
return { value: `${this.waybillCurrencyRemark(row)}${display}` };
|
||
},
|
||
openDetail(row) {
|
||
if (!this.isStandaloneWaybillDetailPage) {
|
||
// 已不在自己的路由上(被缓存后路由切走):此时 row.id 极可能是别的单据的 id,
|
||
// 再 push 会生成一堆错误的「运单管理详情」标签
|
||
if (!this.isOwnedRouteActive) return;
|
||
this.$router.push({ path: standaloneWaybillDetailRoute, query: { id: row.id } });
|
||
return;
|
||
}
|
||
this.detailBox = true;
|
||
this.detailLoading = true;
|
||
this.detailRow = { ...row };
|
||
this.waybillProcessDetailTab = 'punch';
|
||
this.waybillHasRelatedVoucher = false;
|
||
this.waybillDetailProcessNodes = [];
|
||
this.waybillPunchRecords = [];
|
||
this.waybillDriverUploads = [];
|
||
this.waybillVoucherImages = [];
|
||
this.waybillVoucherFolders = [];
|
||
this.waybillVoucherFolder = null;
|
||
this.waybillVoucherFolderView = false;
|
||
this.waybillVoucherImagesLoaded = false;
|
||
const request =
|
||
typeof this.api.getDetail === 'function'
|
||
? this.api.getDetail(row.id)
|
||
: Promise.resolve({ data: { data: row } });
|
||
request
|
||
.then(res => {
|
||
const detail = res?.data?.data || res?.data || row;
|
||
this.detailRow = detail;
|
||
this.loadWaybillPunchRecords();
|
||
this.loadWaybillVoucherImages();
|
||
if (detail.contractId) {
|
||
return getContractDetail(detail.contractId)
|
||
.then(contractRes => {
|
||
const contract = contractRes?.data?.data || contractRes?.data || {};
|
||
this.detailRow = {
|
||
...detail,
|
||
contractNo: contract.contractNo || detail.contractNo || detail.contractName || '',
|
||
customerName:
|
||
contract.partyA || contract.customerName || detail.customerName || '',
|
||
};
|
||
this.applyFormDetail(this.detailRow);
|
||
return this.loadWaybillDetailProcessNodes(this.detailRow.projectId);
|
||
})
|
||
.catch(() => {
|
||
this.detailRow = {
|
||
...detail,
|
||
contractNo: detail.contractNo || detail.contractName || '',
|
||
};
|
||
this.applyFormDetail(this.detailRow);
|
||
return this.loadWaybillDetailProcessNodes(this.detailRow.projectId);
|
||
});
|
||
}
|
||
this.applyFormDetail(detail);
|
||
return this.loadWaybillDetailProcessNodes(detail.projectId);
|
||
})
|
||
.finally(() => {
|
||
this.detailLoading = false;
|
||
});
|
||
},
|
||
closeDetail() {
|
||
this.destroyWaybillLocateMap();
|
||
this.destroyWaybillTrackMap();
|
||
this.waybillTrackMode = '';
|
||
this.waybillLocateInfo = null;
|
||
this.waybillLocateHint = '';
|
||
this.waybillTrackInfo = null;
|
||
this.waybillTrackHint = '';
|
||
if (this.isStandaloneWaybillDetailPage) {
|
||
this.$router.$avueRouter?.closeTag?.();
|
||
this.$router.push({ path: '/business/waybill-manage', query: {} });
|
||
return;
|
||
}
|
||
this.detailBox = false;
|
||
},
|
||
async openLoadingDetail(row = {}) {
|
||
let id = row.loadingId || row.loadingManageId || '';
|
||
if (!id && row.loadingNo) {
|
||
try {
|
||
const res = await getLoadingList(1, 1, { loadingNo: row.loadingNo });
|
||
const data = res?.data?.data || res?.data || res || {};
|
||
const records = data.records || data.rows || (Array.isArray(data) ? data : []);
|
||
id = records[0]?.id || '';
|
||
} catch (error) {
|
||
id = '';
|
||
}
|
||
}
|
||
if (!id) {
|
||
this.$message.info('当前配载单暂无详情数据');
|
||
return;
|
||
}
|
||
this.$router.push({
|
||
path: '/business/loading-manage/detail',
|
||
query: { id, name: '配载单详情' },
|
||
});
|
||
},
|
||
loadWaybillDetailProcessNodes(projectId) {
|
||
this.waybillDetailProcessNodes = [];
|
||
if (!projectId) return Promise.resolve([]);
|
||
return getProcessConfigList(1, 100, { projectIds: projectId, status: 1 })
|
||
.then(res => {
|
||
const config = extractRecords(res).find(row => {
|
||
const projectIds = String(row.projectIds || '')
|
||
.split(',')
|
||
.map(item => item.trim());
|
||
return projectIds.includes(String(projectId));
|
||
});
|
||
if (!config?.id) return [];
|
||
return getProcessConfigDetail(config.id, this.detailRow.id).then(detailRes => {
|
||
const detail = detailRes?.data?.data || detailRes?.data || config;
|
||
this.waybillHasRelatedVoucher =
|
||
this.waybillHasRelatedVoucher || Boolean(detail.hasRelatedVoucher);
|
||
return this.getProcessConfigEnabledNodes(detail);
|
||
});
|
||
})
|
||
.then(nodes => {
|
||
if (String(this.detailRow.projectId) === String(projectId)) {
|
||
this.waybillDetailProcessNodes = nodes;
|
||
}
|
||
return nodes;
|
||
})
|
||
.catch(() => []);
|
||
},
|
||
// 解析打卡时间(支持常见日期字符串格式),未打卡或无有效时间返回 null
|
||
waybillPunchRecordTimestamp(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 = this.$dayjs ? this.$dayjs(text) : null;
|
||
if (parsed && typeof parsed.isValid === 'function' && parsed.isValid()) {
|
||
return parsed.valueOf();
|
||
}
|
||
const fallback = new Date(text.replace(/-/g, '/')).getTime();
|
||
return Number.isNaN(fallback) ? null : fallback;
|
||
},
|
||
loadWaybillPunchRecords() {
|
||
const waybillId = this.detailRow?.id;
|
||
if (!waybillId || this.waybillPunchRecordsLoading) return;
|
||
this.waybillPunchRecordsLoading = true;
|
||
const request =
|
||
typeof this.api.getPunchRecords === 'function'
|
||
? this.api.getPunchRecords(waybillId)
|
||
: getWaybillPunchRecords(waybillId);
|
||
request
|
||
.then(res => {
|
||
const data = res?.data?.data || res?.data || {};
|
||
this.waybillPunchRecords = Array.isArray(data.records) ? data.records : [];
|
||
this.waybillDriverUploads = Array.isArray(data.driverUploads)
|
||
? data.driverUploads
|
||
: [];
|
||
})
|
||
.catch(() => {
|
||
this.waybillPunchRecords = [];
|
||
this.waybillDriverUploads = [];
|
||
})
|
||
.finally(() => {
|
||
this.waybillPunchRecordsLoading = 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;
|
||
},
|
||
loadWaybillVoucherImages() {
|
||
if (
|
||
!this.detailRow.id ||
|
||
this.waybillVoucherImagesLoading ||
|
||
this.waybillVoucherImagesLoaded
|
||
) {
|
||
return;
|
||
}
|
||
this.waybillVoucherImagesLoading = true;
|
||
getProcessConfigVoucherImages(this.detailRow.id)
|
||
.then(res => {
|
||
this.waybillVoucherImages = extractRecords(res);
|
||
this.waybillVoucherFolders = this.waybillVoucherImages.filter(item =>
|
||
this.isWaybillVoucherFolder(item)
|
||
);
|
||
this.waybillVoucherFolder = null;
|
||
this.waybillVoucherFolderView = false;
|
||
if (this.waybillVoucherImages.length) this.waybillHasRelatedVoucher = true;
|
||
this.waybillVoucherImagesLoaded = true;
|
||
})
|
||
.finally(() => {
|
||
this.waybillVoucherImagesLoading = false;
|
||
});
|
||
},
|
||
isWaybillVoucherFolder(item) {
|
||
return Boolean(item && (item.isFolder === true || item.type === 'folder'));
|
||
},
|
||
waybillVoucherFolderName(folder) {
|
||
return folder?.folderName || folder?.name || folder?.plateNo || '凭证文件夹';
|
||
},
|
||
openWaybillVoucherFolder(folder) {
|
||
if (!this.detailRow.id || !this.isWaybillVoucherFolder(folder)) return;
|
||
if (!folder.voucherId || !folder.folderName) {
|
||
this.$message.warning('凭证文件夹信息不完整');
|
||
return;
|
||
}
|
||
this.waybillVoucherImagesLoading = true;
|
||
getProcessConfigVoucherImages(this.detailRow.id, {
|
||
voucherId: folder.voucherId,
|
||
folderName: folder.folderName,
|
||
})
|
||
.then(res => {
|
||
this.waybillVoucherFolder = folder;
|
||
this.waybillVoucherImages = extractRecords(res).filter(
|
||
item => !this.isWaybillVoucherFolder(item)
|
||
);
|
||
this.waybillVoucherFolderView = true;
|
||
})
|
||
.finally(() => {
|
||
this.waybillVoucherImagesLoading = false;
|
||
});
|
||
},
|
||
backToWaybillVoucherFolders() {
|
||
this.waybillVoucherImages = [...this.waybillVoucherFolders];
|
||
this.waybillVoucherFolder = null;
|
||
this.waybillVoucherFolderView = false;
|
||
},
|
||
previewWaybillVoucherImage(index) {
|
||
const imageRows = this.waybillVoucherImages.filter(
|
||
image => !this.isWaybillVoucherFolder(image)
|
||
);
|
||
const currentImage = this.waybillVoucherImages[index];
|
||
const imageIndex = imageRows.findIndex(image => image === currentImage);
|
||
this.attachmentImagePreviewUrls = imageRows
|
||
.map(image => image.url)
|
||
.filter(Boolean);
|
||
this.attachmentImagePreviewIndex = imageIndex >= 0 ? imageIndex : 0;
|
||
this.attachmentImagePreviewVisible = this.attachmentImagePreviewUrls.length > 0;
|
||
},
|
||
buildWaybillRouteNodes(rows = []) {
|
||
const nodes = [];
|
||
rows.forEach((row, rowIndex) => {
|
||
[
|
||
['departure', '发'],
|
||
['arrival', '收'],
|
||
].forEach(([field, tag]) => {
|
||
const address = row[`${field}Address`] || row[`${field}Name`];
|
||
if (!address) return;
|
||
nodes.push({
|
||
key: `${row.id || row.waybillNo || rowIndex}-${field}`,
|
||
address,
|
||
tags: [tag, row.waybillNo].filter(Boolean),
|
||
waybillId: row.id,
|
||
waybillNo: row.waybillNo,
|
||
field,
|
||
});
|
||
});
|
||
});
|
||
return nodes;
|
||
},
|
||
restoreWaybillRouteChangeRecords() {
|
||
const taskInfo = this.parseJsonObject(this.detailRow.taskInfoJson);
|
||
this.waybillRouteChangeRecords = Array.isArray(taskInfo.routeChangeRecords)
|
||
? [...taskInfo.routeChangeRecords].sort((a, b) =>
|
||
String(b.changeTime).localeCompare(String(a.changeTime))
|
||
)
|
||
: [];
|
||
},
|
||
openWaybillRouteChangeDialog() {
|
||
if (!this.detailRow.id) return;
|
||
this.restoreWaybillRouteChangeRecords();
|
||
this.waybillRouteChangeTab = 'change';
|
||
this.waybillRouteChangeRemark = '';
|
||
this.waybillRouteChangeRows = [
|
||
{
|
||
...this.detailRow,
|
||
originalDepartureAddress:
|
||
this.detailRow.departureAddress || this.detailRow.departureName || '',
|
||
originalArrivalAddress: this.detailRow.arrivalAddress || this.detailRow.arrivalName || '',
|
||
departureAddress: this.detailRow.departureAddress || this.detailRow.departureName || '',
|
||
arrivalAddress: this.detailRow.arrivalAddress || this.detailRow.arrivalName || '',
|
||
},
|
||
];
|
||
this.waybillRouteChangeNodes = this.buildWaybillRouteNodes(this.waybillRouteChangeRows);
|
||
this.waybillRouteChangeBox = true;
|
||
},
|
||
waybillRouteChangeTypeText(index) {
|
||
if (index === 0) return '起';
|
||
if (index === this.waybillRouteChangeNodes.length - 1) return '终';
|
||
return '经';
|
||
},
|
||
handleWaybillRouteChangeDragStart(index) {
|
||
this.waybillRouteChangeDragIndex = index;
|
||
},
|
||
handleWaybillRouteChangeDrop(index) {
|
||
if (this.waybillRouteChangeDragIndex < 0 || this.waybillRouteChangeDragIndex === index)
|
||
return;
|
||
const nodes = [...this.waybillRouteChangeNodes];
|
||
const [node] = nodes.splice(this.waybillRouteChangeDragIndex, 1);
|
||
nodes.splice(index, 0, node);
|
||
this.waybillRouteChangeNodes = nodes;
|
||
this.waybillRouteChangeDragIndex = -1;
|
||
},
|
||
updateWaybillRouteChangeNodes() {
|
||
this.waybillRouteChangeNodes = this.buildWaybillRouteNodes(this.waybillRouteChangeRows);
|
||
},
|
||
openWaybillCommonAddressDialog(row, field) {
|
||
this.waybillCommonAddressTarget = { row, field };
|
||
this.waybillCommonAddressQuery = { addressName: '' };
|
||
this.waybillCommonAddressPage.currentPage = 1;
|
||
this.waybillCommonAddressBox = true;
|
||
this.loadWaybillCommonAddresses();
|
||
},
|
||
loadWaybillCommonAddresses() {
|
||
this.waybillCommonAddressLoading = true;
|
||
getCommonAddressList(
|
||
this.waybillCommonAddressPage.currentPage,
|
||
this.waybillCommonAddressPage.pageSize,
|
||
{
|
||
...this.waybillCommonAddressQuery,
|
||
allDept: 0,
|
||
}
|
||
)
|
||
.then(res => {
|
||
const data = res.data?.data || {};
|
||
this.waybillCommonAddressRows = data.records || [];
|
||
this.waybillCommonAddressPage.total = data.total || 0;
|
||
})
|
||
.finally(() => {
|
||
this.waybillCommonAddressLoading = false;
|
||
});
|
||
},
|
||
selectWaybillCommonAddress(address) {
|
||
const target = this.waybillCommonAddressTarget;
|
||
if (!target) return;
|
||
target.row[`${target.field}Address`] =
|
||
[address.regionName, address.detailAddress || address.addressName]
|
||
.filter(Boolean)
|
||
.join('') ||
|
||
address.addressName ||
|
||
'';
|
||
this.updateWaybillRouteChangeNodes();
|
||
this.waybillCommonAddressBox = false;
|
||
},
|
||
async confirmWaybillRouteChange() {
|
||
const row = this.waybillRouteChangeRows[0] || {};
|
||
const changed =
|
||
row.departureAddress !== row.originalDepartureAddress ||
|
||
row.arrivalAddress !== row.originalArrivalAddress;
|
||
const routeJson = JSON.stringify(this.waybillRouteChangeNodes);
|
||
const originalRouteJson = this.detailRow.routeJson || '';
|
||
if (!changed && routeJson === originalRouteJson && !this.waybillRouteChangeRemark) {
|
||
this.$message.warning('请修改地址、调整路线或填写变更备注');
|
||
return;
|
||
}
|
||
if (typeof this.api.changeRoute !== 'function') {
|
||
this.$message.warning('当前接口未接入路线变更功能');
|
||
return;
|
||
}
|
||
|
||
const content = changed
|
||
? `${row.waybillNo || '运单'}:发货地 ${row.originalDepartureAddress || '-'} → ${
|
||
row.departureAddress || '-'
|
||
};到货地 ${row.originalArrivalAddress || '-'} → ${row.arrivalAddress || '-'}`
|
||
: '调整运输路线顺序';
|
||
const records = [
|
||
{
|
||
changeTime: this.$dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||
changeUser: this.userInfo?.realName || this.userInfo?.account || '当前用户',
|
||
content,
|
||
remark: this.waybillRouteChangeRemark,
|
||
},
|
||
...this.waybillRouteChangeRecords,
|
||
];
|
||
const taskInfo = this.parseJsonObject(this.detailRow.taskInfoJson);
|
||
const taskInfoJson = JSON.stringify({
|
||
...taskInfo,
|
||
routeChangeRecords: records,
|
||
});
|
||
|
||
this.waybillRouteChangeSaving = true;
|
||
try {
|
||
await this.api.changeRoute({
|
||
id: this.detailRow.id,
|
||
routeJson,
|
||
departureAddress: row.departureAddress,
|
||
arrivalAddress: row.arrivalAddress,
|
||
taskInfoJson,
|
||
});
|
||
let refreshedDetail = null;
|
||
if (typeof this.api.getDetail === 'function') {
|
||
try {
|
||
const detailRes = await this.api.getDetail(this.detailRow.id);
|
||
refreshedDetail = detailRes?.data?.data || detailRes?.data || null;
|
||
} catch (error) {
|
||
refreshedDetail = null;
|
||
}
|
||
}
|
||
this.detailRow = {
|
||
...this.detailRow,
|
||
...(refreshedDetail || {}),
|
||
departureAddress: row.departureAddress,
|
||
arrivalAddress: row.arrivalAddress,
|
||
routeJson,
|
||
taskInfoJson,
|
||
};
|
||
this.waybillRouteChangeRecords = records;
|
||
this.waybillRouteChangeBox = false;
|
||
this.$message.success('路线变更已保存');
|
||
this.onLoad(this.page, this.query);
|
||
} finally {
|
||
this.waybillRouteChangeSaving = false;
|
||
}
|
||
},
|
||
handleWaybillTrackAction(action) {
|
||
if (action === 'playback') {
|
||
this.destroyWaybillLocateMap();
|
||
this.waybillTrackMode = 'playback';
|
||
this.waybillLocateInfo = null;
|
||
this.waybillLocateHint = '';
|
||
if (!Array.isArray(this.waybillTrackDateRange) || this.waybillTrackDateRange.length !== 2) {
|
||
this.waybillTrackDateRange = this.buildDefaultTrackDateRange();
|
||
}
|
||
this.loadWaybillTrack();
|
||
return;
|
||
}
|
||
this.destroyWaybillTrackMap();
|
||
this.waybillTrackMode = 'locate';
|
||
this.waybillTrackInfo = null;
|
||
this.waybillTrackHint = '';
|
||
this.loadWaybillLocate();
|
||
},
|
||
buildDefaultTrackDateRange() {
|
||
const pad = value => String(value).padStart(2, '0');
|
||
const formatDate = date =>
|
||
`${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
||
const today = new Date();
|
||
const yesterday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1);
|
||
return [formatDate(yesterday), formatDate(today)];
|
||
},
|
||
destroyWaybillLocateMap() {
|
||
if (this.waybillLocateMarker) {
|
||
this.waybillLocateMarker.setMap(null);
|
||
this.waybillLocateMarker = null;
|
||
}
|
||
if (this.waybillLocateInfoWindow) {
|
||
this.waybillLocateInfoWindow.close();
|
||
this.waybillLocateInfoWindow = null;
|
||
}
|
||
if (this.waybillLocateMapInstance) {
|
||
this.waybillLocateMapInstance.destroy();
|
||
this.waybillLocateMapInstance = null;
|
||
}
|
||
},
|
||
destroyWaybillTrackMap() {
|
||
if (this.waybillTrackPolyline) {
|
||
this.waybillTrackPolyline.setMap(null);
|
||
this.waybillTrackPolyline = null;
|
||
}
|
||
if (this.waybillTrackStartMarker) {
|
||
this.waybillTrackStartMarker.setMap(null);
|
||
this.waybillTrackStartMarker = null;
|
||
}
|
||
if (this.waybillTrackEndMarker) {
|
||
this.waybillTrackEndMarker.setMap(null);
|
||
this.waybillTrackEndMarker = null;
|
||
}
|
||
if (this.waybillTrackMapInstance) {
|
||
this.waybillTrackMapInstance.destroy();
|
||
this.waybillTrackMapInstance = null;
|
||
}
|
||
},
|
||
async loadWaybillLocate() {
|
||
const waybillId = this.detailRow?.id;
|
||
if (!waybillId) {
|
||
this.$message.warning('运单信息不完整');
|
||
return;
|
||
}
|
||
if (!this.detailRow.vehicleNo) {
|
||
this.$message.warning('运单未绑定车牌号,无法实时定位');
|
||
return;
|
||
}
|
||
this.waybillLocateLoading = true;
|
||
this.waybillLocateHint = '正在获取车辆实时定位...';
|
||
this.waybillLocateInfo = null;
|
||
try {
|
||
const locateApi =
|
||
typeof this.api.locateVehicle === 'function' ? this.api.locateVehicle : locateVehicle;
|
||
const res = await locateApi(waybillId);
|
||
const data = res?.data?.data || null;
|
||
if (!data || data.longitude == null || data.latitude == null) {
|
||
this.waybillLocateInfo = null;
|
||
this.waybillLocateHint = '暂无车辆实时定位数据';
|
||
this.$message.warning('暂无车辆实时定位数据');
|
||
return;
|
||
}
|
||
this.waybillLocateInfo = data;
|
||
this.waybillLocateHint = '';
|
||
await this.$nextTick();
|
||
await this.renderWaybillLocateMap(data);
|
||
} catch (error) {
|
||
this.waybillLocateInfo = null;
|
||
this.waybillLocateHint = '实时定位获取失败';
|
||
this.$message.error(error?.message || '实时定位获取失败');
|
||
} finally {
|
||
this.waybillLocateLoading = false;
|
||
}
|
||
},
|
||
async loadWaybillTrack() {
|
||
const waybillId = this.detailRow?.id;
|
||
if (!waybillId) {
|
||
this.$message.warning('运单信息不完整');
|
||
return;
|
||
}
|
||
if (!this.detailRow.vehicleNo) {
|
||
this.$message.warning('运单未绑定车牌号,无法查询历史轨迹');
|
||
return;
|
||
}
|
||
if (!Array.isArray(this.waybillTrackDateRange) || this.waybillTrackDateRange.length !== 2) {
|
||
this.waybillTrackDateRange = this.buildDefaultTrackDateRange();
|
||
}
|
||
const [startDate, endDate] = this.waybillTrackDateRange;
|
||
this.waybillTrackLoading = true;
|
||
this.waybillTrackHint = '正在获取历史轨迹...';
|
||
this.destroyWaybillTrackMap();
|
||
try {
|
||
const trackApi =
|
||
typeof this.api.trackVehicle === 'function' ? this.api.trackVehicle : trackVehicle;
|
||
const res = await trackApi(waybillId, startDate, endDate);
|
||
const data = res?.data?.data || null;
|
||
const points = Array.isArray(data?.points) ? data.points : [];
|
||
if (!data || points.length === 0) {
|
||
this.waybillTrackInfo = data || { total: 0, points: [] };
|
||
this.waybillTrackHint = '暂无可回放的物流轨迹';
|
||
this.$message.warning('暂无可回放的物流轨迹');
|
||
return;
|
||
}
|
||
this.waybillTrackInfo = data;
|
||
this.waybillTrackHint = '';
|
||
await this.$nextTick();
|
||
await this.renderWaybillTrackMap(points);
|
||
} catch (error) {
|
||
this.waybillTrackInfo = null;
|
||
this.waybillTrackHint = '历史轨迹获取失败';
|
||
this.$message.error(error?.message || '历史轨迹获取失败');
|
||
} finally {
|
||
this.waybillTrackLoading = false;
|
||
}
|
||
},
|
||
async renderWaybillLocateMap(locateInfo) {
|
||
const longitude = Number(locateInfo?.longitude);
|
||
const latitude = Number(locateInfo?.latitude);
|
||
if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) {
|
||
this.$message.warning('定位坐标无效,无法在地图上展示');
|
||
return;
|
||
}
|
||
try {
|
||
await this.loadAmap();
|
||
await this.$nextTick();
|
||
const container = this.$refs.waybillLocateMap;
|
||
if (!container || !window.AMap) {
|
||
this.$message.warning('高德地图容器未就绪');
|
||
return;
|
||
}
|
||
this.destroyWaybillLocateMap();
|
||
this.waybillLocateMapInstance = new window.AMap.Map(container, {
|
||
zoom: 15,
|
||
center: [longitude, latitude],
|
||
viewMode: '2D',
|
||
resizeEnable: true,
|
||
});
|
||
const content = [
|
||
`<div style="padding:4px 2px;line-height:1.6;font-size:12px;">`,
|
||
`<div><b>${locateInfo.vehicleNo || '车辆位置'}</b></div>`,
|
||
locateInfo.locateTime ? `<div>时间:${locateInfo.locateTime}</div>` : '',
|
||
locateInfo.address ? `<div>地址:${locateInfo.address}</div>` : '',
|
||
`<div>坐标:${longitude}, ${latitude}</div>`,
|
||
`</div>`,
|
||
].join('');
|
||
this.waybillLocateInfoWindow = new window.AMap.InfoWindow({
|
||
content,
|
||
offset: new window.AMap.Pixel(0, -30),
|
||
});
|
||
this.waybillLocateMarker = new window.AMap.Marker({
|
||
position: [longitude, latitude],
|
||
title: locateInfo.vehicleNo || '车辆位置',
|
||
anchor: 'bottom-center',
|
||
});
|
||
this.waybillLocateMarker.on('click', () => {
|
||
this.waybillLocateInfoWindow.open(this.waybillLocateMapInstance, [longitude, latitude]);
|
||
});
|
||
this.waybillLocateMapInstance.add(this.waybillLocateMarker);
|
||
this.waybillLocateInfoWindow.open(this.waybillLocateMapInstance, [longitude, latitude]);
|
||
this.waybillLocateMapInstance.setFitView([this.waybillLocateMarker], false, [40, 40, 40, 40]);
|
||
setTimeout(() => {
|
||
this.waybillLocateMapInstance?.resize?.();
|
||
}, 80);
|
||
} catch (error) {
|
||
window.console.warn('实时定位地图渲染失败', error);
|
||
this.$message.error('高德地图渲染失败,请稍后重试');
|
||
}
|
||
},
|
||
async renderWaybillTrackMap(points) {
|
||
const path = (points || [])
|
||
.map(item => [Number(item.longitude), Number(item.latitude)])
|
||
.filter(item => Number.isFinite(item[0]) && Number.isFinite(item[1]));
|
||
if (!path.length) {
|
||
this.$message.warning('轨迹坐标无效,无法在地图上展示');
|
||
return;
|
||
}
|
||
try {
|
||
await this.loadAmap();
|
||
await this.$nextTick();
|
||
const container = this.$refs.waybillTrackMap;
|
||
if (!container || !window.AMap) {
|
||
this.$message.warning('高德地图容器未就绪');
|
||
return;
|
||
}
|
||
this.destroyWaybillTrackMap();
|
||
this.waybillTrackMapInstance = new window.AMap.Map(container, {
|
||
zoom: 12,
|
||
center: path[0],
|
||
viewMode: '2D',
|
||
resizeEnable: true,
|
||
});
|
||
this.waybillTrackPolyline = new window.AMap.Polyline({
|
||
path,
|
||
strokeColor: '#409eff',
|
||
strokeWeight: 6,
|
||
strokeOpacity: 0.9,
|
||
lineJoin: 'round',
|
||
lineCap: 'round',
|
||
showDir: path.length > 1,
|
||
});
|
||
this.waybillTrackStartMarker = new window.AMap.Marker({
|
||
position: path[0],
|
||
title: '起点',
|
||
anchor: 'bottom-center',
|
||
label: {
|
||
content: '起',
|
||
direction: 'top',
|
||
offset: new window.AMap.Pixel(0, -6),
|
||
},
|
||
});
|
||
this.waybillTrackEndMarker = new window.AMap.Marker({
|
||
position: path[path.length - 1],
|
||
title: '终点',
|
||
anchor: 'bottom-center',
|
||
label: {
|
||
content: '终',
|
||
direction: 'top',
|
||
offset: new window.AMap.Pixel(0, -6),
|
||
},
|
||
});
|
||
this.waybillTrackMapInstance.add([
|
||
this.waybillTrackPolyline,
|
||
this.waybillTrackStartMarker,
|
||
this.waybillTrackEndMarker,
|
||
]);
|
||
this.waybillTrackMapInstance.setFitView(
|
||
[this.waybillTrackPolyline, this.waybillTrackStartMarker, this.waybillTrackEndMarker],
|
||
false,
|
||
[48, 48, 48, 48]
|
||
);
|
||
setTimeout(() => {
|
||
this.waybillTrackMapInstance?.resize?.();
|
||
}, 80);
|
||
} catch (error) {
|
||
window.console.warn('历史轨迹地图渲染失败', error);
|
||
this.$message.error('高德地图渲染失败,请稍后重试');
|
||
}
|
||
},
|
||
displayStatus(row, prop) {
|
||
const formatStatus = this.config.formatStatus;
|
||
const textProp = prop === this.config.statusProp ? this.config.statusTextProp : '';
|
||
const column = this.findColumn(this.option.column, prop);
|
||
const item = column && (column.dicData || []).find(dic => dic.value === row[prop]);
|
||
const defaultText =
|
||
textProp && row[textProp] ? row[textProp] : item ? item.label : row[prop] || '未知';
|
||
return typeof formatStatus === 'function'
|
||
? formatStatus(row, prop, defaultText)
|
||
: defaultText;
|
||
},
|
||
statusTagType(status) {
|
||
if ([1, 'pending', 'processing', 'approved', 'change_approved'].includes(status)) {
|
||
return 'success';
|
||
}
|
||
if ([2, 'cancelled', 'withdrawn', 'draft'].includes(status)) {
|
||
return 'info';
|
||
}
|
||
if (['rejected', 'change_rejected'].includes(status)) {
|
||
return 'danger';
|
||
}
|
||
if (status === 'completed') {
|
||
return 'primary';
|
||
}
|
||
return 'warning';
|
||
},
|
||
normalizeRow(row, saveAsDraft = false) {
|
||
const submitRow = { ...row };
|
||
const isAddMode =
|
||
this.crudDialogType === 'add' ||
|
||
(this.isStandaloneWaybillFormPage && this.$route.query.mode === 'add');
|
||
if (isAddMode) {
|
||
delete submitRow.id;
|
||
delete submitRow.waybillNo;
|
||
}
|
||
const skipValidation = saveAsDraft && this.config.skipDraftValidation === true;
|
||
if (
|
||
!skipValidation &&
|
||
!this.validateMobileFields([
|
||
[submitRow.departurePhone, '发货联系方式'],
|
||
[submitRow.arrivalPhone, '收货联系方式'],
|
||
[submitRow.driverPhone, this.isNonRoadTransport ? '联系电话' : '手机号'],
|
||
[submitRow.escortPhone, '押运人手机号'],
|
||
])
|
||
) {
|
||
return false;
|
||
}
|
||
if (this.taskInfoFormEnabled) {
|
||
this.syncTaskFreightCurrencyFromContract();
|
||
if (!skipValidation && !this.validateTaskInfoForm(submitRow)) return false;
|
||
this.syncTaskInfoToRow(submitRow);
|
||
}
|
||
if (this.config.enableAttachmentTable) {
|
||
submitRow.attachmentsJson = JSON.stringify(this.attachmentRows);
|
||
}
|
||
if (this.config.saveAsDraft && saveAsDraft) {
|
||
submitRow[this.config.draftStatusProp || 'approvalStatus'] =
|
||
this.config.draftStatus || 'draft';
|
||
}
|
||
delete submitRow.basicInfoTitle;
|
||
delete submitRow.templateInfoTitle;
|
||
delete submitRow.shippingInfoTitle;
|
||
delete submitRow.taskInfoTitle;
|
||
delete submitRow.taskInfoForm;
|
||
delete submitRow.goodsInfoTitle;
|
||
delete submitRow.contractFileTitle;
|
||
delete submitRow.paymentRatioTitle;
|
||
delete submitRow.attachmentTitle;
|
||
delete submitRow.changeRecordTitle;
|
||
delete submitRow.contractPeriod;
|
||
Object.keys(submitRow).forEach(key => {
|
||
if (typeof submitRow[key] === 'string') {
|
||
submitRow[key] = submitRow[key].trim();
|
||
}
|
||
});
|
||
return submitRow;
|
||
},
|
||
validateMobileFields(fields = []) {
|
||
const invalidField = fields.find(([value]) => String(value || '').trim() && !isMobile(value));
|
||
if (!invalidField) return true;
|
||
this.$message.warning(`${invalidField[1]}格式不正确`);
|
||
return false;
|
||
},
|
||
currencyName(value) {
|
||
const text = String(value || '');
|
||
if (text.includes(' - ')) return text.slice(text.indexOf(' - ') + 3);
|
||
return (
|
||
{
|
||
CNY: '人民币',
|
||
RMB: '人民币',
|
||
USD: '美元',
|
||
EUR: '欧元',
|
||
JPY: '日元',
|
||
GBP: '英镑',
|
||
}[text.toUpperCase()] || text
|
||
);
|
||
},
|
||
currencyRemark(value) {
|
||
const option = this.freightCurrencyOptions.find(
|
||
item => String(item.value).toUpperCase() === String(value || '').toUpperCase()
|
||
);
|
||
return option?.remark || this.currencyName(option?.label || value);
|
||
},
|
||
normalizeFreightCurrency(value) {
|
||
const raw =
|
||
typeof value === 'object'
|
||
? value.dictKey || value.value || value.code || value.dictValue || ''
|
||
: value;
|
||
const normalized = String(raw || 'RMB')
|
||
.trim()
|
||
.toUpperCase();
|
||
const code = normalized.includes(' - ') ? normalized.split(' - ')[0].trim() : normalized;
|
||
const option = this.freightCurrencyOptions.find(
|
||
item => String(item.value || '').toUpperCase() === code
|
||
);
|
||
return option?.value || code || 'RMB';
|
||
},
|
||
syncTaskFreightCurrencyFromContract(contract = null) {
|
||
const selected =
|
||
contract ||
|
||
this.contractOptions.find(item => String(item.id) === String(this.form.contractId));
|
||
const value =
|
||
selected?.settlementCurrency ||
|
||
selected?.settlementCurrencyCode ||
|
||
selected?.currency ||
|
||
this.form.settlementCurrency ||
|
||
'RMB';
|
||
this.taskFreightCurrency = this.normalizeFreightCurrency(value);
|
||
},
|
||
loadFreightCurrencyOptions() {
|
||
if (this.freightCurrencyOptions.length || this.freightCurrencyLoading) {
|
||
return Promise.resolve(this.freightCurrencyOptions);
|
||
}
|
||
this.freightCurrencyLoading = true;
|
||
return getSystemDictionary({ code: 'currency_type' })
|
||
.then(res => {
|
||
const currencyNameMap = {
|
||
CNY: '人民币',
|
||
RMB: '人民币',
|
||
USD: '美元',
|
||
EUR: '欧元',
|
||
JPY: '日元',
|
||
GBP: '英镑',
|
||
};
|
||
this.freightCurrencyOptions = this.normalizeDictOptions(res.data?.data || []).map(
|
||
item => {
|
||
const code = String(item.value || '').toUpperCase();
|
||
return {
|
||
...item,
|
||
label: `${code} - ${currencyNameMap[code] || item.label || item.value}`,
|
||
};
|
||
}
|
||
);
|
||
const resolveCurrencyValue = value => {
|
||
const normalized = String(value || '').toUpperCase();
|
||
const exact = this.freightCurrencyOptions.find(
|
||
item => String(item.value).toUpperCase() === normalized
|
||
);
|
||
if (exact) return exact.value;
|
||
if (normalized === 'CNY') {
|
||
const rmb = this.freightCurrencyOptions.find(
|
||
item => String(item.value).toUpperCase() === 'RMB'
|
||
);
|
||
return rmb?.value || value;
|
||
}
|
||
return value;
|
||
};
|
||
this.taskFreightCurrency = resolveCurrencyValue(this.taskFreightCurrency);
|
||
return this.freightCurrencyOptions;
|
||
})
|
||
.finally(() => {
|
||
this.freightCurrencyLoading = false;
|
||
});
|
||
},
|
||
openWaybillProcessConfig() {
|
||
if (!this.form.projectId || this.waybillProcessConfigLoading) return;
|
||
this.waybillProcessConfigBox = true;
|
||
this.waybillProcessConfigLoading = true;
|
||
this.getProjectProcessConfigRows(this.form.projectId)
|
||
.then(rows =>
|
||
Promise.all(
|
||
rows.map(row =>
|
||
getProcessConfigDetail(row.id)
|
||
.then(res => ({ ...row, ...(res.data?.data || res.data || {}) }))
|
||
.catch(() => row)
|
||
)
|
||
)
|
||
)
|
||
.then(rows => {
|
||
this.waybillProcessConfigRows = rows;
|
||
})
|
||
.finally(() => {
|
||
this.waybillProcessConfigLoading = false;
|
||
});
|
||
},
|
||
getProjectProcessConfigRows(projectId) {
|
||
if (!projectId) return Promise.resolve([]);
|
||
return getProcessConfigList(1, 100, { projectIds: projectId, status: 1 }).then(res =>
|
||
extractRecords(res).filter(row => {
|
||
const projectIds = String(row.projectIds || '')
|
||
.split(',')
|
||
.map(item => item.trim());
|
||
return Number(row.status) === 1 && projectIds.includes(String(projectId));
|
||
})
|
||
);
|
||
},
|
||
loadProjectProcessConfigState() {
|
||
const projectId = this.form.projectId;
|
||
this.hasProjectProcessConfig = false;
|
||
this.waybillProcessConfigRows = [];
|
||
if (!projectId) return Promise.resolve([]);
|
||
return this.getProjectProcessConfigRows(projectId)
|
||
.then(rows => {
|
||
if (String(this.form.projectId) === String(projectId)) {
|
||
this.hasProjectProcessConfig = rows.length > 0;
|
||
}
|
||
return rows;
|
||
})
|
||
.catch(() => []);
|
||
},
|
||
getProcessConfigEnabledNodes(row = {}) {
|
||
let storedNodes = [];
|
||
try {
|
||
const parsed = JSON.parse(row.nodeConfigJson || '[]');
|
||
storedNodes = Array.isArray(parsed) ? parsed : parsed.nodes || [];
|
||
} catch (error) {
|
||
storedNodes = [];
|
||
}
|
||
const includedNodes = String(row.includedNodes || '')
|
||
.split(',')
|
||
.map(item => item.trim())
|
||
.filter(Boolean);
|
||
return storedNodes.filter(
|
||
node =>
|
||
node.enabled !== false && (!includedNodes.length || includedNodes.includes(node.name))
|
||
);
|
||
},
|
||
processConfigTimelineDescriptions(node = {}) {
|
||
const descriptions = [];
|
||
if (node.key === 'accept') {
|
||
descriptions.push(
|
||
`是否接单:${node.confirmMode === 'no_confirm_accept' ? '无需确认接单' : '是'}`
|
||
);
|
||
if (node.confirmMode === 'yes' && node.confirmDriver) {
|
||
descriptions.push('确认接单人:司机');
|
||
}
|
||
return descriptions;
|
||
}
|
||
if (node.key === 'return') {
|
||
descriptions.push(
|
||
`是否确认:${node.confirmMode === 'no_confirm_complete' ? '无需确认完成' : '是'}`
|
||
);
|
||
if (node.confirmMode === 'yes' && node.confirmInternal) {
|
||
descriptions.push('确认完成:内部人员');
|
||
}
|
||
} else {
|
||
descriptions.push(`打卡操作:${node.punch ? '是' : '否'}`);
|
||
descriptions.push(`打卡定位:${node.location ? '是' : '否'}`);
|
||
if (node.key === 'transit' && node.punch) {
|
||
descriptions.push(`打卡频次:每${node.frequencyDays || 1}天打卡1次`);
|
||
descriptions.push(`打卡时段:${node.timeStart || '00:00'}-${node.timeEnd || '23:59'}`);
|
||
}
|
||
if (node.supportCargo && node.uploadCargo && node.cargoTypes?.length) {
|
||
descriptions.push(`上传货量:${node.cargoTypes.join('、')}`);
|
||
}
|
||
}
|
||
if (node.supportVoucher && node.uploadVoucher && node.voucherTypes?.length) {
|
||
descriptions.push(`上传凭证:${node.voucherTypes.join('、')}`);
|
||
}
|
||
return descriptions;
|
||
},
|
||
stopSubmitLoading(loading) {
|
||
if (typeof loading === 'function') {
|
||
loading();
|
||
}
|
||
},
|
||
rowSave(row, done, loading) {
|
||
const submitRow = this.normalizeRow(row);
|
||
if (!submitRow) {
|
||
this.stopSubmitLoading(loading);
|
||
return;
|
||
}
|
||
this.getSaveRequest()(submitRow).then(
|
||
() => {
|
||
this.onLoad(this.page);
|
||
this.$message({ type: 'success', message: '操作成功!' });
|
||
done();
|
||
if (this.isStandaloneWaybillPage) this.closeCrudDialog();
|
||
},
|
||
error => {
|
||
window.console.log(error);
|
||
this.stopSubmitLoading(loading);
|
||
}
|
||
);
|
||
},
|
||
rowUpdate(row, index, done, loading) {
|
||
const submitRow = this.normalizeRow(row);
|
||
if (!submitRow) {
|
||
this.stopSubmitLoading(loading);
|
||
return;
|
||
}
|
||
const request =
|
||
this.attachmentUploadMode && typeof this.api.updateAttachments === 'function'
|
||
? this.api.updateAttachments
|
||
: this.getSaveRequest();
|
||
request(submitRow).then(
|
||
() => {
|
||
this.attachmentUploadMode = false;
|
||
this.onLoad(this.page);
|
||
this.$message({ type: 'success', message: '操作成功!' });
|
||
done();
|
||
if (this.isStandaloneWaybillPage) this.closeCrudDialog();
|
||
},
|
||
error => {
|
||
this.attachmentUploadMode = false;
|
||
window.console.log(error);
|
||
this.stopSubmitLoading(loading);
|
||
}
|
||
);
|
||
},
|
||
getSaveRequest() {
|
||
return this.api.submit;
|
||
},
|
||
handleDraftSave() {
|
||
const request =
|
||
typeof this.api.saveDraft === 'function' ? this.api.saveDraft : this.api.submit;
|
||
if (this.draftSaveLoading) return;
|
||
const submitRow = this.normalizeRow(this.form, true);
|
||
if (!submitRow) return;
|
||
this.draftSaveLoading = true;
|
||
request(submitRow)
|
||
.then(() => {
|
||
this.$message.success(`${this.config.draftSaveText || '保存'}成功`);
|
||
this.onLoad(this.page, this.query);
|
||
this.closeCrudDialog();
|
||
})
|
||
.finally(() => {
|
||
this.draftSaveLoading = false;
|
||
});
|
||
},
|
||
closeCrudDialog() {
|
||
if (this.isStandaloneWaybillPage) {
|
||
this.$router.$avueRouter?.closeTag?.();
|
||
this.$router.push({ path: '/business/waybill-manage', query: {} });
|
||
return;
|
||
}
|
||
const crud = this.$refs.crud;
|
||
if (crud && typeof crud.closeDialog === 'function') {
|
||
crud.closeDialog();
|
||
}
|
||
},
|
||
rowDel(row) {
|
||
this.$confirm('确定将选择数据删除?', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
})
|
||
.then(() => this.api.remove(row.id))
|
||
.then(res => {
|
||
this.afterRemove(res.data.data);
|
||
});
|
||
},
|
||
handleDelete() {
|
||
if (this.selectionList.length === 0) {
|
||
this.$message.warning('请选择至少一条数据');
|
||
return;
|
||
}
|
||
const selection = this.selectionList.filter(item => this.canDelete(item));
|
||
if (!selection.length) {
|
||
this.$message.warning('所选数据当前状态不可删除');
|
||
return;
|
||
}
|
||
this.$confirm('确定将选择数据删除?', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
})
|
||
.then(() => this.api.remove(selection.map(item => item.id).join(',')))
|
||
.then(res => {
|
||
this.afterRemove(res.data.data);
|
||
});
|
||
},
|
||
afterRemove(result = {}) {
|
||
this.onLoad(this.page);
|
||
const successCount = result.successCount || 0;
|
||
const skippedCount = result.skippedCount || 0;
|
||
if (skippedCount) {
|
||
this.$message.warning(`成功删除${successCount}条,因业务引用跳过${skippedCount}条`);
|
||
} else {
|
||
this.$message({ type: 'success', message: '删除成功!' });
|
||
}
|
||
this.selectionClear();
|
||
},
|
||
beforeOpen(done, type) {
|
||
this.crudDialogType = type;
|
||
this.dialogReadonly = type === 'view';
|
||
const copyId = this.$route.query.copyId || '';
|
||
|
||
// 复制模式:先加载原数据,然后清除 ID 相关字段
|
||
if (type === 'add' && copyId) {
|
||
this.api.getDetail(copyId).then(res => {
|
||
const sourceData = res.data.data || {};
|
||
// 清除不应该复制的字段(业务状态与原运单保持一致)
|
||
delete sourceData.id;
|
||
delete sourceData.code;
|
||
delete sourceData.waybillNo;
|
||
delete sourceData.waybillStatus;
|
||
delete sourceData.driverAcceptStatus;
|
||
delete sourceData.driverAcceptTime;
|
||
delete sourceData.driverAcceptDriverId;
|
||
delete sourceData.driverRejectTime;
|
||
delete sourceData.driverRejectReason;
|
||
delete sourceData.loadingNo;
|
||
delete sourceData.masterNo;
|
||
delete sourceData.createTime;
|
||
delete sourceData.updateTime;
|
||
delete sourceData.createUser;
|
||
delete sourceData.updateUser;
|
||
delete sourceData.createDept;
|
||
delete sourceData.createUserName;
|
||
delete sourceData.updateUserName;
|
||
|
||
this.applyFormDetail(sourceData);
|
||
// 标记为手工创建;业务状态沿用原运单
|
||
this.form.dataSource = '手工创建';
|
||
done();
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (type === 'add') {
|
||
this.form = {
|
||
...(this.config.defaultForm || {}),
|
||
};
|
||
this.form.dataSource = '手工创建';
|
||
this.form.taskEntryMode = 'simple';
|
||
this.selectedProjectId = '';
|
||
this.attachmentRows = [];
|
||
this.selectedAttachmentRows = [];
|
||
this.shippingPlanOptions = [];
|
||
if (this.contractSelectEnabled) {
|
||
this.contractOptions = [];
|
||
this.form.contractId = '';
|
||
this.form.contractName = '';
|
||
}
|
||
if (this.shippingPlanSelectEnabled) {
|
||
this.clearShippingPlan();
|
||
}
|
||
this.transportCargoRows = [];
|
||
this.taskFreightCurrency = 'RMB';
|
||
this.hasProjectProcessConfig = false;
|
||
this.waybillProcessConfigRows = [];
|
||
this.taskCarrierRequestId += 1;
|
||
this.taskCarrierOptions = [];
|
||
this.taskCarrierLoading = false;
|
||
this.resetWaybillCarrierContractState();
|
||
this.initTaskInfoForm();
|
||
this.initWaybillCargoRows();
|
||
if (this.config.enableTemplateCodePreview && typeof this.api.nextCode === 'function') {
|
||
this.api
|
||
.nextCode()
|
||
.then(res => {
|
||
this.form.templateCode = res.data?.data || '';
|
||
})
|
||
.finally(done);
|
||
return;
|
||
}
|
||
done();
|
||
return;
|
||
}
|
||
if (['edit', 'view'].includes(type)) {
|
||
this.api.getDetail(this.form.id).then(res => {
|
||
this.applyFormDetail(res.data.data || {});
|
||
done();
|
||
});
|
||
return;
|
||
}
|
||
done();
|
||
},
|
||
applyFormDetail(row) {
|
||
this.suppressTransportTypeClear = true;
|
||
this.form = { ...(row || {}) };
|
||
['quantity', 'mileage', 'unitPrice'].forEach(prop => {
|
||
if (Number(this.form[prop]) === -1) this.form[prop] = '';
|
||
});
|
||
if ([this.form.planId, this.form.planName].some(value => Number(value) === -1)) {
|
||
this.form.planId = '';
|
||
this.form.planName = '';
|
||
}
|
||
this.form.mileage = this.normalizeMileageValue(this.form.mileage);
|
||
this.$nextTick(() => {
|
||
this.suppressTransportTypeClear = false;
|
||
});
|
||
this.selectedProjectId = this.form.projectId || '';
|
||
this.resetWaybillCarrierContractState();
|
||
this.attachmentRows = this.parseJsonArray(this.form.attachmentsJson);
|
||
this.selectedAttachmentRows = [];
|
||
this.initWaybillCargoRows();
|
||
this.initTaskInfoForm();
|
||
if (this.config.enableProjectSelect) {
|
||
this.syncCurrentProjectOption();
|
||
}
|
||
this.loadProjectProcessConfigState();
|
||
if (this.contractSelectEnabled) {
|
||
this.syncCurrentContractOption();
|
||
this.syncTaskFreightCurrencyFromContract();
|
||
this.loadContractOptionsForProject();
|
||
}
|
||
if (this.shippingPlanSelectEnabled) {
|
||
this.syncCurrentShippingPlanOption();
|
||
}
|
||
},
|
||
parseJsonArray(value) {
|
||
if (!value) return [];
|
||
if (Array.isArray(value)) return value;
|
||
try {
|
||
const list = JSON.parse(value);
|
||
return Array.isArray(list) ? list : [];
|
||
} catch (error) {
|
||
return [];
|
||
}
|
||
},
|
||
parseJsonObject(value) {
|
||
if (!value) return {};
|
||
if (typeof value === 'object' && !Array.isArray(value)) return value;
|
||
try {
|
||
const data = JSON.parse(value);
|
||
return data && typeof data === 'object' && !Array.isArray(data) ? data : {};
|
||
} catch (error) {
|
||
return {};
|
||
}
|
||
},
|
||
parseQuantity(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;
|
||
},
|
||
formatQuantity(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+$/, '');
|
||
},
|
||
loadProjectOptions() {
|
||
if (!this.config.enableProjectSelect || this.projectLoading) return;
|
||
this.projectLoading = true;
|
||
getProjectList(1, 9999, this.config.projectQueryParams || {})
|
||
.then(res => {
|
||
this.projectOptions = extractRecords(res).filter(
|
||
item => !this.config.excludeVoidedProjects || item.approvalStatus !== 'voided'
|
||
);
|
||
this.syncCurrentProjectOption();
|
||
})
|
||
.finally(() => {
|
||
this.projectLoading = false;
|
||
});
|
||
},
|
||
syncCurrentProjectOption() {
|
||
if (!this.form.projectId || !this.form.projectName) return;
|
||
if (this.config.excludeVoidedProjects) return;
|
||
const exists = this.projectOptions.some(
|
||
item => String(item.id) === String(this.form.projectId)
|
||
);
|
||
if (!exists) {
|
||
this.projectOptions.unshift({
|
||
id: this.form.projectId,
|
||
projectName: this.form.projectName,
|
||
projectCode: this.form.projectCode,
|
||
undertakeDeptName: this.form.undertakeDeptName,
|
||
fundLimit: this.form.projectFundLimit,
|
||
});
|
||
}
|
||
},
|
||
handleProjectChange(projectId) {
|
||
const project = this.projectOptions.find(item => String(item.id) === String(projectId));
|
||
if (!project) {
|
||
this.form.projectId = '';
|
||
this.form.projectName = '';
|
||
this.form.projectCode = '';
|
||
this.form.undertakeDeptName = '';
|
||
if (this.contractSelectEnabled) {
|
||
this.form.contractId = '';
|
||
this.form.contractName = '';
|
||
this.contractOptions = [];
|
||
this.syncTaskFreightCurrencyFromContract();
|
||
}
|
||
if (this.shippingPlanSelectEnabled) {
|
||
this.clearShippingPlan();
|
||
this.shippingPlanOptions = [];
|
||
}
|
||
this.taskCarrierRequestId += 1;
|
||
this.taskCarrierOptions = [];
|
||
this.taskCarrierLoading = false;
|
||
this.resetWaybillCarrierContractState();
|
||
this.form.carrierName = '';
|
||
this.form.carrierId = '';
|
||
this.form.carrierContractId = '';
|
||
this.hasProjectProcessConfig = false;
|
||
this.waybillProcessConfigRows = [];
|
||
return;
|
||
}
|
||
this.form.projectId = project.id;
|
||
this.form.projectName = project.projectName || '';
|
||
this.form.projectCode = project.projectCode || '';
|
||
this.form.undertakeDeptName = project.undertakeDeptName || '';
|
||
this.taskCarrierRequestId += 1;
|
||
this.taskCarrierLoading = false;
|
||
this.resetWaybillCarrierContractState();
|
||
this.form.carrierName = '';
|
||
this.form.carrierId = '';
|
||
this.form.carrierContractId = '';
|
||
this.loadProjectProcessConfigState();
|
||
this.loadTaskCarrierOptions(project);
|
||
if (this.contractSelectEnabled) {
|
||
this.form.customerName =
|
||
project.customerName ||
|
||
project.customerNames ||
|
||
project.customer ||
|
||
this.form.customerName ||
|
||
'';
|
||
}
|
||
if (this.contractSelectEnabled) {
|
||
this.form.contractId = '';
|
||
this.form.contractName = '';
|
||
this.syncTaskFreightCurrencyFromContract();
|
||
this.loadContractOptionsForProject(true);
|
||
}
|
||
if (this.shippingPlanSelectEnabled) {
|
||
this.clearShippingPlan();
|
||
this.loadShippingPlanOptions();
|
||
}
|
||
this.$refs.crud?.validateField?.('projectName');
|
||
},
|
||
loadContractOptionsForProject(autoPick = false) {
|
||
if (!this.contractSelectEnabled || !this.form.projectId || this.contractLoading) {
|
||
return Promise.resolve([]);
|
||
}
|
||
this.contractLoading = true;
|
||
return getContractList(1, 9999, {
|
||
projectId: this.form.projectId,
|
||
projectName: this.form.projectName,
|
||
...(this.config.contractQueryParams || {}),
|
||
})
|
||
.then(res => {
|
||
this.contractOptions = extractRecords(res);
|
||
this.syncCurrentContractOption();
|
||
this.syncTaskFreightCurrencyFromContract();
|
||
if (autoPick && this.contractOptions.length) {
|
||
this.applyContract(this.contractOptions[0]);
|
||
}
|
||
return this.contractOptions;
|
||
})
|
||
.finally(() => {
|
||
this.contractLoading = false;
|
||
});
|
||
},
|
||
syncCurrentContractOption() {
|
||
if (!this.form.contractId || !this.form.contractName) return;
|
||
const exists = this.contractOptions.some(
|
||
item => String(item.id) === String(this.form.contractId)
|
||
);
|
||
if (!exists) {
|
||
this.contractOptions.unshift({
|
||
id: this.form.contractId,
|
||
contractName: this.form.contractName,
|
||
settlementCurrency: this.form.settlementCurrency || '',
|
||
});
|
||
}
|
||
},
|
||
handleContractChange(contractId) {
|
||
const contract = this.contractOptions.find(item => String(item.id) === String(contractId));
|
||
if (!contract) {
|
||
this.form.contractId = '';
|
||
this.form.contractName = '';
|
||
this.syncTaskFreightCurrencyFromContract();
|
||
return;
|
||
}
|
||
this.applyContract(contract);
|
||
},
|
||
applyContract(contract = {}) {
|
||
this.form.contractId = contract.id || '';
|
||
this.form.contractName = contract.contractName || '';
|
||
this.form.settlementCurrency =
|
||
contract.settlementCurrency || contract.settlementCurrencyCode || contract.currency || '';
|
||
this.form.customerName =
|
||
contract.partyA ||
|
||
contract.customerName ||
|
||
contract.customerNames ||
|
||
this.form.customerName ||
|
||
'';
|
||
this.syncTaskFreightCurrencyFromContract(contract);
|
||
this.fillSelfOperatedCarrierFromContract(contract);
|
||
this.$refs.crud?.validateField?.('contractName');
|
||
},
|
||
loadShippingPlanOptions() {
|
||
if (!this.shippingPlanSelectEnabled || this.shippingPlanLoading) {
|
||
return Promise.resolve([]);
|
||
}
|
||
this.shippingPlanLoading = true;
|
||
return getShippingPlanList(1, 9999, {
|
||
projectId: this.form.projectId,
|
||
projectName: this.form.projectName,
|
||
})
|
||
.then(res => {
|
||
this.shippingPlanOptions = extractRecords(res);
|
||
this.syncCurrentShippingPlanOption();
|
||
return this.shippingPlanOptions;
|
||
})
|
||
.finally(() => {
|
||
this.shippingPlanLoading = false;
|
||
});
|
||
},
|
||
syncCurrentShippingPlanOption() {
|
||
if (!this.form.planName) return;
|
||
const exists = this.shippingPlanOptions.some(
|
||
item =>
|
||
String(item.id) === String(this.form.planId) ||
|
||
String(item.planName) === String(this.form.planName)
|
||
);
|
||
if (!exists) {
|
||
this.shippingPlanOptions.unshift({
|
||
id: this.form.planId || this.form.planName,
|
||
planName: this.form.planName,
|
||
});
|
||
}
|
||
},
|
||
handleShippingPlanChange(planId) {
|
||
const plan = this.shippingPlanOptions.find(item => String(item.id) === String(planId));
|
||
if (!plan) {
|
||
this.clearShippingPlan();
|
||
return;
|
||
}
|
||
this.shippingPlanLoading = true;
|
||
getShippingPlanDetail(plan.id)
|
||
.then(res => this.applyShippingPlan(res.data?.data || res.data || plan))
|
||
.finally(() => {
|
||
this.shippingPlanLoading = false;
|
||
});
|
||
},
|
||
applyShippingPlan(plan = {}) {
|
||
this.suppressTransportTypeClear = true;
|
||
this.form.planId = plan.id || '';
|
||
this.form.planName = plan.planName || '';
|
||
[
|
||
'contractId',
|
||
'contractName',
|
||
'customerName',
|
||
'transportType',
|
||
'transportMode',
|
||
'transportTypeName',
|
||
'transportModeName',
|
||
'departureAddressId',
|
||
'departureName',
|
||
'departureAddress',
|
||
'departureContact',
|
||
'departurePhone',
|
||
'departureProvince',
|
||
'departureCity',
|
||
'departureDistrict',
|
||
'arrivalAddressId',
|
||
'arrivalName',
|
||
'arrivalAddress',
|
||
'arrivalContact',
|
||
'arrivalPhone',
|
||
'arrivalProvince',
|
||
'arrivalCity',
|
||
'arrivalDistrict',
|
||
].forEach(prop => {
|
||
if (plan[prop] !== undefined) this.form[prop] = plan[prop];
|
||
});
|
||
this.form.estimatedStartTime = plan.planStartDate || plan.estimatedStartTime || '';
|
||
this.form.estimatedEndTime = plan.planEndDate || plan.estimatedEndTime || '';
|
||
this.form.goodsJson =
|
||
typeof plan.goodsJson === 'string' ? plan.goodsJson : JSON.stringify(plan.goodsJson || []);
|
||
this.attachmentRows = this.parseJsonArray(plan.attachmentsJson);
|
||
this.form.attachmentsJson = JSON.stringify(this.attachmentRows);
|
||
const goodsRows = this.parseJsonArray(this.form.goodsJson);
|
||
this.form.taskEntryMode = goodsRows.length > 1 ? 'full' : 'simple';
|
||
const firstGoods = goodsRows[0] || {};
|
||
[
|
||
'cargoType',
|
||
'cargoTypeCode',
|
||
'cargoName',
|
||
'specification',
|
||
'model',
|
||
'quantity',
|
||
'quantityUnit',
|
||
].forEach(prop => {
|
||
if (firstGoods[prop] !== undefined) this.form[prop] = firstGoods[prop];
|
||
});
|
||
this.form.cargoTypePath = this.normalizeCargoTypePath(firstGoods.cargoTypePath);
|
||
this.initTaskInfoForm();
|
||
this.ensureCargoTypeOptions().then(() => {
|
||
if (this.form.cargoTypePath.length) return;
|
||
const cargoType = this.findCargoTypeByCodeOrName({
|
||
cargoTypeCode: this.form.cargoTypeCode,
|
||
cargoType: this.form.cargoType,
|
||
});
|
||
if (cargoType?.path) this.form.cargoTypePath = cargoType.path;
|
||
});
|
||
if (this.isTaskFullMode) {
|
||
this.initTaskFullCargoRows(goodsRows);
|
||
}
|
||
if (this.contractSelectEnabled) this.syncCurrentContractOption();
|
||
this.$refs.crud?.validateField?.('planName');
|
||
this.$nextTick(() => {
|
||
this.suppressTransportTypeClear = false;
|
||
});
|
||
},
|
||
clearShippingPlan() {
|
||
this.form.planId = '';
|
||
this.form.planName = '';
|
||
},
|
||
initTaskInfoForm() {
|
||
if (!this.taskInfoFormEnabled) return;
|
||
const taskInfo = this.parseJsonObject(this.form.taskInfoJson);
|
||
const freightInfo = this.parseJsonObject(this.form.freightJson);
|
||
const carrierRows = this.parseJsonArray(this.form.carrierJson);
|
||
const carrierInfo = carrierRows[0] || this.parseJsonObject(this.form.carrierJson);
|
||
const goodsRows = this.parseJsonArray(this.form.goodsJson);
|
||
const goodsInfo = goodsRows[0] || {};
|
||
this.form.taskEntryMode =
|
||
this.form.taskEntryMode || taskInfo.taskEntryMode || carrierInfo.taskEntryMode || 'simple';
|
||
if (this.hasMultipleConfiguredGoods(this.form, goodsRows)) {
|
||
this.form.taskEntryMode = 'full';
|
||
}
|
||
this.form.carrierType =
|
||
this.form.carrierType ||
|
||
taskInfo.carrierType ||
|
||
carrierInfo.carrierType ||
|
||
carrierInfo.type ||
|
||
'承运商';
|
||
this.form.carrierName =
|
||
this.form.carrierName ||
|
||
taskInfo.carrierName ||
|
||
carrierInfo.carrierName ||
|
||
carrierInfo.customerName ||
|
||
carrierInfo.name ||
|
||
'';
|
||
this.form.carrierContractId =
|
||
this.form.carrierContractId ||
|
||
taskInfo.carrierContractId ||
|
||
carrierInfo.carrierContractId ||
|
||
'';
|
||
this.form.driverName =
|
||
this.form.driverName || taskInfo.driverName || carrierInfo.driverName || '';
|
||
this.form.driverPhone =
|
||
this.form.driverPhone ||
|
||
taskInfo.driverPhone ||
|
||
carrierInfo.driverPhone ||
|
||
carrierInfo.mobile ||
|
||
'';
|
||
this.form.vehicleNo =
|
||
this.form.vehicleNo || taskInfo.vehicleNo || carrierInfo.vehicleNo || '';
|
||
this.form.captainName = this.form.captainName || taskInfo.captainName || '';
|
||
this.form.cabinNo = this.form.cabinNo || taskInfo.cabinNo || '';
|
||
this.form.containerNo = this.form.containerNo || taskInfo.containerNo || '';
|
||
this.form.trailerVehicleNo =
|
||
this.form.trailerVehicleNo ||
|
||
taskInfo.trailerVehicleNo ||
|
||
carrierInfo.trailerVehicleNo ||
|
||
'';
|
||
this.form.escortName =
|
||
this.form.escortName || taskInfo.escortName || carrierInfo.escortName || '';
|
||
this.form.escortPhone =
|
||
this.form.escortPhone || taskInfo.escortPhone || carrierInfo.escortPhone || '';
|
||
this.form.cargoType =
|
||
this.form.cargoType ||
|
||
taskInfo.cargoType ||
|
||
goodsInfo.cargoType ||
|
||
goodsInfo.goodsType ||
|
||
'';
|
||
this.form.cargoTypeCode =
|
||
this.form.cargoTypeCode ||
|
||
taskInfo.cargoTypeCode ||
|
||
goodsInfo.cargoTypeCode ||
|
||
goodsInfo.goodsTypeCode ||
|
||
'';
|
||
this.form.cargoName =
|
||
this.form.cargoName ||
|
||
taskInfo.cargoName ||
|
||
goodsInfo.cargoName ||
|
||
goodsInfo.goodsName ||
|
||
'';
|
||
this.form.specification =
|
||
this.form.specification || taskInfo.specification || goodsInfo.specification || '';
|
||
this.form.model = this.form.model || taskInfo.model || goodsInfo.model || '';
|
||
this.form.quantity = this.form.quantity || taskInfo.quantity || goodsInfo.quantity || '';
|
||
this.form.quantityUnit =
|
||
this.form.quantityUnit || taskInfo.quantityUnit || goodsInfo.quantityUnit || '吨';
|
||
const mileage = this.form.mileage || taskInfo.mileage || goodsInfo.mileage || '';
|
||
this.form.mileage = this.normalizeMileageValue(mileage);
|
||
this.form.unitPrice = this.form.unitPrice || taskInfo.unitPrice || goodsInfo.unitPrice || '';
|
||
['quantity', 'mileage', 'unitPrice'].forEach(prop => {
|
||
if (Number(this.form[prop]) === -1) this.form[prop] = '';
|
||
});
|
||
this.form.priceUnit =
|
||
this.form.priceUnit || taskInfo.priceUnit || goodsInfo.priceUnit || '元/吨';
|
||
this.form.otherFeeTotal =
|
||
this.form.otherFeeTotal || taskInfo.otherFeeTotal || goodsInfo.otherFeeTotal || '';
|
||
if (Number(this.form.otherFeeTotal) === -1) {
|
||
this.form.otherFeeTotal = '';
|
||
}
|
||
this.form.estimatedStartTime =
|
||
this.form.estimatedStartTime ||
|
||
taskInfo.estimatedStartTime ||
|
||
goodsInfo.estimatedStartTime ||
|
||
'';
|
||
this.form.estimatedEndTime =
|
||
this.form.estimatedEndTime || taskInfo.estimatedEndTime || goodsInfo.estimatedEndTime || '';
|
||
this.form.taskRemark = this.form.taskRemark || taskInfo.taskRemark || goodsInfo.remark || '';
|
||
this.taskFreightCurrency = freightInfo.currency || this.taskFreightCurrency || 'RMB';
|
||
this.syncTaskFreightCurrencyFromContract();
|
||
this.loadFreightCurrencyOptions();
|
||
this.form.cargoTypePath = this.normalizeCargoTypePath(this.form.cargoTypePath);
|
||
if (!this.form.cargoTypePath.length) {
|
||
this.ensureCargoTypeOptions().then(() => {
|
||
const cargoType = this.findCargoTypeByCodeOrName({
|
||
cargoTypeCode: this.form.cargoTypeCode,
|
||
cargoType: this.form.cargoType,
|
||
});
|
||
if (cargoType?.path) {
|
||
this.form.cargoTypePath = cargoType.path;
|
||
}
|
||
});
|
||
}
|
||
if (this.isTaskFullMode) {
|
||
this.initTaskFullCargoRows(goodsRows);
|
||
}
|
||
if (this.form.projectId) {
|
||
this.loadTaskCarrierOptions();
|
||
}
|
||
},
|
||
handleTaskEntryModeChange(value) {
|
||
if (
|
||
value === 'simple' &&
|
||
this.hasMultipleConfiguredGoods(this.form, this.transportCargoRows)
|
||
) {
|
||
this.form.taskEntryMode = 'full';
|
||
this.$message.warning('当前配置了多条货物信息,不能切换为精简录入');
|
||
return;
|
||
}
|
||
this.form.taskEntryMode = value || 'simple';
|
||
if (this.form.taskEntryMode === 'full') {
|
||
this.initTaskFullCargoRows(this.parseJsonArray(this.form.goodsJson));
|
||
}
|
||
},
|
||
handleTaskFullFreightNumberInput(cargo, prop, value) {
|
||
const text = String(value || '').replace(/[^\d.]/g, '');
|
||
const parts = text.split('.');
|
||
cargo[prop] =
|
||
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0];
|
||
this.syncTransportCargoJson();
|
||
},
|
||
handleTaskFullFreightAmountInput(cargo, value) {
|
||
this.handleTaskFullFreightNumberInput(cargo, 'freightAmount', value);
|
||
},
|
||
taskFullFreightAutoCalculable() {
|
||
const rows = this.transportCargoRows || [];
|
||
if (!rows.length) return false;
|
||
const quantityUnits = rows.map(row => String(row.quantityUnit || '').trim());
|
||
if (quantityUnits.some(unit => !unit) || new Set(quantityUnits).size !== 1) return false;
|
||
const quantityUnit = quantityUnits[0];
|
||
return rows.every(row => {
|
||
const priceUnit = String(row.priceUnit || '').trim();
|
||
const separatorIndex = Math.max(priceUnit.lastIndexOf('/'), priceUnit.lastIndexOf('/'));
|
||
const priceQuantityUnit =
|
||
separatorIndex >= 0 ? priceUnit.slice(separatorIndex + 1).trim() : priceUnit;
|
||
return !this.isBillingFieldEmpty(row.unitPrice) && priceQuantityUnit === quantityUnit;
|
||
});
|
||
},
|
||
taskFullFreightQuantity(index) {
|
||
return this.formatTaskFullFreightNumber(this.transportCargoRows[index]?.quantity || 0);
|
||
},
|
||
taskFullFreightAmount(cargo = {}) {
|
||
if (!this.taskFullFreightAutoCalculable()) {
|
||
return this.formatTaskFullFreightNumber(cargo.freightAmount);
|
||
}
|
||
if (!this.isBillingFieldEmpty(cargo.freightAmount)) {
|
||
return this.formatTaskFullFreightNumber(cargo.freightAmount);
|
||
}
|
||
return this.formatTaskFullFreightNumber(
|
||
Number(cargo.unitPrice || 0) * Number(cargo.quantity || 0)
|
||
);
|
||
},
|
||
taskFullFreightGroupQuantity(group = {}) {
|
||
const total = (group.rows || []).reduce((sum, cargo) => sum + Number(cargo.quantity || 0), 0);
|
||
return this.formatTaskFullFreightNumber(total);
|
||
},
|
||
taskFullFreightGroupUnitPrice(group = {}) {
|
||
const prices = (group.rows || [])
|
||
.map(cargo => String(cargo.unitPrice ?? '').trim())
|
||
.filter(Boolean);
|
||
if (!prices.length || prices.some(price => price !== prices[0])) return '';
|
||
return prices[0];
|
||
},
|
||
taskFullFreightGroupPriceUnit(group = {}) {
|
||
return group.rows?.[0]?.priceUnit || (group.quantityUnit ? `元/${group.quantityUnit}` : '');
|
||
},
|
||
taskFullFreightGroupAmount(group = {}) {
|
||
const total = (group.rows || []).reduce(
|
||
(sum, cargo) => sum + Number(this.taskFullFreightAmount(cargo) || 0),
|
||
0
|
||
);
|
||
return this.formatTaskFullFreightNumber(total);
|
||
},
|
||
handleTaskFullFreightGroupNumberInput(group, prop, value) {
|
||
const text = String(value || '').replace(/[^\d.]/g, '');
|
||
const parts = text.split('.');
|
||
const normalized =
|
||
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0];
|
||
(group.rows || []).forEach(cargo => {
|
||
cargo[prop] = normalized;
|
||
});
|
||
this.syncTransportCargoJson();
|
||
},
|
||
handleTaskFullFreightGroupPriceUnitChange(group, value) {
|
||
(group.rows || []).forEach(cargo => {
|
||
cargo.priceUnit = value || '';
|
||
});
|
||
this.syncTransportCargoJson();
|
||
},
|
||
handleTaskFullFreightGroupAmountInput(group, value) {
|
||
const text = String(value || '').replace(/[^\d.]/g, '');
|
||
const parts = text.split('.');
|
||
const normalized =
|
||
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0];
|
||
const rows = group.rows || [];
|
||
const total = Number(normalized || 0);
|
||
const quantities = rows.map(cargo => Math.max(0, Number(cargo.quantity || 0)));
|
||
const quantityTotal = quantities.reduce((sum, quantity) => sum + quantity, 0);
|
||
let allocated = 0;
|
||
rows.forEach((cargo, index) => {
|
||
if (index === rows.length - 1) {
|
||
cargo.freightAmount = this.formatTaskFullFreightNumber(total - allocated);
|
||
return;
|
||
}
|
||
let amount = 0;
|
||
if (quantityTotal) {
|
||
amount = Number((total * (quantities[index] / quantityTotal)).toFixed(2));
|
||
} else if (index === 0) {
|
||
amount = total;
|
||
}
|
||
allocated += amount;
|
||
cargo.freightAmount = this.formatTaskFullFreightNumber(amount);
|
||
});
|
||
this.syncTransportCargoJson();
|
||
},
|
||
formatTaskFullFreightNumber(value) {
|
||
if (value === undefined || value === null || String(value).trim() === '') return '';
|
||
const number = Number(value || 0);
|
||
if (!Number.isFinite(number)) return '';
|
||
return Number(number.toFixed(2)).toString();
|
||
},
|
||
handleTaskCarrierTypeChange(value) {
|
||
const nextValue = value || '承运商';
|
||
this.form.carrierName = '';
|
||
this.form.carrierId = '';
|
||
this.form.carrierContractId = '';
|
||
if (nextValue === '承运商') {
|
||
this.form.trailerVehicleNo = '';
|
||
this.form.escortName = '';
|
||
this.form.escortPhone = '';
|
||
}
|
||
this.form.carrierType = nextValue;
|
||
this.taskCarrierRequestId += 1;
|
||
this.taskCarrierOptions = [];
|
||
this.taskCarrierLoading = false;
|
||
this.resetWaybillCarrierContractState();
|
||
// 承运类型切换后必须基于当前项目重新获取承运商合同;
|
||
// 非自运场景的承运商选项仅允许使用合同乙方。
|
||
if (this.form.projectId) {
|
||
this.loadTaskCarrierOptions();
|
||
}
|
||
},
|
||
fillSelfOperatedCarrierFromContract(contract = {}) {
|
||
if (this.form.carrierType !== '自运') return;
|
||
const carrierName = String(
|
||
contract.partyB ||
|
||
contract.partyBName ||
|
||
contract.contractPartyB ||
|
||
contract.customerContractPartyB ||
|
||
''
|
||
).trim();
|
||
if (!carrierName) return;
|
||
const exists = this.taskCarrierOptions.some(
|
||
item => String(item.value || item.carrierName || '') === carrierName
|
||
);
|
||
if (!exists) {
|
||
this.taskCarrierOptions.push({
|
||
carrierName,
|
||
fullName: carrierName,
|
||
value: carrierName,
|
||
label: carrierName,
|
||
});
|
||
}
|
||
this.form.carrierName = carrierName;
|
||
this.form.carrierId = '';
|
||
},
|
||
isTaskCarrierRequired(carrierType) {
|
||
return String(carrierType || '').trim() === '承运商';
|
||
},
|
||
taskCarrierOptionValue(item = {}) {
|
||
if (this.form.carrierType !== '自运') {
|
||
return item.carrierContractId || item.value || item.id || '';
|
||
}
|
||
return (
|
||
item.carrierName ||
|
||
item.customerName ||
|
||
item.fullName ||
|
||
item.name ||
|
||
item.value ||
|
||
''
|
||
);
|
||
},
|
||
getProjectCarrierOptions(project = {}) {
|
||
const rows = this.parseJsonArray(project.carrierJson);
|
||
const names = String(project.carrierNames || '')
|
||
.split(/[、,,;;\n]/)
|
||
.map(item => item.trim())
|
||
.filter(Boolean);
|
||
const options = [];
|
||
const append = item => {
|
||
if (!item) return;
|
||
const id = item.id || item.carrierId || item.customerId || '';
|
||
const label =
|
||
(typeof item === 'string' ? item : '') ||
|
||
item.carrier ||
|
||
item.carrierName ||
|
||
item.fullName ||
|
||
item.shortName ||
|
||
item.customerName ||
|
||
item.name ||
|
||
'';
|
||
const value = String(label).trim();
|
||
if (!value || options.some(option => option.value === value)) return;
|
||
options.push({ id, carrierName: value, fullName: value, value });
|
||
};
|
||
rows.forEach(append);
|
||
names.forEach(append);
|
||
return options;
|
||
},
|
||
getWaybillCarrierContractOptions(contracts = []) {
|
||
const carrierNameCounts = contracts.reduce((counts, contract) => {
|
||
const carrierName = String(
|
||
contract.partyB ||
|
||
contract.partyBName ||
|
||
contract.contractPartyB ||
|
||
contract.customerContractPartyB ||
|
||
contract.secondParty ||
|
||
contract.secondPartyName ||
|
||
''
|
||
).trim();
|
||
if (carrierName) counts[carrierName] = (counts[carrierName] || 0) + 1;
|
||
return counts;
|
||
}, {});
|
||
return contracts
|
||
.map(contract => {
|
||
const carrierName = String(
|
||
contract.partyB ||
|
||
contract.partyBName ||
|
||
contract.contractPartyB ||
|
||
contract.customerContractPartyB ||
|
||
contract.secondParty ||
|
||
contract.secondPartyName ||
|
||
''
|
||
).trim();
|
||
if (!carrierName || !contract.id) return null;
|
||
const contractIdentifier = contract.contractName || contract.contractNo || contract.id;
|
||
return {
|
||
id: contract.id,
|
||
carrierId:
|
||
contract.partyBId ||
|
||
contract.partyBUserId ||
|
||
contract.contractPartyBId ||
|
||
contract.customerContractPartyBId ||
|
||
contract.secondPartyId ||
|
||
contract.secondPartyUserId ||
|
||
contract.carrierId ||
|
||
'',
|
||
value: contract.id,
|
||
label:
|
||
carrierNameCounts[carrierName] > 1
|
||
? `${carrierName}(${contractIdentifier})`
|
||
: carrierName,
|
||
carrierName,
|
||
fullName: carrierName,
|
||
carrierContractId: contract.id,
|
||
};
|
||
})
|
||
.filter(Boolean);
|
||
},
|
||
resetWaybillCarrierContractState() {
|
||
this.waybillCarrierContractRequestId += 1;
|
||
this.waybillCarrierContractsLoaded = false;
|
||
this.waybillHasCarrierContracts = null;
|
||
},
|
||
loadWaybillCarrierContractState(project = {}) {
|
||
const projectId = project.id || project.projectId || this.form.projectId;
|
||
const requestId = ++this.waybillCarrierContractRequestId;
|
||
if (!projectId) {
|
||
this.waybillCarrierContractsLoaded = false;
|
||
this.waybillHasCarrierContracts = null;
|
||
return Promise.resolve([]);
|
||
}
|
||
return getContractList(1, 9999, {
|
||
projectId,
|
||
projectName: project.projectName || this.form.projectName,
|
||
contractCategory: '承运商合同',
|
||
})
|
||
.then(res => {
|
||
if (requestId !== this.waybillCarrierContractRequestId) return null;
|
||
const options = this.getWaybillCarrierContractOptions(extractRecords(res));
|
||
this.waybillCarrierContractsLoaded = true;
|
||
this.waybillHasCarrierContracts = options.length > 0;
|
||
if (!options.length && this.form.carrierType !== '自运') {
|
||
this.form.carrierType = '自运';
|
||
this.form.carrierName = '';
|
||
this.form.carrierId = '';
|
||
this.form.carrierContractId = '';
|
||
}
|
||
return options;
|
||
})
|
||
.catch(() => {
|
||
if (requestId === this.waybillCarrierContractRequestId) {
|
||
this.waybillCarrierContractsLoaded = false;
|
||
this.waybillHasCarrierContracts = null;
|
||
}
|
||
return null;
|
||
});
|
||
},
|
||
loadTaskCarrierOptions(project = {}) {
|
||
if (!this.taskInfoFormEnabled || this.taskCarrierLoading) return Promise.resolve([]);
|
||
const projectId = project.id || project.projectId || this.form.projectId;
|
||
const requestId = ++this.taskCarrierRequestId;
|
||
const carrierTypeAtRequest = this.form.carrierType;
|
||
const contractPromise = this.loadWaybillCarrierContractState({
|
||
...project,
|
||
id: projectId,
|
||
});
|
||
if (carrierTypeAtRequest !== '自运') {
|
||
if (!projectId) {
|
||
this.taskCarrierOptions = [];
|
||
return Promise.resolve([]);
|
||
}
|
||
this.taskCarrierLoading = true;
|
||
return contractPromise
|
||
.then(options => {
|
||
if (requestId !== this.taskCarrierRequestId) return this.taskCarrierOptions;
|
||
if (options === null) return this.taskCarrierOptions;
|
||
this.taskCarrierOptions = options || [];
|
||
return this.taskCarrierOptions;
|
||
})
|
||
.catch(() => this.taskCarrierOptions)
|
||
.finally(() => {
|
||
if (requestId === this.taskCarrierRequestId) this.taskCarrierLoading = false;
|
||
});
|
||
}
|
||
const currentProject = this.projectOptions.find(
|
||
item => String(item.id) === String(projectId)
|
||
);
|
||
this.taskCarrierOptions = this.getProjectCarrierOptions({
|
||
...currentProject,
|
||
...project,
|
||
});
|
||
if (!projectId) return Promise.resolve(this.taskCarrierOptions);
|
||
this.taskCarrierLoading = true;
|
||
const detailPromise = getProjectDetail(projectId)
|
||
.then(res => {
|
||
if (requestId !== this.taskCarrierRequestId) return this.taskCarrierOptions;
|
||
const detail = res?.data?.data || {};
|
||
this.taskCarrierOptions = this.getProjectCarrierOptions({
|
||
...currentProject,
|
||
...project,
|
||
...detail,
|
||
});
|
||
return this.taskCarrierOptions;
|
||
})
|
||
.catch(() => this.taskCarrierOptions);
|
||
return Promise.all([contractPromise, detailPromise])
|
||
.then(([carrierContractOptions, options]) => {
|
||
if (requestId !== this.taskCarrierRequestId) return this.taskCarrierOptions;
|
||
if (carrierTypeAtRequest === '自运') {
|
||
this.taskCarrierOptions = [];
|
||
const customerContract = this.contractOptions.find(
|
||
item => String(item.id) === String(this.form.contractId)
|
||
);
|
||
this.fillSelfOperatedCarrierFromContract(customerContract);
|
||
return this.taskCarrierOptions;
|
||
}
|
||
this.taskCarrierOptions = options || [];
|
||
return this.taskCarrierOptions;
|
||
})
|
||
.finally(() => {
|
||
if (requestId === this.taskCarrierRequestId) this.taskCarrierLoading = false;
|
||
});
|
||
},
|
||
handleTaskCarrierChange(value) {
|
||
const carrier = this.taskCarrierOptions.find(item =>
|
||
[
|
||
this.taskCarrierOptionValue(item),
|
||
item.value,
|
||
item.customerName,
|
||
item.carrierName,
|
||
item.fullName,
|
||
item.name,
|
||
].some(name => String(name || '') === String(value || ''))
|
||
);
|
||
if (this.form.carrierType !== '自运') {
|
||
this.form.carrierContractId = carrier?.carrierContractId || '';
|
||
this.form.carrierId = carrier?.carrierId || '';
|
||
this.form.carrierName = carrier?.carrierName || '';
|
||
return;
|
||
}
|
||
this.form.carrierId = carrier?.carrierContractId ? '' : carrier?.id || '';
|
||
this.form.carrierName = value || '';
|
||
},
|
||
loadTaskDriverOptions() {
|
||
if (!this.taskInfoFormEnabled || this.taskDriverLoading) return Promise.resolve([]);
|
||
this.taskDriverLoading = true;
|
||
return getDriverList(1, 9999, { posts: '司机' })
|
||
.then(res => {
|
||
this.taskDriverOptions = extractRecords(res);
|
||
return this.taskDriverOptions;
|
||
})
|
||
.finally(() => {
|
||
this.taskDriverLoading = false;
|
||
});
|
||
},
|
||
fetchTaskDriverSuggestions(queryString, callback) {
|
||
const keyword = String(queryString || '').trim();
|
||
this.taskDriverLoading = true;
|
||
getDriverList(1, 20, { ...(keyword ? { driverName: keyword } : {}), posts: '司机' })
|
||
.then(res => {
|
||
const records = extractRecords(res);
|
||
this.taskDriverOptions = records;
|
||
callback(
|
||
records.map(item => ({
|
||
...item,
|
||
value: item.driverName || item.name || '',
|
||
}))
|
||
);
|
||
})
|
||
.catch(error => {
|
||
window.console.log(error);
|
||
callback([]);
|
||
})
|
||
.finally(() => {
|
||
this.taskDriverLoading = false;
|
||
});
|
||
},
|
||
fetchTaskEscortSuggestions(queryString, callback) {
|
||
const keyword = String(queryString || '').trim();
|
||
this.taskEscortLoading = true;
|
||
getDriverList(1, 20, { ...(keyword ? { driverName: keyword } : {}), posts: '押运员' })
|
||
.then(res => {
|
||
const records = extractRecords(res);
|
||
this.taskEscortOptions = records;
|
||
callback(records.map(item => ({ ...item, value: item.driverName || item.name || '' })));
|
||
})
|
||
.catch(error => {
|
||
window.console.log(error);
|
||
callback([]);
|
||
})
|
||
.finally(() => {
|
||
this.taskEscortLoading = false;
|
||
});
|
||
},
|
||
handleTaskDriverSelect(item = {}) {
|
||
const value = item.driverName || item.name || '';
|
||
const driverPhone = item.mobile || item.phone || item.driverPhone || '';
|
||
const drivingVehicle = String(item.drivingVehicle || '').trim();
|
||
this.form.driverId = item.id || '';
|
||
this.form.driverName = value;
|
||
if (driverPhone) this.form.driverPhone = driverPhone;
|
||
if (drivingVehicle) this.form.vehicleNo = drivingVehicle;
|
||
},
|
||
handleTaskEscortSelect(item = {}) {
|
||
const value = item.driverName || item.name || '';
|
||
const escortPhone = item.mobile || item.phone || item.driverPhone || '';
|
||
this.form.escortName = value;
|
||
if (escortPhone) this.form.escortPhone = escortPhone;
|
||
},
|
||
handleTaskDriverChange(value) {
|
||
const driver = this.taskDriverOptions.find(item =>
|
||
[item.driverName, item.name].some(name => String(name || '') === String(value || ''))
|
||
);
|
||
this.form.driverId = driver?.id || '';
|
||
this.form.driverName = value || '';
|
||
if (driver) {
|
||
this.form.driverPhone =
|
||
driver.mobile || driver.phone || driver.driverPhone || this.form.driverPhone || '';
|
||
if (String(driver.drivingVehicle || '').trim()) {
|
||
this.form.vehicleNo = String(driver.drivingVehicle).trim();
|
||
}
|
||
}
|
||
},
|
||
getTaskCargoTypeOptionsKey(path = []) {
|
||
const normalizedPath = this.normalizeCargoTypePath(path);
|
||
if (!normalizedPath.length) return '';
|
||
const cargoType = this.findCargoTypeByPath(normalizedPath);
|
||
return String(cargoType?.cargoCode || cargoType?.code || normalizedPath.join('/'));
|
||
},
|
||
getTaskCargoOptionsByPath(path = []) {
|
||
const key = this.getTaskCargoTypeOptionsKey(path);
|
||
return key ? this.taskCargoOptionsMap[key] || [] : [];
|
||
},
|
||
isTaskCargoOptionsLoading(path = []) {
|
||
const key = this.getTaskCargoTypeOptionsKey(path);
|
||
return key ? !!this.taskCargoLoadingMap[key] : false;
|
||
},
|
||
setTaskCargoOptionsByKey(key, options = []) {
|
||
if (!key) return;
|
||
this.taskCargoOptionsMap = {
|
||
...this.taskCargoOptionsMap,
|
||
[key]: options,
|
||
};
|
||
},
|
||
setTaskCargoLoadingByKey(key, loading) {
|
||
if (!key) return;
|
||
this.taskCargoLoadingMap = {
|
||
...this.taskCargoLoadingMap,
|
||
[key]: loading,
|
||
};
|
||
},
|
||
buildTaskCargoListQueryParams(path = []) {
|
||
const normalizedPath = this.normalizeCargoTypePath(path);
|
||
const labels = this.getCargoTypePathLabels(normalizedPath);
|
||
const secondCargoType = this.findCargoTypeByPath(normalizedPath);
|
||
return {
|
||
secondCargoTypeName: labels[1] || '',
|
||
secondCargoTypeCode: secondCargoType?.cargoCode || secondCargoType?.code || '',
|
||
};
|
||
},
|
||
loadTaskCargoOptionsByPath(path = [], cargoName = '') {
|
||
const normalizedPath = this.normalizeCargoTypePath(path);
|
||
if (!normalizedPath.length) return Promise.resolve([]);
|
||
return this.ensureCargoTypeOptions().then(() => {
|
||
const key = this.getTaskCargoTypeOptionsKey(normalizedPath);
|
||
const params = this.buildTaskCargoListQueryParams(normalizedPath);
|
||
const keyword = String(cargoName || '').trim();
|
||
if (!params.secondCargoTypeName && !params.secondCargoTypeCode && !keyword) {
|
||
this.setTaskCargoOptionsByKey(key, []);
|
||
return [];
|
||
}
|
||
this.setTaskCargoLoadingByKey(key, true);
|
||
return getCommonCargoList(1, 500, {
|
||
...params,
|
||
...(keyword ? { cargoName: keyword } : {}),
|
||
allDept: 0,
|
||
})
|
||
.then(res => {
|
||
const records = extractRecords(res);
|
||
this.setTaskCargoOptionsByKey(key, records);
|
||
return records;
|
||
})
|
||
.catch(error => {
|
||
window.console.log(error);
|
||
this.setTaskCargoOptionsByKey(key, []);
|
||
return [];
|
||
})
|
||
.finally(() => {
|
||
this.setTaskCargoLoadingByKey(key, false);
|
||
});
|
||
});
|
||
},
|
||
fetchTaskCargoSuggestions(queryString, callback, path = []) {
|
||
const keyword = String(queryString || '').trim();
|
||
const normalizedPath = this.normalizeCargoTypePath(path);
|
||
if (!keyword && !normalizedPath.length) {
|
||
callback([]);
|
||
return;
|
||
}
|
||
let loadingKey = '';
|
||
this.ensureCargoTypeOptions()
|
||
.then(() => {
|
||
const key = this.getTaskCargoTypeOptionsKey(normalizedPath);
|
||
loadingKey = key;
|
||
const params = this.buildTaskCargoListQueryParams(normalizedPath);
|
||
if (key) this.setTaskCargoLoadingByKey(key, true);
|
||
return getCommonCargoList(1, 50, {
|
||
...params,
|
||
...(keyword ? { cargoName: keyword } : {}),
|
||
allDept: 0,
|
||
}).then(res => {
|
||
const records = extractRecords(res);
|
||
if (key) this.setTaskCargoOptionsByKey(key, records);
|
||
callback(
|
||
records.map(item => ({
|
||
...item,
|
||
value: this.formatCargoTypeLabel(item),
|
||
}))
|
||
);
|
||
return key;
|
||
});
|
||
})
|
||
.catch(error => {
|
||
window.console.log(error);
|
||
callback([]);
|
||
})
|
||
.finally(() => {
|
||
if (loadingKey) this.setTaskCargoLoadingByKey(loadingKey, false);
|
||
});
|
||
},
|
||
handleTaskCargoRowSelect(row, item = {}) {
|
||
const value = this.formatCargoTypeLabel(item);
|
||
row.cargoName = value;
|
||
row.specification = item.specification || item.spec || row.specification || '';
|
||
row.model = item.model || item.modelName || row.model || '';
|
||
this.syncTransportCargoJson();
|
||
},
|
||
loadTaskCargoOptionsForForm() {
|
||
return this.loadTaskCargoOptionsByPath(this.form.cargoTypePath);
|
||
},
|
||
handleTaskCargoTypeChange(value) {
|
||
const path = this.normalizeCargoTypePath(value);
|
||
const cargoType = this.findCargoTypeByPath(path);
|
||
const labels = this.getCargoTypePathLabels(path);
|
||
this.form.cargoTypePath = path;
|
||
this.form.cargoType = labels.length ? labels[labels.length - 1] : '';
|
||
this.form.cargoTypeCode = cargoType?.cargoCode || cargoType?.code || cargoType?.id || '';
|
||
this.form.cargoName = '';
|
||
this.form.specification = '';
|
||
this.form.model = '';
|
||
this.loadTaskCargoOptionsForForm();
|
||
},
|
||
handleTaskCargoChange(value) {
|
||
const cargo = this.taskCargoOptions.find(item => this.formatCargoTypeLabel(item) === value);
|
||
this.form.cargoName = value || '';
|
||
if (cargo) {
|
||
this.form.specification =
|
||
cargo.specification || cargo.spec || this.form.specification || '';
|
||
this.form.model = cargo.model || cargo.modelName || this.form.model || '';
|
||
}
|
||
},
|
||
handleTaskCargoSelect(item = {}) {
|
||
this.form.cargoName = this.formatCargoTypeLabel(item);
|
||
this.form.specification = item.specification || item.spec || this.form.specification || '';
|
||
this.form.model = item.model || item.modelName || this.form.model || '';
|
||
},
|
||
getTaskCargoRowOptions(row = {}) {
|
||
return this.getTaskCargoOptionsByPath(row.cargoTypePath);
|
||
},
|
||
handleTaskCargoRowNameChange(row, value) {
|
||
const cargo = this.getTaskCargoRowOptions(row).find(
|
||
item => this.formatCargoTypeLabel(item) === value
|
||
);
|
||
row.cargoName = value || '';
|
||
if (cargo) {
|
||
row.specification = cargo.specification || cargo.spec || row.specification || '';
|
||
row.model = cargo.model || cargo.modelName || row.model || '';
|
||
}
|
||
this.syncTransportCargoJson();
|
||
},
|
||
initTaskFullCargoRows(goodsRows = []) {
|
||
if (!this.taskInfoFormEnabled) return;
|
||
const freightItems = this.parseJsonObject(this.form.freightJson).freightItems || [];
|
||
const rows = (goodsRows.length ? goodsRows : this.transportCargoRows).map((row, index) => {
|
||
const cargo = this.normalizeTransportCargoRow(row);
|
||
if (index === 0 && !cargo.unitPrice && this.form.unitPrice) {
|
||
cargo.unitPrice = this.form.unitPrice;
|
||
}
|
||
cargo.priceUnit = cargo.priceUnit || this.form.priceUnit || '元/吨';
|
||
if (this.isBillingFieldEmpty(cargo.freightAmount)) {
|
||
cargo.freightAmount = freightItems[index]?.freightAmount ?? '';
|
||
}
|
||
return cargo;
|
||
});
|
||
while (rows.length < 1) {
|
||
rows.push(this.normalizeTransportCargoRow());
|
||
}
|
||
this.transportCargoRows = rows;
|
||
this.syncTransportCargoJson();
|
||
},
|
||
loadTaskQuantityUnitOptions() {
|
||
if (this.taskQuantityUnitOptions.length || this.taskQuantityUnitLoading) {
|
||
return Promise.resolve(this.taskQuantityUnitOptions);
|
||
}
|
||
this.taskQuantityUnitLoading = true;
|
||
return getDictionary({ code: 'quantity_unit' })
|
||
.then(res => {
|
||
this.taskQuantityUnitOptions = this.normalizeDictOptions(res.data.data || []);
|
||
return this.taskQuantityUnitOptions;
|
||
})
|
||
.finally(() => {
|
||
this.taskQuantityUnitLoading = false;
|
||
});
|
||
},
|
||
loadTaskFeeUnitOptions() {
|
||
if (this.taskFeeUnitOptions.length || this.taskFeeUnitLoading) {
|
||
return Promise.resolve(this.taskFeeUnitOptions);
|
||
}
|
||
this.taskFeeUnitLoading = true;
|
||
return getDictionary({ code: 'unit_fee' })
|
||
.then(res => {
|
||
this.taskFeeUnitOptions = this.normalizeDictOptions(res.data.data || []);
|
||
return this.taskFeeUnitOptions;
|
||
})
|
||
.finally(() => {
|
||
this.taskFeeUnitLoading = false;
|
||
});
|
||
},
|
||
normalizeDictOptions(list = []) {
|
||
return (list || []).map(item => ({
|
||
label: item.dictValue || item.label || item.value || '',
|
||
value: item.dictKey || item.value || item.dictValue || '',
|
||
remark: item.remark || '',
|
||
}));
|
||
},
|
||
handleTaskNumberInput(prop, value) {
|
||
this.form[prop] = this.normalizeTaskNumber(value);
|
||
},
|
||
handleTaskMileageInput(value) {
|
||
this.form.mileage = String(value || '')
|
||
.replace(/\D/g, '')
|
||
.slice(0, 10);
|
||
},
|
||
normalizeMileageValue(value) {
|
||
if (value === undefined || value === null || value === '' || Number(value) === -1) return '';
|
||
const mileage = Number(value);
|
||
return Number.isFinite(mileage) && mileage >= 0 ? String(Math.trunc(mileage)) : '';
|
||
},
|
||
normalizeTaskNumber(value) {
|
||
const text = String(value || '').replace(/[^\d.]/g, '');
|
||
const parts = text.split('.');
|
||
return parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('')}` : parts[0];
|
||
},
|
||
validateTaskCargoRows() {
|
||
if (!this.transportCargoRows.length) {
|
||
this.$message.warning('请至少添加一条货物信息');
|
||
return false;
|
||
}
|
||
for (const [index, cargo] of this.transportCargoRows.entries()) {
|
||
const requiredFields = [
|
||
['cargoName', '货物名称'],
|
||
['cargoType', '货物类型'],
|
||
['quantity', '数量'],
|
||
['quantityUnit', '数量单位'],
|
||
];
|
||
const emptyField = requiredFields.find(([prop]) => this.isBillingFieldEmpty(cargo[prop]));
|
||
if (emptyField) {
|
||
this.$message.warning(`第${index + 1}行${emptyField[1]}不能为空`);
|
||
return false;
|
||
}
|
||
const invalidField = [['quantity', '数量']].find(([prop]) => {
|
||
const value = cargo[prop];
|
||
return value !== undefined && value !== null && value !== '' && Number(value) < 0;
|
||
});
|
||
if (invalidField) {
|
||
this.$message.warning(`第${index + 1}行${invalidField[1]}不能小于0`);
|
||
return false;
|
||
}
|
||
const invalidPrice = Number(cargo.unitPrice || 0) < 0;
|
||
if (invalidPrice) {
|
||
this.$message.warning(`第${index + 1}行单价不能小于0`);
|
||
return false;
|
||
}
|
||
if (!this.isBillingFieldEmpty(cargo.freightAmount) && Number(cargo.freightAmount) < 0) {
|
||
this.$message.warning(`第${index + 1}行运费不能小于0`);
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
},
|
||
validateTaskInfoForm(row = {}) {
|
||
if (this.isTaskFullMode && !this.validateTaskCargoRows()) {
|
||
return false;
|
||
}
|
||
if (this.isTaskFullMode && !this.taskFreightCurrency) {
|
||
this.$message.warning('请选择币种');
|
||
return false;
|
||
}
|
||
const requiredFields = this.config.transportFormRequiredFields || [
|
||
['carrierType', '承运类型'],
|
||
['vehicleNo', this.transportVehicleNoLabel],
|
||
];
|
||
if (this.isTaskCarrierRequired(row.carrierType)) {
|
||
requiredFields.push(['carrierName', '承运商']);
|
||
}
|
||
if (row.carrierType === '承运商') {
|
||
requiredFields.push(['carrierContractId', '承运商合同']);
|
||
}
|
||
if (this.isRoadTransport && row.carrierType === '自运') {
|
||
requiredFields.push(['driverName', '司机']);
|
||
requiredFields.push(['driverPhone', '手机号']);
|
||
}
|
||
const emptyField = requiredFields.find(([prop]) => this.isBillingFieldEmpty(row[prop]));
|
||
if (emptyField) {
|
||
this.$message.warning(`请选择或输入${emptyField[1]}`);
|
||
return false;
|
||
}
|
||
const numberFields = [
|
||
['quantity', '数量'],
|
||
['mileage', '里程'],
|
||
['otherFeeTotal', '其他费用合计'],
|
||
];
|
||
if (!this.isTaskFullMode) numberFields.push(['unitPrice', '单价']);
|
||
const invalidField = numberFields.find(([prop]) => {
|
||
const value = row[prop];
|
||
return value !== undefined && value !== null && value !== '' && Number(value) < 0;
|
||
});
|
||
if (invalidField) {
|
||
this.$message.warning(`${invalidField[1]}不能小于0`);
|
||
return false;
|
||
}
|
||
if (this.isRoadTransport) {
|
||
const invalidLengthField = [
|
||
['driverName', '司机', 20],
|
||
['driverPhone', '手机号', 20],
|
||
['vehicleNo', '车牌号', 30],
|
||
['trailerVehicleNo', '挂车车牌号', 30],
|
||
['escortName', '押运人', 20],
|
||
['escortPhone', '押运人手机号', 20],
|
||
].find(([prop, , max]) => String(row[prop] || '').length > max);
|
||
if (invalidLengthField) {
|
||
this.$message.warning(`${invalidLengthField[1]}不能超过${invalidLengthField[2]}个字符`);
|
||
return false;
|
||
}
|
||
} else {
|
||
const invalidLengthField = [
|
||
['vehicleNo', '船/航/班列号', 30],
|
||
['captainName', '船长', 20],
|
||
['cabinNo', '舱位', 30],
|
||
['containerNo', '箱号', 30],
|
||
].find(([prop, , max]) => String(row[prop] || '').length > max);
|
||
if (invalidLengthField) {
|
||
this.$message.warning(`${invalidLengthField[1]}不能超过${invalidLengthField[2]}个字符`);
|
||
return false;
|
||
}
|
||
}
|
||
if (row.mileage && (!/^\d{1,10}$/.test(String(row.mileage)) || Number(row.mileage) <= 0)) {
|
||
this.$message.warning('里程必须为不超过10位的正整数');
|
||
return false;
|
||
}
|
||
if (
|
||
row.estimatedStartTime &&
|
||
row.estimatedEndTime &&
|
||
row.estimatedEndTime < row.estimatedStartTime
|
||
) {
|
||
this.$message.warning('预计完成日期不能早于预计发货日期');
|
||
return false;
|
||
}
|
||
if (row.taskRemark && row.taskRemark.length > 200) {
|
||
this.$message.warning('任务备注不能超过200个字符');
|
||
return false;
|
||
}
|
||
return true;
|
||
},
|
||
syncTaskInfoToRow(row) {
|
||
row.taskEntryMode = row.taskEntryMode || 'simple';
|
||
['quantity', 'mileage', 'unitPrice'].forEach(prop => {
|
||
if (this.isBillingFieldEmpty(row[prop]) || Number(row[prop]) === -1) row[prop] = '';
|
||
});
|
||
const taskInfo = {
|
||
taskEntryMode: row.taskEntryMode,
|
||
carrierType: row.carrierType,
|
||
carrierId: row.carrierId,
|
||
carrierName: row.carrierName,
|
||
carrierContractId: row.carrierContractId,
|
||
driverId: row.driverId,
|
||
driverName: row.driverName,
|
||
driverPhone: row.driverPhone,
|
||
vehicleNo: row.vehicleNo,
|
||
captainName: row.captainName,
|
||
cabinNo: row.cabinNo,
|
||
containerNo: row.containerNo,
|
||
trailerVehicleNo: row.trailerVehicleNo,
|
||
escortName: row.escortName,
|
||
escortPhone: row.escortPhone,
|
||
mileage: row.mileage,
|
||
estimatedStartTime: row.estimatedStartTime,
|
||
estimatedEndTime: row.estimatedEndTime,
|
||
unitPrice: row.unitPrice,
|
||
priceUnit: row.priceUnit,
|
||
taskRemark: row.taskRemark,
|
||
};
|
||
if (!this.isBillingFieldEmpty(row.otherFeeTotal) && Number(row.otherFeeTotal) !== -1) {
|
||
taskInfo.otherFeeTotal = row.otherFeeTotal;
|
||
} else {
|
||
delete row.otherFeeTotal;
|
||
}
|
||
row.carrierJson = JSON.stringify([
|
||
{
|
||
carrierType: row.carrierType,
|
||
carrierId: row.carrierId,
|
||
carrierName: row.carrierName,
|
||
carrierContractId: row.carrierContractId,
|
||
driverId: row.driverId,
|
||
driverName: row.driverName,
|
||
driverPhone: row.driverPhone,
|
||
vehicleNo: row.vehicleNo,
|
||
captainName: row.captainName,
|
||
cabinNo: row.cabinNo,
|
||
containerNo: row.containerNo,
|
||
trailerVehicleNo: row.trailerVehicleNo,
|
||
escortName: row.escortName,
|
||
escortPhone: row.escortPhone,
|
||
mileage: row.mileage,
|
||
taskEntryMode: row.taskEntryMode,
|
||
},
|
||
]);
|
||
const goodsRows = this.isTaskFullMode
|
||
? this.transportCargoRows.map(item => this.normalizeTransportCargoRow(item))
|
||
: [
|
||
{
|
||
cargoType: row.cargoType,
|
||
cargoTypeCode: row.cargoTypeCode,
|
||
cargoTypePath: row.cargoTypePath,
|
||
cargoName: row.cargoName,
|
||
specification: row.specification,
|
||
model: row.model,
|
||
quantity: row.quantity,
|
||
quantityUnit: row.quantityUnit,
|
||
mileage: row.mileage,
|
||
unitPrice: row.unitPrice,
|
||
priceUnit: row.priceUnit,
|
||
remark: row.taskRemark,
|
||
},
|
||
];
|
||
if (this.isTaskFullMode) {
|
||
const firstCargo = goodsRows[0] || {};
|
||
taskInfo.unitPrice = firstCargo.unitPrice || '';
|
||
taskInfo.priceUnit = firstCargo.priceUnit || '';
|
||
row.freightJson = this.serializeTaskFullFreight();
|
||
}
|
||
row.taskInfoJson = JSON.stringify(taskInfo);
|
||
row.goodsJson = JSON.stringify(goodsRows);
|
||
const firstGoodsRow =
|
||
goodsRows.find(
|
||
item =>
|
||
!this.isBillingFieldEmpty(item.cargoName) || !this.isBillingFieldEmpty(item.cargoType)
|
||
) ||
|
||
goodsRows[0] ||
|
||
{};
|
||
row.cargoType = firstGoodsRow.cargoType || row.cargoType || '';
|
||
row.cargoTypeCode = firstGoodsRow.cargoTypeCode || row.cargoTypeCode || '';
|
||
row.cargoTypePath = firstGoodsRow.cargoTypePath || row.cargoTypePath || [];
|
||
row.cargoName = firstGoodsRow.cargoName || row.cargoName || '';
|
||
row.specification = firstGoodsRow.specification || row.specification || '';
|
||
row.model = firstGoodsRow.model || row.model || '';
|
||
row.quantity = firstGoodsRow.quantity || row.quantity || '';
|
||
row.quantityUnit = firstGoodsRow.quantityUnit || row.quantityUnit || '';
|
||
row.mileage = firstGoodsRow.mileage || row.mileage || '';
|
||
row.unitPrice = firstGoodsRow.unitPrice || (this.isTaskFullMode ? '' : row.unitPrice) || '';
|
||
row.priceUnit = firstGoodsRow.priceUnit || (this.isTaskFullMode ? '' : row.priceUnit) || '';
|
||
row.taskRemark = firstGoodsRow.remark || row.taskRemark || '';
|
||
},
|
||
serializeTaskFullFreight() {
|
||
return JSON.stringify({
|
||
currency: this.taskFreightCurrency || 'RMB',
|
||
totalFreightAmount: this.taskFullFreightTotal,
|
||
otherFreightAmount: this.form.otherFeeTotal || '',
|
||
freightItems: this.transportCargoRows.map((cargo, index) => ({
|
||
cargoIndex: index,
|
||
cargoName: cargo.cargoName || '',
|
||
cargoType: cargo.cargoType || '',
|
||
unitPrice: cargo.unitPrice || '',
|
||
priceUnit: cargo.priceUnit || '',
|
||
quantity: this.taskFullFreightQuantity(index),
|
||
quantityUnit: cargo.quantityUnit || '',
|
||
freightAmount: this.taskFullFreightAmount(cargo),
|
||
})),
|
||
});
|
||
},
|
||
initWaybillCargoRows() {
|
||
const goodsRows = this.parseJsonArray(this.form.goodsJson);
|
||
this.transportCargoRows = goodsRows.map(row => this.normalizeTransportCargoRow(row));
|
||
if (!this.transportCargoRows.length) {
|
||
this.transportCargoRows.push(this.normalizeTransportCargoRow());
|
||
}
|
||
if (this.hasMultipleConfiguredGoods(this.form, this.transportCargoRows)) {
|
||
this.form.taskEntryMode = 'full';
|
||
}
|
||
this.syncTransportCargoJson();
|
||
this.syncCurrentContractOption();
|
||
this.restoreTransportCargoTypePaths();
|
||
},
|
||
restoreTransportCargoTypePaths() {
|
||
if (!this.transportCargoRows.length) return Promise.resolve();
|
||
return this.ensureCargoTypeOptions().then(() => {
|
||
this.transportCargoRows.forEach(row => {
|
||
if (this.normalizeCargoTypePath(row.cargoTypePath).length) return;
|
||
const cargoType = this.findCargoTypeByCodeOrName({
|
||
cargoTypeCode: row.cargoTypeCode,
|
||
cargoType: row.cargoType,
|
||
});
|
||
if (!cargoType?.path) return;
|
||
row.cargoTypePath = cargoType.path;
|
||
row.cargoType = this.getCargoTypePathLabels(cargoType.path).at(-1) || row.cargoType;
|
||
row.cargoTypeCode = cargoType.cargoCode || cargoType.code || row.cargoTypeCode;
|
||
});
|
||
this.syncTransportCargoJson();
|
||
});
|
||
},
|
||
normalizeTransportCargoRow(row = {}) {
|
||
const cargo = {
|
||
...defaultTransportCargo(),
|
||
...row,
|
||
cargoTypePath: this.normalizeCargoTypePath(row.cargoTypePath),
|
||
cargoType:
|
||
row.cargoType || row.goodsType || row.secondCargoTypeName || row.firstCargoTypeName || '',
|
||
cargoTypeCode:
|
||
row.cargoTypeCode ||
|
||
row.goodsTypeCode ||
|
||
row.secondCargoTypeCode ||
|
||
row.firstCargoTypeCode ||
|
||
'',
|
||
cargoName: row.cargoName || row.goodsName || '',
|
||
packageType: row.packageType || row.package || '',
|
||
specification: row.specification || row.spec || '',
|
||
freightAmount: row.freightAmount ?? row.amount ?? row.totalAmount ?? '',
|
||
};
|
||
['quantity', 'mileage', 'unitPrice'].forEach(prop => {
|
||
if (Number(cargo[prop]) === -1) cargo[prop] = '';
|
||
});
|
||
return cargo;
|
||
},
|
||
hasMultipleConfiguredGoods(source = {}, rows = []) {
|
||
const configuredRows = this.parseJsonArray(
|
||
source?.goodsJson || source?.goodsRows || source?.goodsList
|
||
);
|
||
const list = configuredRows.length ? configuredRows : Array.isArray(rows) ? rows : [];
|
||
return list.length > 1;
|
||
},
|
||
syncTransportCargoJson() {
|
||
if (!this.isTaskFullMode) return;
|
||
this.form.goodsJson = JSON.stringify(
|
||
this.transportCargoRows.map(row => this.normalizeTransportCargoRow(row))
|
||
);
|
||
},
|
||
addTransportCargoRow(index = -1, row = {}) {
|
||
const nextRow = this.normalizeTransportCargoRow({
|
||
priceUnit: this.form.priceUnit || '元/吨',
|
||
...row,
|
||
});
|
||
if (
|
||
index < 0 &&
|
||
this.transportCargoRows.length === 1 &&
|
||
this.isEmptyTransportCargoRow(this.transportCargoRows[0])
|
||
) {
|
||
this.transportCargoRows.splice(0, 1, nextRow);
|
||
} else if (index > -1) {
|
||
this.transportCargoRows.splice(index + 1, 0, nextRow);
|
||
} else {
|
||
this.transportCargoRows.push(nextRow);
|
||
}
|
||
this.syncTransportCargoJson();
|
||
},
|
||
isEmptyTransportCargoRow(row = {}) {
|
||
return ![
|
||
row.cargoName,
|
||
row.cargoType,
|
||
row.cargoTypeCode,
|
||
row.quantity,
|
||
row.quantityUnit,
|
||
row.packageType,
|
||
row.brand,
|
||
row.specification,
|
||
row.model,
|
||
row.materialCode,
|
||
row.deviceCode,
|
||
row.remark,
|
||
].some(value => !this.isBillingFieldEmpty(value));
|
||
},
|
||
removeTransportCargoRow(index) {
|
||
this.transportCargoRows.splice(index, 1);
|
||
if (!this.transportCargoRows.length && this.isTaskFullMode) {
|
||
this.transportCargoRows.push(this.normalizeTransportCargoRow());
|
||
}
|
||
this.syncTransportCargoJson();
|
||
},
|
||
handleTransportCargoQuantityInput(row, value) {
|
||
const text = String(value || '').replace(/[^\d.]/g, '');
|
||
const parts = text.split('.');
|
||
row.quantity =
|
||
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 3)}` : parts[0];
|
||
this.syncTransportCargoJson();
|
||
},
|
||
handleTransportCargoTypeChange(row, value) {
|
||
const path = this.normalizeCargoTypePath(value);
|
||
const cargoType = this.findCargoTypeByPath(path);
|
||
const labels = this.getCargoTypePathLabels(path);
|
||
row.cargoTypePath = path;
|
||
row.cargoType = labels.length ? labels[labels.length - 1] : '';
|
||
row.cargoTypeCode = cargoType?.cargoCode || cargoType?.code || cargoType?.id || '';
|
||
row.cargoName = '';
|
||
row.specification = '';
|
||
row.model = '';
|
||
this.syncTransportCargoJson();
|
||
this.loadTaskCargoOptionsByPath(path);
|
||
},
|
||
handleTransportTypeChange(value) {
|
||
this.form.transportTypeName = this.getTransportTypeLabel(value);
|
||
this.clearTransportShippingInfo();
|
||
},
|
||
clearTransportShippingInfo() {
|
||
[
|
||
'departureName',
|
||
'departureAddress',
|
||
'departureAddressCode',
|
||
'departureSiteCode',
|
||
'departureLongitude',
|
||
'departureLatitude',
|
||
'departureContact',
|
||
'departurePhone',
|
||
'arrivalName',
|
||
'arrivalAddress',
|
||
'arrivalAddressCode',
|
||
'arrivalSiteCode',
|
||
'arrivalLongitude',
|
||
'arrivalLatitude',
|
||
'arrivalContact',
|
||
'arrivalPhone',
|
||
].forEach(prop => {
|
||
this.form[prop] = '';
|
||
});
|
||
this.transportRouteSelected = null;
|
||
this.transportAddressSelected = null;
|
||
this.transportMapSelected = {};
|
||
this.transportRouteBox = false;
|
||
this.transportAddressBox = false;
|
||
this.transportStationBox = false;
|
||
this.transportMapBox = false;
|
||
this.transportAddressTarget = '';
|
||
this.transportStationTarget = '';
|
||
this.transportMapTarget = '';
|
||
this.transportMapKeyword = '';
|
||
this.transportMapStatus = '可搜索地址或点击地图选点';
|
||
if (this.transportAmapMarker) {
|
||
this.transportAmapMarker.setMap(null);
|
||
this.transportAmapMarker = null;
|
||
}
|
||
this.$refs.crud?.clearValidate?.([
|
||
'departureAddress',
|
||
'arrivalAddress',
|
||
'departureName',
|
||
'arrivalName',
|
||
'departureContact',
|
||
'departurePhone',
|
||
'arrivalContact',
|
||
'arrivalPhone',
|
||
]);
|
||
},
|
||
getTransportTypeLabel(value) {
|
||
if (value && typeof value === 'object') {
|
||
return value.label || value.dictValue || value.name || value.value || value.dictKey || '';
|
||
}
|
||
const column = (this.option.column || []).find(item => item.prop === 'transportType');
|
||
const dictionary = [...(column?.dicData || []), ...this.transportTypeOptions];
|
||
const item = dictionary.find(
|
||
dic =>
|
||
String(dic.value ?? dic.dictKey ?? '') === String(value ?? '') ||
|
||
String(dic.dictKey ?? '') === String(value ?? '')
|
||
);
|
||
return item?.label || item?.dictValue || '';
|
||
},
|
||
loadTransportTypeOptions() {
|
||
if (this.transportTypeOptions.length || this.transportTypeOptionsLoading) {
|
||
return Promise.resolve(this.transportTypeOptions);
|
||
}
|
||
this.transportTypeOptionsLoading = true;
|
||
return getDictionary({ code: 'transport_type' })
|
||
.then(res => {
|
||
this.transportTypeOptions = this.normalizeDictOptions(res.data?.data || []);
|
||
return this.transportTypeOptions;
|
||
})
|
||
.finally(() => {
|
||
this.transportTypeOptionsLoading = false;
|
||
});
|
||
},
|
||
resolveTransportMode(value) {
|
||
const values = [value, this.form.transportTypeName, this.getTransportTypeLabel(value)]
|
||
.flatMap(item => {
|
||
if (!item || typeof item !== 'object') return [item];
|
||
return [item.label, item.dictValue, item.name, item.value, item.dictKey];
|
||
})
|
||
.filter(Boolean);
|
||
const text = values.join(' ').toLowerCase();
|
||
if (!text) return '';
|
||
if (
|
||
text.includes('公路') ||
|
||
text.includes('道路') ||
|
||
text.includes('road') ||
|
||
text.includes('highway') ||
|
||
text === 'gl'
|
||
) {
|
||
return 'road';
|
||
}
|
||
if (
|
||
text.includes('铁路') ||
|
||
text.includes('rail') ||
|
||
text.includes('train') ||
|
||
text === 'tl'
|
||
) {
|
||
return 'rail';
|
||
}
|
||
if (
|
||
text.includes('水路') ||
|
||
text.includes('水运') ||
|
||
text.includes('海运') ||
|
||
text.includes('港') ||
|
||
text.includes('water') ||
|
||
text.includes('ship') ||
|
||
text.includes('sea') ||
|
||
text === 'sl'
|
||
) {
|
||
return 'water';
|
||
}
|
||
if (
|
||
text.includes('航空') ||
|
||
text.includes('空运') ||
|
||
text.includes('空港') ||
|
||
text.includes('air') ||
|
||
text.includes('airport') ||
|
||
text === 'hk'
|
||
) {
|
||
return 'air';
|
||
}
|
||
return '';
|
||
},
|
||
openTransportPrimaryAddressPicker(target) {
|
||
if (this.transportStationMode) {
|
||
this.openTransportStationDialog(target);
|
||
}
|
||
},
|
||
openTransportSecondaryAddressPicker(target) {
|
||
if (this.transportStationMode) {
|
||
this.openTransportStationDialog(target);
|
||
return;
|
||
}
|
||
this.openTransportMapDialog(target);
|
||
},
|
||
handleTransportSecondaryAddressInputClick(target) {
|
||
if (this.transportStationMode || !this.config.editableRoadAddress) {
|
||
this.openTransportSecondaryAddressPicker(target);
|
||
}
|
||
},
|
||
isBillingFieldEmpty(value) {
|
||
return value === undefined || value === null || String(value).trim() === '';
|
||
},
|
||
ensureTransportTypeBeforeAddress() {
|
||
if (!this.shippingInfoFormEnabled || !this.isBillingFieldEmpty(this.form.transportType)) {
|
||
return true;
|
||
}
|
||
this.$message.warning('请先选择运输方式');
|
||
return false;
|
||
},
|
||
openTransportRouteDialog() {
|
||
if (!this.ensureTransportTypeBeforeAddress()) return;
|
||
this.transportRouteSelected = null;
|
||
this.transportRoutePage.currentPage = 1;
|
||
this.transportRouteQuery = {};
|
||
this.transportRouteBox = true;
|
||
},
|
||
loadTransportRouteList() {
|
||
this.transportRouteLoading = true;
|
||
getCommonRouteList(this.transportRoutePage.currentPage, this.transportRoutePage.pageSize, {
|
||
...this.transportRouteQuery,
|
||
allDept: 0,
|
||
})
|
||
.then(res => {
|
||
const data = res.data.data || {};
|
||
this.transportRouteRows = data.records || [];
|
||
this.transportRoutePage.total = data.total || 0;
|
||
})
|
||
.finally(() => {
|
||
this.transportRouteLoading = false;
|
||
});
|
||
},
|
||
handleTransportRouteSearch() {
|
||
this.transportRoutePage.currentPage = 1;
|
||
this.loadTransportRouteList();
|
||
},
|
||
handleTransportRouteReset() {
|
||
this.transportRouteQuery = {};
|
||
this.transportRoutePage.currentPage = 1;
|
||
this.loadTransportRouteList();
|
||
},
|
||
handleTransportRouteSizeChange(pageSize) {
|
||
this.transportRoutePage.pageSize = pageSize;
|
||
this.transportRoutePage.currentPage = 1;
|
||
this.loadTransportRouteList();
|
||
},
|
||
handleTransportRouteCurrentChange(currentPage) {
|
||
this.transportRoutePage.currentPage = currentPage;
|
||
this.loadTransportRouteList();
|
||
},
|
||
formatTransportRouteUpdateUserName(row) {
|
||
return formatUpdateUserName(row);
|
||
},
|
||
selectTransportRouteRow(row) {
|
||
this.transportRouteSelected = row;
|
||
this.applyTransportRoute(row);
|
||
this.transportRouteBox = false;
|
||
},
|
||
applyTransportRoute(route = {}) {
|
||
this.applyTransportAddress('departure', {
|
||
addressName: route.departureName,
|
||
detailAddress: route.departureAddress,
|
||
longitude: route.departureLongitude,
|
||
latitude: route.departureLatitude,
|
||
contactName: route.departureContact,
|
||
contactPhone: route.departurePhone,
|
||
addressCode: route.departureAddressCode,
|
||
siteCode: route.departureSiteCode,
|
||
});
|
||
this.applyTransportAddress('arrival', {
|
||
addressName: route.arrivalName,
|
||
detailAddress: route.arrivalAddress,
|
||
longitude: route.arrivalLongitude,
|
||
latitude: route.arrivalLatitude,
|
||
contactName: route.arrivalContact,
|
||
contactPhone: route.arrivalPhone,
|
||
addressCode: route.arrivalAddressCode,
|
||
siteCode: route.arrivalSiteCode,
|
||
});
|
||
},
|
||
openTransportStationDialog(target) {
|
||
if (!this.ensureTransportTypeBeforeAddress()) return;
|
||
if (!this.transportStationMode) return;
|
||
this.transportStationTarget = target;
|
||
this.transportStationQuery = {};
|
||
this.transportStationPage.currentPage = 1;
|
||
this.transportStationBox = true;
|
||
},
|
||
getTransportStationListRequest() {
|
||
const requestMap = {
|
||
water: getPortTerminalList,
|
||
rail: getRailwayStationList,
|
||
air: getAirportMasterList,
|
||
};
|
||
return requestMap[this.transportMode];
|
||
},
|
||
loadTransportStationList() {
|
||
const request = this.getTransportStationListRequest();
|
||
if (!request) return;
|
||
this.transportStationLoading = true;
|
||
request(
|
||
this.transportStationPage.currentPage,
|
||
this.transportStationPage.pageSize,
|
||
this.buildTransportStationQueryParams()
|
||
)
|
||
.then(res => {
|
||
const data = res.data.data || {};
|
||
this.transportStationRows = (data.records || []).map(row =>
|
||
this.normalizeTransportStationRow(row)
|
||
);
|
||
this.transportStationPage.total = data.total || 0;
|
||
})
|
||
.finally(() => {
|
||
this.transportStationLoading = false;
|
||
});
|
||
},
|
||
buildTransportStationQueryParams() {
|
||
const query = this.transportStationQuery || {};
|
||
return Object.keys(query).reduce((params, key) => {
|
||
const value = query[key];
|
||
if (value !== undefined && value !== null && value !== '') {
|
||
params[key] = value;
|
||
}
|
||
return params;
|
||
}, {});
|
||
},
|
||
handleTransportStationSearch() {
|
||
this.transportStationPage.currentPage = 1;
|
||
this.loadTransportStationList();
|
||
},
|
||
handleTransportStationReset() {
|
||
this.transportStationQuery = {};
|
||
this.transportStationPage.currentPage = 1;
|
||
this.loadTransportStationList();
|
||
},
|
||
handleTransportStationSizeChange(pageSize) {
|
||
this.transportStationPage.pageSize = pageSize;
|
||
this.transportStationPage.currentPage = 1;
|
||
this.loadTransportStationList();
|
||
},
|
||
handleTransportStationCurrentChange(currentPage) {
|
||
this.transportStationPage.currentPage = currentPage;
|
||
this.loadTransportStationList();
|
||
},
|
||
normalizeTransportStationRow(row = {}) {
|
||
const regionName =
|
||
row.regionName ||
|
||
[row.provinceName, row.cityName, row.districtName].filter(Boolean).join('');
|
||
return {
|
||
...row,
|
||
stationName: row.name || row.stationName || row.addressName || '',
|
||
stationType: row.category || this.transportStationTypeLabel,
|
||
stationCode: row.code || row.tmisCode || row.iataCode || row.icaoCode || row.siteCode || '',
|
||
detailAddress: row.detailAddress || row.address || '',
|
||
regionName,
|
||
};
|
||
},
|
||
selectTransportStationRow(row) {
|
||
const station = this.normalizeTransportStationRow(row);
|
||
this.applyTransportAddress(this.transportStationTarget, {
|
||
addressName: station.stationName,
|
||
addressType: this.transportStationTypeLabel,
|
||
detailAddress: station.detailAddress,
|
||
longitude: station.longitude,
|
||
latitude: station.latitude,
|
||
regionName: station.regionName,
|
||
regionCode: station.regionCode,
|
||
siteCode: station.stationCode,
|
||
siteCodeDisplay: station.stationCode,
|
||
remark: station.remark,
|
||
});
|
||
this.transportStationBox = false;
|
||
},
|
||
openTransportAddressDialog(target) {
|
||
if (!this.ensureTransportTypeBeforeAddress()) return;
|
||
const open = () => {
|
||
this.transportAddressTarget = target;
|
||
this.transportAddressSelected = null;
|
||
this.resetTransportAddressQuery();
|
||
this.transportAddressPage.currentPage = 1;
|
||
this.transportAddressBox = true;
|
||
};
|
||
open();
|
||
},
|
||
openTransportAddressPicker(target) {
|
||
if (this.transportStationMode) {
|
||
this.openTransportStationDialog(target);
|
||
return;
|
||
}
|
||
this.openTransportAddressDialog(target);
|
||
},
|
||
resetTransportAddressQuery() {
|
||
const addressType = this.getTransportFixedAddressType();
|
||
this.transportAddressQuery = addressType ? { addressType } : {};
|
||
},
|
||
getTransportFixedAddressType() {
|
||
if (!this.config.fixedTransportAddressType) return '';
|
||
const typeMap = {
|
||
road: '常规地址',
|
||
rail: '铁路车站',
|
||
water: '港口/码头',
|
||
air: '空港机场',
|
||
};
|
||
return typeMap[this.transportMode] || '';
|
||
},
|
||
loadTransportAddressList() {
|
||
this.transportAddressLoading = true;
|
||
getCommonAddressList(
|
||
this.transportAddressPage.currentPage,
|
||
this.transportAddressPage.pageSize,
|
||
{
|
||
...this.transportAddressQuery,
|
||
allDept: 0,
|
||
}
|
||
)
|
||
.then(res => {
|
||
const data = res.data.data || {};
|
||
this.transportAddressRows = data.records || [];
|
||
this.transportAddressPage.total = data.total || 0;
|
||
})
|
||
.finally(() => {
|
||
this.transportAddressLoading = false;
|
||
});
|
||
},
|
||
handleTransportAddressSearch() {
|
||
this.transportAddressPage.currentPage = 1;
|
||
this.loadTransportAddressList();
|
||
},
|
||
handleTransportAddressReset() {
|
||
this.resetTransportAddressQuery();
|
||
this.transportAddressPage.currentPage = 1;
|
||
this.loadTransportAddressList();
|
||
},
|
||
handleTransportAddressSizeChange(pageSize) {
|
||
this.transportAddressPage.pageSize = pageSize;
|
||
this.transportAddressPage.currentPage = 1;
|
||
this.loadTransportAddressList();
|
||
},
|
||
handleTransportAddressCurrentChange(currentPage) {
|
||
this.transportAddressPage.currentPage = currentPage;
|
||
this.loadTransportAddressList();
|
||
},
|
||
formatTransportAddressSiteCode(row) {
|
||
return row.addressType === '常规地址' ? '/' : row.siteCodeDisplay || row.siteCode || '';
|
||
},
|
||
selectTransportAddressRow(row) {
|
||
this.transportAddressSelected = row;
|
||
this.applyTransportAddress(this.transportAddressTarget, {
|
||
...row,
|
||
preferRegionName: true,
|
||
});
|
||
this.transportAddressBox = false;
|
||
},
|
||
applyTransportAddress(target, address = {}) {
|
||
const prefix = target === 'departure' ? 'departure' : 'arrival';
|
||
this.form[`${prefix}Name`] = address.preferRegionName
|
||
? address.regionName || address.addressName || ''
|
||
: address.addressName || address.regionName || '';
|
||
this.form[`${prefix}Address`] = address.detailAddress || address.address || '';
|
||
this.form[`${prefix}AddressCode`] = address.addressCode || '';
|
||
this.form[`${prefix}SiteCode`] =
|
||
address.addressType === '常规地址'
|
||
? '/'
|
||
: address.siteCodeDisplay || address.siteCode || '';
|
||
this.form[`${prefix}Longitude`] = address.longitude || '';
|
||
this.form[`${prefix}Latitude`] = address.latitude || '';
|
||
this.form[`${prefix}Contact`] = address.contactName || this.form[`${prefix}Contact`] || '';
|
||
this.form[`${prefix}Phone`] = address.contactPhone || this.form[`${prefix}Phone`] || '';
|
||
// 地址值在本轮响应式更新后再清理校验状态,避免 Avue 以旧空值重新触发必填提示。
|
||
this.$nextTick(() => {
|
||
this.$refs.crud?.clearValidate?.([`${prefix}Address`]);
|
||
});
|
||
},
|
||
openTransportMapDialog(target) {
|
||
if (this.dialogReadonly) return;
|
||
if (!this.ensureTransportTypeBeforeAddress()) return;
|
||
const prefix = target === 'departure' ? 'departure' : 'arrival';
|
||
this.transportMapTarget = prefix;
|
||
this.transportMapKeyword = this.form[`${prefix}Address`] || this.form[`${prefix}Name`] || '';
|
||
this.transportMapSelected = this.buildTransportMapSelection({
|
||
lng: this.form[`${prefix}Longitude`],
|
||
lat: this.form[`${prefix}Latitude`],
|
||
address: this.form[`${prefix}Address`],
|
||
regionName: this.form[`${prefix}Name`],
|
||
});
|
||
this.transportMapStatus = this.transportMapSelected.longitude
|
||
? '已加载当前选点,可重新选点'
|
||
: '可搜索地址或点击地图选点';
|
||
this.transportMapBox = true;
|
||
},
|
||
loadAmap() {
|
||
if (window.AMap && window.AMap.Map) {
|
||
return Promise.resolve();
|
||
}
|
||
if (!amapLoader) {
|
||
amapLoader = 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 amapLoader;
|
||
},
|
||
initTransportAmap() {
|
||
this.loadAmap()
|
||
.then(() => {
|
||
this.$nextTick(() => {
|
||
if (!this.transportAmap) {
|
||
const center = this.toLngLat(
|
||
this.transportMapSelected.longitude || 116.40769,
|
||
this.transportMapSelected.latitude || 39.89945
|
||
);
|
||
this.transportAmap = new window.AMap.Map(this.$refs.transportAmap, {
|
||
center,
|
||
zoom: this.transportMapSelected.longitude ? 14 : 11,
|
||
});
|
||
window.AMap.plugin(['AMap.ToolBar', 'AMap.Geocoder'], () => {
|
||
this.transportAmap.addControl(new window.AMap.ToolBar());
|
||
if (!this.transportAmapGeocoder) {
|
||
this.transportAmapGeocoder = new window.AMap.Geocoder();
|
||
}
|
||
});
|
||
this.transportAmap.on('click', event => this.pickTransportMapPoint(event.lnglat));
|
||
} else if (typeof this.transportAmap.resize === 'function') {
|
||
this.transportAmap.resize();
|
||
}
|
||
if (this.transportMapSelected.longitude) {
|
||
this.renderTransportMapMarker(
|
||
this.toLngLat(
|
||
this.transportMapSelected.longitude,
|
||
this.transportMapSelected.latitude
|
||
)
|
||
);
|
||
}
|
||
});
|
||
})
|
||
.catch(() => {
|
||
this.$message.error('高德地图组件加载失败,请稍后重试或重新打开弹窗');
|
||
this.transportMapBox = false;
|
||
});
|
||
},
|
||
searchTransportMapKeyword() {
|
||
const keyword = String(this.transportMapKeyword || '').trim();
|
||
if (!keyword) {
|
||
this.$message.warning('请输入地址关键词');
|
||
return;
|
||
}
|
||
this.loadAmap()
|
||
.then(() => {
|
||
this.transportMapLoading = true;
|
||
this.ensureTransportAmapGeocoder()
|
||
.then(() => this.runAmapGeocode('location', keyword))
|
||
.then(result => {
|
||
this.transportMapSearchResults = (result.geocodes || []).map((item, index) => ({
|
||
id: item.id || index,
|
||
name: item.formattedAddress || keyword,
|
||
address: item.formattedAddress || keyword,
|
||
location: item.location,
|
||
}));
|
||
const point = this.resolveMapPoint(result);
|
||
if (!point) {
|
||
this.transportMapStatus = '未找到匹配地址';
|
||
this.$message.warning('地图搜索无匹配地址');
|
||
return;
|
||
}
|
||
this.pickTransportMapPoint(point, keyword);
|
||
})
|
||
.catch(() => {
|
||
this.transportMapStatus = '地图搜索失败';
|
||
this.$message.error('地图搜索失败,请稍后重试');
|
||
})
|
||
.finally(() => {
|
||
this.transportMapLoading = false;
|
||
});
|
||
})
|
||
.catch(() => {
|
||
this.$message.error('高德地图组件加载失败,请稍后重试或重新打开弹窗');
|
||
});
|
||
},
|
||
selectTransportMapSearchResult(item) {
|
||
if (item && item.location) {
|
||
this.pickTransportMapPoint(item.location, item.address || item.name || '');
|
||
}
|
||
},
|
||
pickTransportMapPoint(lnglat, keyword) {
|
||
const longitude = this.getPointLng(lnglat);
|
||
const latitude = this.getPointLat(lnglat);
|
||
if (longitude === undefined || latitude === undefined) {
|
||
this.$message.warning('选点坐标无效');
|
||
return;
|
||
}
|
||
const point = this.toLngLat(longitude, latitude);
|
||
this.renderTransportMapMarker(point);
|
||
this.transportMapSelected = this.buildTransportMapSelection({
|
||
lng: longitude,
|
||
lat: latitude,
|
||
address: keyword,
|
||
});
|
||
this.transportMapStatus = '正在反查地址...';
|
||
this.ensureTransportAmapGeocoder()
|
||
.then(() => this.runAmapGeocode('address', point))
|
||
.then(result => {
|
||
const address = this.resolveMapAddress(result);
|
||
this.transportMapSelected = {
|
||
...this.transportMapSelected,
|
||
detailAddress: address.detailAddress || this.transportMapSelected.detailAddress,
|
||
regionName: address.regionName || this.transportMapSelected.regionName,
|
||
regionCode: address.regionCode || this.transportMapSelected.regionCode,
|
||
};
|
||
this.transportMapKeyword =
|
||
this.transportMapSelected.detailAddress || this.transportMapKeyword;
|
||
this.transportMapStatus = this.transportMapSelected.detailAddress || '已选点,可确认回填';
|
||
})
|
||
.catch(() => {
|
||
this.transportMapStatus = '反查地址失败';
|
||
this.$message.error('反查地址失败,请重新选点');
|
||
});
|
||
},
|
||
renderTransportMapMarker(point) {
|
||
if (!this.transportAmap || !window.AMap) return;
|
||
if (this.transportAmapMarker) {
|
||
this.transportAmapMarker.setMap(null);
|
||
}
|
||
this.transportAmapMarker = new window.AMap.Marker({
|
||
position: point,
|
||
});
|
||
this.transportAmapMarker.setMap(this.transportAmap);
|
||
this.transportAmap.setCenter(point);
|
||
},
|
||
confirmTransportMapPick() {
|
||
if (!this.transportMapSelected.longitude) {
|
||
this.$message.warning('请先搜索或点击地图完成选点');
|
||
return;
|
||
}
|
||
const prefix = this.transportMapTarget;
|
||
this.form[`${prefix}Address`] =
|
||
this.transportMapSelected.detailAddress || this.form[`${prefix}Address`];
|
||
this.form[`${prefix}Name`] =
|
||
this.transportMapSelected.regionName || this.form[`${prefix}Name`];
|
||
this.form[`${prefix}Longitude`] = this.transportMapSelected.longitude;
|
||
this.form[`${prefix}Latitude`] = this.transportMapSelected.latitude;
|
||
this.transportMapBox = false;
|
||
this.$nextTick(() => {
|
||
this.$refs.crud?.clearValidate?.([`${prefix}Address`]);
|
||
});
|
||
},
|
||
buildTransportMapSelection({ lng, lat, address, regionName, regionCode }) {
|
||
const longitude = this.formatCoordinate(lng);
|
||
const latitude = this.formatCoordinate(lat);
|
||
return {
|
||
longitude,
|
||
latitude,
|
||
detailAddress: address || '',
|
||
regionName: regionName || '',
|
||
regionCode: regionCode || '',
|
||
};
|
||
},
|
||
formatCoordinate(value) {
|
||
if (value === undefined || value === null || value === '') return '';
|
||
const numberValue = Number(value);
|
||
return Number.isFinite(numberValue) ? numberValue.toFixed(6) : '';
|
||
},
|
||
toLngLat(lng, lat) {
|
||
return new window.AMap.LngLat(Number(lng), Number(lat));
|
||
},
|
||
getPointLng(point) {
|
||
if (!point) return undefined;
|
||
if (typeof point.getLng === 'function') return point.getLng();
|
||
return point.lng ?? point.lon;
|
||
},
|
||
getPointLat(point) {
|
||
if (!point) return undefined;
|
||
if (typeof point.getLat === 'function') return point.getLat();
|
||
return point.lat;
|
||
},
|
||
ensureTransportAmapGeocoder() {
|
||
if (this.transportAmapGeocoder) {
|
||
return Promise.resolve(this.transportAmapGeocoder);
|
||
}
|
||
return new Promise((resolve, reject) => {
|
||
if (!window.AMap || !window.AMap.plugin) {
|
||
reject(new Error('高德地图组件未就绪'));
|
||
return;
|
||
}
|
||
window.AMap.plugin(['AMap.Geocoder'], () => {
|
||
try {
|
||
this.transportAmapGeocoder = new window.AMap.Geocoder();
|
||
resolve(this.transportAmapGeocoder);
|
||
} catch (error) {
|
||
reject(error);
|
||
}
|
||
});
|
||
});
|
||
},
|
||
runAmapGeocode(action, input) {
|
||
return new Promise((resolve, reject) => {
|
||
const timer = window.setTimeout(() => {
|
||
reject(new Error('高德地图请求超时'));
|
||
}, 10000);
|
||
const done = (status, result) => {
|
||
window.clearTimeout(timer);
|
||
if (status === 'complete' && result) {
|
||
resolve(result);
|
||
return;
|
||
}
|
||
reject(new Error('高德地图请求失败'));
|
||
};
|
||
try {
|
||
if (action === 'location') {
|
||
this.transportAmapGeocoder.getLocation(input, done);
|
||
} else {
|
||
this.transportAmapGeocoder.getAddress(input, done);
|
||
}
|
||
} catch (error) {
|
||
window.clearTimeout(timer);
|
||
reject(error);
|
||
}
|
||
});
|
||
},
|
||
resolveMapPoint(result) {
|
||
if (!result) return null;
|
||
const geocode = Array.isArray(result.geocodes) ? result.geocodes[0] : null;
|
||
if (geocode && geocode.location) return geocode.location;
|
||
if (result.location) return result.location;
|
||
if (result.lnglat) return result.lnglat;
|
||
if (result.getLng || result.lng || result.lon) return result;
|
||
return null;
|
||
},
|
||
resolveMapAddress(result = {}) {
|
||
const regeocode = result.regeocode || {};
|
||
const component = regeocode.addressComponent || {};
|
||
const city = Array.isArray(component.city) ? '' : component.city;
|
||
const regionName = [component.province, city, component.district].filter(Boolean).join('');
|
||
return {
|
||
detailAddress: regeocode.formattedAddress || '',
|
||
regionName,
|
||
regionCode: component.adcode || '',
|
||
};
|
||
},
|
||
openCommonCargoDialog() {
|
||
this.commonCargoSelected = [];
|
||
this.commonCargoQuery = {};
|
||
this.commonCargoPage.currentPage = 1;
|
||
this.commonCargoBox = true;
|
||
},
|
||
loadCommonCargoList() {
|
||
this.commonCargoSelected = [];
|
||
this.commonCargoLoading = true;
|
||
getCommonCargoList(this.commonCargoPage.currentPage, this.commonCargoPage.pageSize, {
|
||
...this.buildCommonCargoQueryParams(),
|
||
allDept: 0,
|
||
})
|
||
.then(res => {
|
||
const data = res.data.data || {};
|
||
this.commonCargoRows = data.records || [];
|
||
this.commonCargoPage.total = data.total || 0;
|
||
})
|
||
.finally(() => {
|
||
this.commonCargoLoading = false;
|
||
});
|
||
},
|
||
buildCommonCargoQueryParams() {
|
||
const { cargoTypePath, ...query } = this.commonCargoQuery || {};
|
||
return Object.keys(query).reduce((params, key) => {
|
||
const value = query[key];
|
||
if (value !== undefined && value !== null && value !== '') {
|
||
params[key] = value;
|
||
}
|
||
return params;
|
||
}, {});
|
||
},
|
||
handleCommonCargoSearch() {
|
||
this.commonCargoPage.currentPage = 1;
|
||
this.loadCommonCargoList();
|
||
},
|
||
handleCommonCargoReset() {
|
||
this.commonCargoQuery = {};
|
||
this.commonCargoPage.currentPage = 1;
|
||
this.loadCommonCargoList();
|
||
},
|
||
handleCommonCargoSizeChange(pageSize) {
|
||
this.commonCargoPage.pageSize = pageSize;
|
||
this.commonCargoPage.currentPage = 1;
|
||
this.loadCommonCargoList();
|
||
},
|
||
handleCommonCargoCurrentChange(currentPage) {
|
||
this.commonCargoPage.currentPage = currentPage;
|
||
this.loadCommonCargoList();
|
||
},
|
||
handleCommonCargoSelectionChange(rows) {
|
||
this.commonCargoSelected = rows || [];
|
||
},
|
||
handleCommonCargoTypeChange(value) {
|
||
const path = this.normalizeCargoTypePath(value);
|
||
const labels = this.getCargoTypePathLabels(path);
|
||
const firstCargoType = this.cargoTypeFlatOptions.find(
|
||
item => String(item.id) === String(path[0] || '') && item.path?.length === 1
|
||
);
|
||
const secondCargoType = this.findCargoTypeByPath(path);
|
||
this.commonCargoQuery = {
|
||
...this.commonCargoQuery,
|
||
cargoTypePath: path,
|
||
firstCargoTypeName: labels[0] || '',
|
||
firstCargoTypeCode: firstCargoType?.cargoCode || firstCargoType?.code || '',
|
||
secondCargoTypeName: labels[1] || '',
|
||
secondCargoTypeCode: secondCargoType?.cargoCode || secondCargoType?.code || '',
|
||
};
|
||
if (!path.length) {
|
||
delete this.commonCargoQuery.firstCargoTypeName;
|
||
delete this.commonCargoQuery.firstCargoTypeCode;
|
||
delete this.commonCargoQuery.secondCargoTypeName;
|
||
delete this.commonCargoQuery.secondCargoTypeCode;
|
||
}
|
||
},
|
||
formatCommonCargoSize(row) {
|
||
return row.cargoSize || row.size || row.dimension || row.dimensions || '';
|
||
},
|
||
async selectCommonCargoRow(row) {
|
||
this.commonCargoSelected = [row];
|
||
await this.ensureCargoTypeOptions();
|
||
const cargo = this.mapCommonCargoRow(row);
|
||
this.addTransportCargoRow(-1, cargo);
|
||
this.commonCargoBox = false;
|
||
},
|
||
async confirmCommonCargoSelection() {
|
||
if (!this.commonCargoSelected.length) return;
|
||
await this.ensureCargoTypeOptions();
|
||
this.commonCargoSelected.forEach(row => {
|
||
this.addTransportCargoRow(-1, this.mapCommonCargoRow(row));
|
||
});
|
||
this.$message.success(`成功选择${this.commonCargoSelected.length}条常用货物`);
|
||
this.commonCargoBox = false;
|
||
},
|
||
mapCommonCargoRow(row = {}) {
|
||
const cargoTypePath = this.resolveCommonCargoTypePath(row);
|
||
const cargoType = this.findCargoTypeByPath(cargoTypePath);
|
||
const labels = this.getCargoTypePathLabels(cargoTypePath);
|
||
return {
|
||
cargoName: row.cargoName,
|
||
cargoTypePath,
|
||
cargoType: labels[1] || row.secondCargoTypeName || row.firstCargoTypeName,
|
||
cargoTypeCode:
|
||
cargoType?.cargoCode ||
|
||
cargoType?.code ||
|
||
row.secondCargoTypeCode ||
|
||
row.firstCargoTypeCode,
|
||
packageType: row.packageType,
|
||
brand: row.brand,
|
||
specification: row.specification,
|
||
model: row.model,
|
||
remark: row.remark || row.descriptionOne || '',
|
||
};
|
||
},
|
||
async handleCargoImportFileChange(file) {
|
||
const rawFile = file.raw;
|
||
if (!rawFile) return;
|
||
if (!/\.(xls|xlsx)$/i.test(rawFile.name || '')) {
|
||
this.$message.warning('请上传 .xlsx、.xls 格式文件');
|
||
return;
|
||
}
|
||
if (rawFile.size > 50 * 1024 * 1024) {
|
||
this.$message.warning('单个文件大小不能超过50M');
|
||
return;
|
||
}
|
||
this.cargoImportLoading = true;
|
||
try {
|
||
const res = await importBlob('/blade-transport/shipping-template/import-goods', rawFile);
|
||
const contentType = res.headers['content-type'] || res.data.type || '';
|
||
if (contentType.includes('application/vnd.ms-excel')) {
|
||
downloadXls(
|
||
res.data,
|
||
`运单货物导入失败明细${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`
|
||
);
|
||
this.$message.warning('部分数据导入失败,已下载失败明细');
|
||
return;
|
||
}
|
||
const result = JSON.parse(await res.data.text());
|
||
if (result.code !== 200) throw new Error(result.msg || '货物导入失败');
|
||
const rows = result.data || [];
|
||
if (!rows.length) {
|
||
this.$message.warning('未读取到货物数据');
|
||
return;
|
||
}
|
||
this.cargoImportRows = rows;
|
||
this.$message.success(`成功读取${rows.length}条货物,请点击确定回填`);
|
||
} catch (error) {
|
||
window.console.log(error);
|
||
this.$message.error('货物导入失败,请检查文件格式');
|
||
} finally {
|
||
this.cargoImportLoading = false;
|
||
}
|
||
},
|
||
confirmCargoImport() {
|
||
if (!this.cargoImportRows.length) {
|
||
this.$message.warning('请先上传货物文件');
|
||
return;
|
||
}
|
||
this.cargoImportRows.forEach(row => {
|
||
this.addTransportCargoRow(-1, row);
|
||
});
|
||
this.$message.success(`成功导入${this.cargoImportRows.length}条货物`);
|
||
this.closeCargoImportDialog();
|
||
},
|
||
closeCargoImportDialog() {
|
||
if (this.cargoImportLoading) return;
|
||
this.cargoImportBox = false;
|
||
this.cargoImportRows = [];
|
||
},
|
||
handleCargoImportTemplate() {
|
||
exportBlob('/blade-transport/shipping-template/export-goods-template').then(res => {
|
||
downloadXls(res.data, '运单货物导入模板.xlsx');
|
||
});
|
||
},
|
||
handleAmountInput(prop, value) {
|
||
const text = String(value || '').replace(/[^\d.]/g, '');
|
||
const parts = text.split('.');
|
||
this.form[prop] =
|
||
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0];
|
||
},
|
||
handleIntegerInput(prop, value) {
|
||
this.form[prop] = String(value || '').replace(/\D/g, '');
|
||
},
|
||
handleAttachmentChange(list) {
|
||
const userName = this.userInfo?.realName || this.userInfo?.userName || '';
|
||
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||
this.attachmentRows = (list || []).map(item => ({
|
||
...item,
|
||
description: item.description || '',
|
||
uploadUserName: item.uploadUserName || userName,
|
||
uploadTime: item.uploadTime || uploadTime,
|
||
}));
|
||
this.selectedAttachmentRows = [];
|
||
this.form.attachmentsJson = JSON.stringify(this.attachmentRows);
|
||
},
|
||
handleAttachmentSelectionChange(rows) {
|
||
this.selectedAttachmentRows = rows || [];
|
||
},
|
||
removeAttachment(index) {
|
||
this.attachmentRows.splice(index, 1);
|
||
this.selectedAttachmentRows = this.selectedAttachmentRows.filter(item =>
|
||
this.attachmentRows.includes(item)
|
||
);
|
||
this.form.attachmentsJson = JSON.stringify(this.attachmentRows);
|
||
},
|
||
formatFileSize(size) {
|
||
const number = Number(size);
|
||
if (!number) return '';
|
||
if (number < 1024) return `${number}B`;
|
||
if (number < 1024 * 1024) return `${(number / 1024).toFixed(1)}KB`;
|
||
return `${(number / 1024 / 1024).toFixed(1)}MB`;
|
||
},
|
||
attachmentUrl(row = {}) {
|
||
return row.url || row.link || row.fileUrl || row.downloadUrl || row.domain || '';
|
||
},
|
||
attachmentName(row = {}) {
|
||
return row.originalName || row.name || row.fileName || '附件';
|
||
},
|
||
attachmentExtension(row = {}) {
|
||
const source = String(this.attachmentName(row) || this.attachmentUrl(row)).split('?')[0];
|
||
const index = source.lastIndexOf('.');
|
||
return index > -1 ? source.slice(index + 1).toLowerCase() : '';
|
||
},
|
||
isAttachmentImage(row) {
|
||
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(this.attachmentExtension(row));
|
||
},
|
||
previewAttachment(row, rows = this.attachmentRows) {
|
||
const url = this.attachmentUrl(row);
|
||
if (!url) {
|
||
this.$message.warning('附件地址为空,无法预览');
|
||
return;
|
||
}
|
||
if (this.isAttachmentImage(row)) {
|
||
this.attachmentImagePreviewUrls = (rows || [])
|
||
.filter(item => this.isAttachmentImage(item) && this.attachmentUrl(item))
|
||
.map(item => this.attachmentUrl(item));
|
||
this.attachmentImagePreviewIndex = Math.max(
|
||
this.attachmentImagePreviewUrls.indexOf(url),
|
||
0
|
||
);
|
||
this.attachmentImagePreviewVisible = true;
|
||
return;
|
||
}
|
||
this.attachmentPreviewFile = {
|
||
name: this.attachmentName(row),
|
||
url,
|
||
mimeType: row.mimeType || row.contentType || '',
|
||
};
|
||
this.attachmentDocumentPreviewVisible = true;
|
||
},
|
||
handleAttachmentPreviewUnsupported() {
|
||
this.$message.warning('当前文件暂不支持在线预览');
|
||
},
|
||
handleAttachmentPreviewError() {
|
||
this.$message.error('附件预览失败');
|
||
},
|
||
downloadAttachment(row) {
|
||
const url = this.attachmentUrl(row);
|
||
if (!url) {
|
||
this.$message.warning('附件地址为空');
|
||
return;
|
||
}
|
||
downloadFileByUrl(url, this.attachmentName(row));
|
||
},
|
||
handleBatchDownload() {
|
||
const rows = this.selectedAttachmentRows.length
|
||
? this.selectedAttachmentRows
|
||
: this.attachmentRows;
|
||
rows.forEach((row, index) => {
|
||
window.setTimeout(() => this.downloadAttachment(row), index * 200);
|
||
});
|
||
},
|
||
ensureCargoTypeOptions() {
|
||
if (this.cargoTypeOptions.length) {
|
||
return Promise.resolve(this.cargoTypeOptions);
|
||
}
|
||
if (this.cargoTypeRequest) {
|
||
return this.cargoTypeRequest;
|
||
}
|
||
this.cargoTypeLoading = true;
|
||
this.cargoTypeRequest = getCargoTypeList(1, 9999)
|
||
.then(res => {
|
||
this.cargoTypeOptions = this.buildCargoTypeTree(extractRecords(res));
|
||
this.cargoTypeFlatOptions = this.flattenCargoTypeOptions(this.cargoTypeOptions);
|
||
return this.cargoTypeOptions;
|
||
})
|
||
.finally(() => {
|
||
this.cargoTypeLoading = false;
|
||
this.cargoTypeRequest = null;
|
||
});
|
||
return this.cargoTypeRequest;
|
||
},
|
||
buildCargoTypeTree(cargoTypes = []) {
|
||
const flatCargoTypes = [];
|
||
const collectCargoTypes = list => {
|
||
(list || []).forEach(item => {
|
||
flatCargoTypes.push({
|
||
...item,
|
||
children: undefined,
|
||
});
|
||
if (item.children?.length) collectCargoTypes(item.children);
|
||
});
|
||
};
|
||
collectCargoTypes(cargoTypes);
|
||
|
||
const nodeMap = new Map();
|
||
const codeMap = new Map();
|
||
flatCargoTypes.forEach(item => {
|
||
const id = item.id ?? item.cargoCode ?? item.code;
|
||
if (id === undefined || id === null) return;
|
||
const node = {
|
||
...item,
|
||
id: String(id),
|
||
cargoName: this.formatCargoTypeLabel(item),
|
||
cargoCode: item.cargoCode || item.code || '',
|
||
parentId:
|
||
item.parentId === undefined || item.parentId === null ? '' : String(item.parentId),
|
||
parentCargoCode: item.parentCargoCode || item.parentCode || '',
|
||
children: [],
|
||
};
|
||
nodeMap.set(node.id, node);
|
||
if (node.cargoCode) {
|
||
codeMap.set(String(node.cargoCode), node);
|
||
}
|
||
});
|
||
|
||
const rootNodes = [];
|
||
nodeMap.forEach(node => {
|
||
const parent =
|
||
nodeMap.get(node.parentId) ||
|
||
codeMap.get(String(node.parentCargoCode || '')) ||
|
||
codeMap.get(String(node.parentId || ''));
|
||
if (parent && parent !== node && Number(node.typeLevel) !== 1) {
|
||
parent.children.push(node);
|
||
} else {
|
||
rootNodes.push(node);
|
||
}
|
||
});
|
||
return rootNodes.map(item => this.normalizeCargoTypeNode(item, 1)).filter(Boolean);
|
||
},
|
||
normalizeCargoTypeNode(cargoType, level = 1, parentPath = []) {
|
||
if (level > 2) return null;
|
||
const id = String(cargoType.id);
|
||
const path = [...parentPath, id];
|
||
const children =
|
||
level === 1
|
||
? (cargoType.children || [])
|
||
.map(item => this.normalizeCargoTypeNode(item, level + 1, path))
|
||
.filter(Boolean)
|
||
: [];
|
||
return {
|
||
...cargoType,
|
||
id,
|
||
cargoName: this.formatCargoTypeLabel(cargoType),
|
||
cargoCode: cargoType.cargoCode || cargoType.code || '',
|
||
path,
|
||
leaf: level === 2,
|
||
children: children.length ? children : undefined,
|
||
};
|
||
},
|
||
flattenCargoTypeOptions(options = []) {
|
||
const result = [];
|
||
const collectOptions = list => {
|
||
(list || []).forEach(item => {
|
||
result.push(item);
|
||
if (item.children?.length) collectOptions(item.children);
|
||
});
|
||
};
|
||
collectOptions(options);
|
||
return result;
|
||
},
|
||
formatCargoTypeLabel(item = {}) {
|
||
return item.cargoName || item.name || item.typeName || item.label || '';
|
||
},
|
||
filterCargoType(node, keyword) {
|
||
const text = String(keyword || '').trim();
|
||
if (!text) return true;
|
||
const labels = node.pathLabels?.length ? node.pathLabels : [node.label];
|
||
return (
|
||
labels.some(label => String(label || '').includes(text)) ||
|
||
String(node.text || '').includes(text)
|
||
);
|
||
},
|
||
getCargoTypePathLabels(path = []) {
|
||
let options = this.cargoTypeOptions;
|
||
const labels = [];
|
||
(path || []).forEach(value => {
|
||
const option = (options || []).find(item => String(item.id) === String(value));
|
||
if (!option) return;
|
||
labels.push(option.cargoName || '');
|
||
options = option.children || [];
|
||
});
|
||
return labels.filter(Boolean);
|
||
},
|
||
findCargoTypeByPath(path = []) {
|
||
const value = Array.isArray(path) && path.length >= 2 ? String(path[1]) : '';
|
||
if (!value) return null;
|
||
return (
|
||
this.cargoTypeFlatOptions.find(
|
||
item => String(item.id) === value && item.path?.length >= 2
|
||
) || null
|
||
);
|
||
},
|
||
findCargoTypeByCodeOrName(condition = {}) {
|
||
const code = String(condition.cargoTypeCode || '').trim();
|
||
const name = String(condition.cargoType || '').trim();
|
||
return (
|
||
this.cargoTypeFlatOptions.find(item => {
|
||
const cargoCode = String(item.cargoCode || item.code || item.id || '');
|
||
return item.path?.length >= 2 && code && cargoCode === code;
|
||
}) ||
|
||
this.cargoTypeFlatOptions.find(item => {
|
||
const cargoName = this.formatCargoTypeLabel(item);
|
||
return item.path?.length >= 2 && name && (cargoName === name || name.endsWith(cargoName));
|
||
}) ||
|
||
null
|
||
);
|
||
},
|
||
resolveCommonCargoTypePath(row = {}) {
|
||
const path = this.normalizeCargoTypePath(row.cargoTypePath);
|
||
if (path.length) return path;
|
||
const cargoType =
|
||
this.findCargoTypeByCodeOrName({
|
||
cargoTypeCode: row.secondCargoTypeCode || row.firstCargoTypeCode || row.cargoTypeCode,
|
||
cargoType: row.secondCargoTypeName || row.firstCargoTypeName || row.cargoType,
|
||
}) ||
|
||
this.findCargoTypeByCodeOrName({
|
||
cargoTypeCode: row.firstCargoTypeCode || '',
|
||
cargoType: row.firstCargoTypeName || '',
|
||
});
|
||
return cargoType?.path || [];
|
||
},
|
||
normalizeCargoTypePath(path) {
|
||
return Array.isArray(path) && path.length >= 2
|
||
? path.slice(0, 2).map(value => String(value))
|
||
: [];
|
||
},
|
||
searchReset() {
|
||
this.query = {};
|
||
this.page.currentPage = 1;
|
||
this.onLoad(this.page);
|
||
},
|
||
searchChange(params, done) {
|
||
this.query = {
|
||
...params,
|
||
};
|
||
this.page.currentPage = 1;
|
||
this.onLoad(this.page, params);
|
||
done();
|
||
},
|
||
selectionChange(list) {
|
||
this.selectionList = list;
|
||
},
|
||
selectionClear() {
|
||
this.selectionList = [];
|
||
if (this.$refs.crud) {
|
||
this.$refs.crud.toggleSelection();
|
||
}
|
||
},
|
||
currentChange(currentPage) {
|
||
this.page.currentPage = currentPage;
|
||
},
|
||
sizeChange(pageSize) {
|
||
this.page.pageSize = pageSize;
|
||
},
|
||
refreshChange() {
|
||
this.onLoad(this.page, this.query);
|
||
},
|
||
handleScopeChange() {
|
||
if (!this.canViewAllDept) {
|
||
this.allDept = 0;
|
||
}
|
||
this.page.currentPage = 1;
|
||
this.onLoad(this.page);
|
||
},
|
||
normalizeSearchRangeValue(value, suffix) {
|
||
if (!value) return undefined;
|
||
return suffix ? `${value} ${suffix}` : value;
|
||
},
|
||
normalizeSearchRangeParams(params = {}) {
|
||
const query = { ...params };
|
||
const rangeMap = this.config.searchRangeMap || {};
|
||
Object.entries(rangeMap).forEach(([rangeProp, mapping]) => {
|
||
const [startProp, endProp, startSuffix, endSuffix] = Array.isArray(mapping)
|
||
? mapping
|
||
: [mapping.startProp, mapping.endProp, mapping.startSuffix, mapping.endSuffix];
|
||
const range = query[rangeProp];
|
||
if (Array.isArray(range) && range.length === 2) {
|
||
query[startProp] = this.normalizeSearchRangeValue(range[0], startSuffix);
|
||
query[endProp] = this.normalizeSearchRangeValue(range[1], endSuffix);
|
||
}
|
||
delete query[rangeProp];
|
||
});
|
||
return query;
|
||
},
|
||
normalizeQueryParams(params = {}) {
|
||
let query = { ...params };
|
||
if (Object.keys(this.config.searchRangeMap || {}).length) {
|
||
query = this.normalizeSearchRangeParams(query);
|
||
}
|
||
return query;
|
||
},
|
||
onLoad(page, params = {}) {
|
||
this.loading = true;
|
||
const query = this.normalizeQueryParams({
|
||
...params,
|
||
...this.query,
|
||
allDept: this.allDept,
|
||
});
|
||
this.api
|
||
.getList(page.currentPage, page.pageSize, query)
|
||
.then(res => {
|
||
const result = res.data.data;
|
||
this.page.total = result.total;
|
||
this.data = result.records;
|
||
this.selectionClear();
|
||
})
|
||
.finally(() => {
|
||
this.loading = false;
|
||
});
|
||
},
|
||
buildExportParams() {
|
||
const query = this.normalizeQueryParams({ ...this.query, allDept: this.allDept });
|
||
return {
|
||
...query,
|
||
ids: this.ids,
|
||
...(this.config.exportColumns
|
||
? { exportColumns: JSON.stringify(this.config.exportColumns) }
|
||
: {}),
|
||
[this.website.tokenHeader]: getToken(),
|
||
};
|
||
},
|
||
handleExport() {
|
||
this.$confirm(`是否导出${this.config.exportName || this.config.title}?`, '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
}).then(() => {
|
||
NProgress.start();
|
||
exportBlob(this.config.exportUrl, this.buildExportParams(), { feedback: true })
|
||
.then(res => {
|
||
downloadXls(
|
||
res.data,
|
||
`${this.config.exportName || this.config.title}${this.$dayjs().format(
|
||
'YYYY-MM-DD HH:mm:ss'
|
||
)}.xlsx`
|
||
);
|
||
})
|
||
.finally(() => {
|
||
NProgress.done();
|
||
});
|
||
});
|
||
},
|
||
handleTemplate() {
|
||
NProgress.start();
|
||
exportBlob(
|
||
this.config.templateUrl,
|
||
{
|
||
[this.website.tokenHeader]: getToken(),
|
||
},
|
||
{ feedback: true }
|
||
)
|
||
.then(res => {
|
||
downloadXls(res.data, `${this.config.title}模板.xlsx`);
|
||
})
|
||
.finally(() => {
|
||
NProgress.done();
|
||
});
|
||
},
|
||
handleImport() {
|
||
if (this.config.enableWaybillImport) {
|
||
this.$router.push({ path: '/business/waybill-import' });
|
||
return;
|
||
}
|
||
const column = this.findColumn(this.excelOptionConfig.column, 'excelFile');
|
||
column.action = this.config.importUrl;
|
||
openImportDialog(this, this.config.title, () => this.onLoad(this.page, this.query));
|
||
},
|
||
handleCopy(row) {
|
||
if (this.isStandaloneWaybillPage) {
|
||
this.$router.push({
|
||
path: standaloneWaybillFormRoute,
|
||
query: {
|
||
mode: 'add',
|
||
copyId: row.id,
|
||
name: `复制${this.config.title || ''}`,
|
||
},
|
||
});
|
||
return;
|
||
}
|
||
// 非独立页面模式,使用原有的弹窗复制逻辑
|
||
this.$confirm(`确定复制该${this.config.title}?`, '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
})
|
||
.then(() => this.api.copy(row.id))
|
||
.then(() => {
|
||
this.$message.success('复制成功');
|
||
this.onLoad(this.page, this.query);
|
||
});
|
||
},
|
||
openMileageDialog(row) {
|
||
this.mileageDialog = {
|
||
visible: true,
|
||
submitting: false,
|
||
row,
|
||
};
|
||
this.mileageForm = {
|
||
id: row.id,
|
||
mileage: row.mileage === null || row.mileage === undefined ? '' : String(row.mileage),
|
||
mileageRemark: row.mileageRemark || '',
|
||
};
|
||
this.$nextTick(() => this.$refs.mileageFormRef?.clearValidate());
|
||
},
|
||
handleMaintainMileageInput(value) {
|
||
this.mileageForm.mileage = String(value || '')
|
||
.replace(/\D/g, '')
|
||
.slice(0, 10);
|
||
},
|
||
async submitMileageMaintenance() {
|
||
const valid = await this.$refs.mileageFormRef?.validate().catch(() => false);
|
||
if (!valid || this.mileageDialog.submitting) return;
|
||
this.mileageDialog.submitting = true;
|
||
try {
|
||
await this.api.maintainMileage({
|
||
id: this.mileageForm.id,
|
||
mileage: Number(this.mileageForm.mileage),
|
||
mileageRemark: String(this.mileageForm.mileageRemark || '').trim(),
|
||
});
|
||
this.$message.success('里程维护成功');
|
||
this.mileageDialog.visible = false;
|
||
this.onLoad(this.page, this.query);
|
||
} finally {
|
||
this.mileageDialog.submitting = false;
|
||
}
|
||
},
|
||
resetMileageDialog() {
|
||
this.mileageDialog.row = null;
|
||
this.mileageForm = { id: '', mileage: '', mileageRemark: '' };
|
||
this.$refs.mileageFormRef?.clearValidate();
|
||
},
|
||
handleAction(action, row) {
|
||
if (action === 'reassign') {
|
||
this.openReassignDrawer(row);
|
||
return;
|
||
}
|
||
const actionName = {
|
||
enable: '启用',
|
||
disable: '停用',
|
||
cancel: '取消',
|
||
complete: this.completeActionLabel,
|
||
reassign: '重新派单',
|
||
}[action];
|
||
this.$confirm(`确定${actionName}该${this.config.title}?`, '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
})
|
||
.then(() => this.api[action](row.id))
|
||
.then(() => {
|
||
this.$message.success(`${actionName}成功`);
|
||
this.onLoad(this.page, this.query);
|
||
});
|
||
},
|
||
driverRejectReasonText(row) {
|
||
if (!row) return '';
|
||
if (typeof this.config.getDriverRejectReason === 'function') {
|
||
return this.config.getDriverRejectReason(row) || '';
|
||
}
|
||
return String(
|
||
row.driverRejectReason ||
|
||
row.driverRefuseReason ||
|
||
row.acceptRejectReason ||
|
||
row.rejectReason ||
|
||
''
|
||
).trim();
|
||
},
|
||
openReassignDrawer(row) {
|
||
if (!row?.id) return;
|
||
this.reassignDrawer.row = row;
|
||
this.reassignForm = {
|
||
id: row.id,
|
||
driverId: '',
|
||
driverName: '',
|
||
driverPhone: '',
|
||
vehicleNo: '',
|
||
};
|
||
this.reassignDrawer.visible = true;
|
||
this.$nextTick(() => this.$refs.reassignFormRef?.clearValidate());
|
||
},
|
||
resetReassignDrawer() {
|
||
this.reassignDrawer.row = null;
|
||
this.reassignDrawer.submitting = false;
|
||
this.reassignDrawer.driverLoading = false;
|
||
this.reassignForm = {
|
||
id: '',
|
||
driverId: '',
|
||
driverName: '',
|
||
driverPhone: '',
|
||
vehicleNo: '',
|
||
};
|
||
this.reassignDriverOptions = [];
|
||
this.$refs.reassignFormRef?.clearValidate();
|
||
},
|
||
fetchReassignDriverSuggestions(queryString, callback) {
|
||
const keyword = String(queryString || '').trim();
|
||
this.reassignDrawer.driverLoading = true;
|
||
getDriverList(1, 20, { ...(keyword ? { driverName: keyword } : {}), posts: '司机' })
|
||
.then(res => {
|
||
const records = extractRecords(res);
|
||
this.reassignDriverOptions = records;
|
||
callback(
|
||
records.map(item => ({
|
||
...item,
|
||
value: item.driverName || item.name || '',
|
||
}))
|
||
);
|
||
})
|
||
.catch(error => {
|
||
window.console.log(error);
|
||
callback([]);
|
||
})
|
||
.finally(() => {
|
||
this.reassignDrawer.driverLoading = false;
|
||
});
|
||
},
|
||
handleReassignDriverSelect(item = {}) {
|
||
const value = item.driverName || item.name || '';
|
||
const driverPhone = item.mobile || item.phone || item.driverPhone || '';
|
||
const drivingVehicle = String(item.drivingVehicle || '').trim();
|
||
this.reassignForm.driverId = item.id || '';
|
||
this.reassignForm.driverName = value;
|
||
if (driverPhone) this.reassignForm.driverPhone = driverPhone;
|
||
if (drivingVehicle) this.reassignForm.vehicleNo = drivingVehicle;
|
||
},
|
||
handleReassignDriverChange(value) {
|
||
const driver = this.reassignDriverOptions.find(item =>
|
||
[item.driverName, item.name].some(name => String(name || '') === String(value || ''))
|
||
);
|
||
this.reassignForm.driverId = driver?.id || '';
|
||
this.reassignForm.driverName = value || '';
|
||
if (driver) {
|
||
this.reassignForm.driverPhone =
|
||
driver.mobile || driver.phone || driver.driverPhone || this.reassignForm.driverPhone || '';
|
||
if (String(driver.drivingVehicle || '').trim()) {
|
||
this.reassignForm.vehicleNo = String(driver.drivingVehicle).trim();
|
||
}
|
||
}
|
||
},
|
||
submitReassign() {
|
||
this.$refs.reassignFormRef?.validate(valid => {
|
||
if (!valid) return;
|
||
this.reassignDrawer.submitting = true;
|
||
this.api
|
||
.reassign({
|
||
id: this.reassignForm.id,
|
||
driverId: this.reassignForm.driverId || null,
|
||
driverName: String(this.reassignForm.driverName || '').trim(),
|
||
driverPhone: String(this.reassignForm.driverPhone || '').trim(),
|
||
vehicleNo: String(this.reassignForm.vehicleNo || '').trim(),
|
||
})
|
||
.then(() => {
|
||
this.$message.success('重新派单成功');
|
||
this.reassignDrawer.visible = false;
|
||
this.onLoad(this.page, this.query);
|
||
})
|
||
.finally(() => {
|
||
this.reassignDrawer.submitting = false;
|
||
});
|
||
});
|
||
},
|
||
handleCustomOperation(operation, row) {
|
||
if (operation.action === 'createFromTemplate') {
|
||
this.api.getDetail(row.id).then(res => {
|
||
const template = res.data?.data || row;
|
||
sessionStorage.setItem(
|
||
'business-template-create',
|
||
JSON.stringify({ target: 'waybill-manage', data: template })
|
||
);
|
||
this.$router.push({
|
||
path: standaloneWaybillFormRoute,
|
||
query: { mode: 'add', fromTemplate: Date.now().toString() },
|
||
});
|
||
});
|
||
return;
|
||
}
|
||
if (operation.action === 'flow') {
|
||
this.handleFlow(row);
|
||
return;
|
||
}
|
||
const run = value => {
|
||
const args = operation.prompt ? [row.id, value] : [row.id];
|
||
this.api[operation.action](...args).then(() => {
|
||
this.$message.success(`${operation.label}成功`);
|
||
this.onLoad(this.page, this.query);
|
||
});
|
||
};
|
||
if (operation.prompt) {
|
||
this.$prompt(operation.prompt, operation.label, {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
inputPattern: /\S+/,
|
||
inputErrorMessage: operation.promptError || '请输入内容',
|
||
}).then(({ value }) => run(value));
|
||
return;
|
||
}
|
||
this.$confirm(operation.confirm || `确定${operation.label}该${this.config.title}?`, '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
}).then(() => run());
|
||
},
|
||
handleFlow(row, loadedDetail = false) {
|
||
const processInstanceId = row.processInstanceId || row.processInstanceID || row.procInstId;
|
||
if (
|
||
!processInstanceId &&
|
||
!loadedDetail &&
|
||
row.id &&
|
||
typeof this.api.getDetail === 'function'
|
||
) {
|
||
this.api.getDetail(row.id).then(res => this.handleFlow(res.data.data || {}, true));
|
||
return;
|
||
}
|
||
if (!processInstanceId) {
|
||
this.$message.warning('流程实例为空,无法查看流程');
|
||
return;
|
||
}
|
||
if (this.website.design.designMode) {
|
||
this.processInstanceId = processInstanceId;
|
||
} else {
|
||
this.flowUrl = `/api/blade-flow/process/diagram-view?processInstanceId=${processInstanceId}`;
|
||
}
|
||
this.flowBox = true;
|
||
},
|
||
handleChangeRecordFlow(row) {
|
||
const processInstanceId = row.processInstanceId || row.processInstanceID || row.procInstId;
|
||
if (!processInstanceId) {
|
||
this.$message.warning('该变更记录暂无流程实例');
|
||
return;
|
||
}
|
||
this.handleFlow(row, true);
|
||
},
|
||
handleBatchComplete() {
|
||
if (!this.selectionList.length) {
|
||
this.$message.warning('请选择至少一条数据');
|
||
return;
|
||
}
|
||
this.$confirm('确定完成选中的运单?', '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
})
|
||
.then(() => this.api.batchComplete(this.ids))
|
||
.then(res => {
|
||
const result = res.data.data || {};
|
||
this.$message.success(
|
||
`成功完成${result.successCount || 0}条,跳过${result.skippedCount || 0}条`
|
||
);
|
||
this.onLoad(this.page, this.query);
|
||
});
|
||
},
|
||
handleRoadLoading() {
|
||
if (!this.selectionList.length) {
|
||
this.$message.warning('请选择至少一条数据');
|
||
return;
|
||
}
|
||
const nonRoadWaybills = this.selectionList.filter(row => !this.waybillIsRoadTransport(row));
|
||
if (nonRoadWaybills.length) {
|
||
this.$message.warning('公路配载仅支持运输方式为“公路运输”的运单');
|
||
return;
|
||
}
|
||
const unavailableWaybills = this.selectionList.filter(
|
||
row =>
|
||
!['processing', 'running'].includes(this.statusValue(row)) ||
|
||
row.loadingId ||
|
||
row.loadingManageId ||
|
||
row.loadingNo
|
||
);
|
||
if (unavailableWaybills.length) {
|
||
this.$message.warning('公路配载仅支持进行中且未配载的运单');
|
||
return;
|
||
}
|
||
try {
|
||
sessionStorage.setItem(
|
||
loadingCreateWaybillsStorageKey,
|
||
JSON.stringify({
|
||
source: 'waybill-manage',
|
||
rows: this.selectionList,
|
||
})
|
||
);
|
||
} catch (error) {
|
||
this.$message.error('暂存待配载运单失败,请重试');
|
||
return;
|
||
}
|
||
this.$router.push({
|
||
path: '/business/loading-manage/form',
|
||
query: {
|
||
mode: 'add',
|
||
name: '新增配载管理',
|
||
},
|
||
});
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
// 对齐 customer-archive 的 .archive-form 风格:label 固定宽折行 + 控件 250px
|
||
.waybill-manage-page__task-form,
|
||
.waybill-manage-page__freight-form {
|
||
:deep(.el-form-item__label) {
|
||
flex: 0 0 calc(6em + 24px) !important;
|
||
width: calc(6em + 24px) !important;
|
||
height: auto;
|
||
white-space: normal !important;
|
||
word-break: break-all;
|
||
overflow-wrap: anywhere;
|
||
line-height: 18px;
|
||
}
|
||
|
||
:deep(.el-form-item) {
|
||
align-items: center;
|
||
}
|
||
|
||
:deep(.el-form-item__content > .el-input),
|
||
:deep(.el-form-item__content > .el-select),
|
||
:deep(.el-form-item__content > .el-cascader),
|
||
:deep(.el-form-item__content > .el-input-number),
|
||
:deep(.el-form-item__content > .el-date-editor) {
|
||
width: 250px;
|
||
max-width: 100%;
|
||
}
|
||
}
|
||
|
||
.waybill-manage-page {
|
||
&__field {
|
||
width: 100%;
|
||
}
|
||
|
||
&__contract-name {
|
||
text-align: left;
|
||
}
|
||
|
||
&__contract-expiry-search {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-left: auto;
|
||
min-width: 0;
|
||
|
||
:deep(.el-select) {
|
||
width: 160px;
|
||
}
|
||
}
|
||
|
||
&__contract-expiry-label {
|
||
flex: 0 0 auto;
|
||
min-width: 140px;
|
||
color: #606266;
|
||
text-align: right;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
&__contract-expiry-tags {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
|
||
:deep(.el-check-tag) {
|
||
margin: 0;
|
||
white-space: nowrap;
|
||
}
|
||
}
|
||
|
||
.dialog-section-title {
|
||
display: flex;
|
||
align-items: center;
|
||
width: 100%;
|
||
color: #303133;
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
|
||
&::before {
|
||
display: block;
|
||
width: 4px;
|
||
height: 16px;
|
||
margin-right: 8px;
|
||
background: #409eff;
|
||
content: '';
|
||
}
|
||
|
||
&--action {
|
||
justify-content: space-between;
|
||
}
|
||
}
|
||
|
||
&__shipping-title,
|
||
&__goods-title {
|
||
min-width: 100%;
|
||
}
|
||
|
||
&__shipping-route-action {
|
||
margin-left: auto;
|
||
}
|
||
|
||
&__goods-title &__section-actions,
|
||
&__shipping-title .el-button {
|
||
margin-left: auto;
|
||
}
|
||
|
||
&__section-actions {
|
||
display: flex;
|
||
flex: 0 0 auto;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
&__task {
|
||
width: 100%;
|
||
}
|
||
|
||
&__task-mode-switch {
|
||
display: flex;
|
||
justify-content: flex-start;
|
||
width: 100%;
|
||
}
|
||
|
||
&__task-content-title {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
&__task-full &__task-content-title {
|
||
margin-top: 16px;
|
||
}
|
||
|
||
&__task-form {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
column-gap: 24px;
|
||
row-gap: 16px;
|
||
width: 100%;
|
||
|
||
:deep(.el-form-item) {
|
||
min-width: 0;
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
:deep(.el-form-item__content) {
|
||
min-width: 0;
|
||
}
|
||
|
||
:deep(.el-select),
|
||
:deep(.el-cascader),
|
||
:deep(.el-date-editor.el-input) {
|
||
width: 100%;
|
||
}
|
||
|
||
:deep(.el-radio-group) {
|
||
width: 100%;
|
||
}
|
||
|
||
:deep(.el-radio-button) {
|
||
flex: 1;
|
||
}
|
||
|
||
:deep(.el-radio-button__inner) {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
&__task-form--full {
|
||
display: block;
|
||
}
|
||
|
||
&__task-row {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
column-gap: 24px;
|
||
row-gap: 16px;
|
||
margin-bottom: 16px;
|
||
width: 100%;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
:deep(.el-form-item) {
|
||
min-width: 0;
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
:deep(.el-form-item__content) {
|
||
min-width: 0;
|
||
}
|
||
|
||
:deep(.el-select),
|
||
:deep(.el-cascader),
|
||
:deep(.el-date-editor.el-input) {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
&__task-span-4 {
|
||
grid-column: 1 / -1;
|
||
}
|
||
|
||
&__task-span-1 {
|
||
grid-column: span 1;
|
||
}
|
||
|
||
&__label-with-info {
|
||
display: inline-flex;
|
||
gap: 4px;
|
||
align-items: center;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
&__info-icon {
|
||
color: #909399;
|
||
cursor: help;
|
||
}
|
||
|
||
&__task-carrier-type {
|
||
:deep(.el-form-item__content) {
|
||
flex: 0 1 260px;
|
||
max-width: 260px;
|
||
}
|
||
|
||
:deep(.el-radio-group) {
|
||
width: auto;
|
||
}
|
||
|
||
:deep(.el-radio-button) {
|
||
flex: 0 0 auto;
|
||
}
|
||
|
||
:deep(.el-radio-button__inner) {
|
||
width: auto;
|
||
}
|
||
}
|
||
|
||
&__task-remark {
|
||
grid-column: 1 / -1;
|
||
|
||
:deep(.el-form-item__content) {
|
||
width: 100%;
|
||
}
|
||
|
||
:deep(.el-input),
|
||
:deep(.el-textarea) {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
&__task-note {
|
||
grid-column: 1 / -1;
|
||
|
||
:deep(.el-input) {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
:global(
|
||
.waybill-manage-page--form-page
|
||
.waybill-manage-page__task-form
|
||
.el-form-item__content
|
||
> .el-autocomplete
|
||
),
|
||
:global(
|
||
.waybill-manage-page--form-page
|
||
.waybill-manage-page__task-form
|
||
.el-form-item__content
|
||
> .el-date-editor
|
||
) {
|
||
width: 250px !important;
|
||
max-width: 100%;
|
||
}
|
||
|
||
&__suffix-select {
|
||
width: 72px;
|
||
margin-right: -8px;
|
||
|
||
:deep(.el-select__wrapper),
|
||
:deep(.el-select__wrapper:hover),
|
||
:deep(.el-select__wrapper.is-focused) {
|
||
min-height: 24px;
|
||
padding: 0 4px;
|
||
border: 0;
|
||
box-shadow: none !important;
|
||
}
|
||
}
|
||
|
||
&__suffix-select--wide {
|
||
width: 104px;
|
||
}
|
||
|
||
&__append-select {
|
||
width: 92px;
|
||
margin-right: -8px;
|
||
|
||
:deep(.el-select__wrapper),
|
||
:deep(.el-select__wrapper:hover),
|
||
:deep(.el-select__wrapper.is-focused) {
|
||
min-height: 24px;
|
||
padding: 0 4px;
|
||
border: 0;
|
||
box-shadow: none !important;
|
||
}
|
||
}
|
||
|
||
&__append-select--wide {
|
||
width: 96px;
|
||
min-width: 96px;
|
||
|
||
// 仅这两个窄 append 内的 placeholder 文字水平居中
|
||
:deep(.el-select__placeholder) {
|
||
left: 50% !important;
|
||
transform: translateY(-50%) translateX(-50%) !important;
|
||
text-align: center;
|
||
width: auto;
|
||
}
|
||
}
|
||
|
||
:deep(.waybill-manage-page__goods-info-cell .cell) {
|
||
overflow: visible;
|
||
height: auto;
|
||
max-height: none;
|
||
line-height: 20px;
|
||
white-space: normal;
|
||
word-break: break-word;
|
||
text-overflow: clip;
|
||
}
|
||
|
||
:deep(.waybill-manage-page__waybill-address-cell) {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
line-height: 20px;
|
||
white-space: normal;
|
||
word-break: break-word;
|
||
}
|
||
|
||
&__route-address-row {
|
||
display: grid;
|
||
grid-template-columns: 260px minmax(280px, 1fr) 40px 58px minmax(160px, 220px) 72px minmax(
|
||
160px,
|
||
220px
|
||
);
|
||
gap: 8px;
|
||
align-items: center;
|
||
width: 100%;
|
||
|
||
:deep(.el-input),
|
||
:deep(.el-select),
|
||
:deep(.el-cascader) {
|
||
width: 100%;
|
||
}
|
||
|
||
.el-button {
|
||
width: 32px;
|
||
min-width: 32px;
|
||
padding: 0;
|
||
}
|
||
|
||
.waybill-manage-page__map-suffix {
|
||
color: #409eff;
|
||
font-size: 18px;
|
||
}
|
||
|
||
:deep(.el-input .el-input__icon) {
|
||
color: #409eff;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.waybill-manage-page__address-pick-btn {
|
||
:deep(.el-icon) {
|
||
font-size: 22px;
|
||
}
|
||
}
|
||
}
|
||
|
||
&__address-choose-link {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
height: 100%;
|
||
padding: 0 4px;
|
||
font-size: 13px;
|
||
line-height: 1;
|
||
cursor: pointer;
|
||
}
|
||
|
||
&__route-label {
|
||
color: #606266;
|
||
font-size: 14px;
|
||
line-height: 32px;
|
||
text-align: right;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
&__map-suffix {
|
||
cursor: pointer;
|
||
color: #909399;
|
||
font-size: 18px;
|
||
|
||
&:hover {
|
||
color: #409eff;
|
||
}
|
||
}
|
||
|
||
&__cargo-wrap {
|
||
display: block;
|
||
width: 100%;
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
}
|
||
|
||
&__cargo-import {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 56px;
|
||
padding: 12px 0 8px;
|
||
|
||
:deep(.el-upload__tip) {
|
||
margin: 16px 0 0;
|
||
color: #606266;
|
||
font-size: 16px;
|
||
}
|
||
}
|
||
|
||
&__cargo-import-dialog {
|
||
:deep(.el-dialog__footer) {
|
||
text-align: center;
|
||
}
|
||
}
|
||
|
||
&__cargo-import {
|
||
padding: 0 !important;
|
||
}
|
||
|
||
&__cargo-import-card {
|
||
background: #fff;
|
||
border-radius: 6px;
|
||
padding: 24px;
|
||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||
width: 90%;
|
||
}
|
||
|
||
&__cargo-table {
|
||
width: 100%;
|
||
min-width: 0;
|
||
|
||
:deep(.el-table__header th) {
|
||
background: #f5f7fa;
|
||
color: #303133;
|
||
font-weight: 600;
|
||
}
|
||
|
||
:deep(.el-table__cell) {
|
||
border-color: #eff1f7;
|
||
}
|
||
|
||
:deep(.el-input),
|
||
:deep(.el-select),
|
||
:deep(.el-cascader) {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
&__cargo-actions {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
white-space: nowrap;
|
||
|
||
.el-button {
|
||
margin-left: 0;
|
||
}
|
||
}
|
||
|
||
&__row-actions {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
justify-content: center;
|
||
}
|
||
|
||
:deep(.waybill-manage-page__full-form-item) {
|
||
width: 100%;
|
||
max-width: 100%;
|
||
}
|
||
|
||
:deep(.waybill-manage-page__full-form-item > .el-form-item) {
|
||
width: 100%;
|
||
}
|
||
|
||
:deep(.waybill-manage-page__full-form-item > .el-form-item > .el-form-item__label) {
|
||
display: none;
|
||
width: 0 !important;
|
||
padding: 0 !important;
|
||
}
|
||
|
||
:deep(.waybill-manage-page__full-form-item > .el-form-item > .el-form-item__content) {
|
||
flex: 1 1 100%;
|
||
width: 100%;
|
||
max-width: 100%;
|
||
margin-left: 0 !important;
|
||
}
|
||
|
||
:deep(.waybill-manage-page__full-form-item > .el-form-item > .el-form-item__content > div) {
|
||
width: 100%;
|
||
min-width: 0;
|
||
}
|
||
|
||
&__cargo-total {
|
||
display: flex;
|
||
justify-content: flex-start;
|
||
padding-top: 12px;
|
||
color: #303133;
|
||
font-weight: 600;
|
||
}
|
||
|
||
&__freight-section {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
&__freight-table {
|
||
width: 100%;
|
||
|
||
:deep(.el-table__header th) {
|
||
background: #f5f7fa;
|
||
color: #303133;
|
||
font-weight: 600;
|
||
}
|
||
|
||
:deep(.el-table__cell) {
|
||
border-color: #eff1f7;
|
||
}
|
||
|
||
:deep(.el-input),
|
||
:deep(.el-select) {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
&__dialog-search {
|
||
padding: 12px 12px 4px;
|
||
margin-bottom: 12px;
|
||
background: #fff;
|
||
border: 1px solid #eff1f7;
|
||
border-radius: 4px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||
|
||
:deep(.el-form-item) {
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
:deep(.el-input),
|
||
:deep(.el-select),
|
||
:deep(.el-cascader) {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
&__dialog-search-actions {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
&__freight-form {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
column-gap: 24px;
|
||
row-gap: 16px;
|
||
width: 100%;
|
||
|
||
:deep(.el-form-item) {
|
||
min-width: 0;
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
:deep(.el-form-item__content) {
|
||
min-width: 0;
|
||
}
|
||
|
||
:deep(.el-input),
|
||
:deep(.el-select) {
|
||
width: 100%;
|
||
}
|
||
|
||
&--road {
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
column-gap: 56px;
|
||
row-gap: 16px;
|
||
|
||
:deep(.waybill-manage-page__append-select) {
|
||
width: 112px;
|
||
}
|
||
}
|
||
}
|
||
|
||
&__freight-footer-summary {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 24px;
|
||
height: 28px;
|
||
color: #303133;
|
||
font-size: 13px;
|
||
|
||
strong {
|
||
margin-left: 4px;
|
||
font-size: 15px;
|
||
}
|
||
|
||
:deep(.el-link) {
|
||
font-size: 13px;
|
||
}
|
||
}
|
||
|
||
&__process-config-dialog {
|
||
max-height: 70vh;
|
||
overflow-y: auto;
|
||
|
||
:deep(.el-dialog__body) {
|
||
padding-top: 8px;
|
||
}
|
||
}
|
||
|
||
&__process-config-section {
|
||
padding: 16px 0;
|
||
border-bottom: 1px solid #eff1f7;
|
||
|
||
&:last-child {
|
||
border-bottom: 0;
|
||
}
|
||
}
|
||
|
||
&__process-config-meta {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 24px;
|
||
margin-bottom: 16px;
|
||
color: #606266;
|
||
|
||
span:first-child {
|
||
color: #303133;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
&__process-timeline {
|
||
position: relative;
|
||
display: flex;
|
||
width: 100%;
|
||
min-width: 0;
|
||
box-sizing: border-box;
|
||
padding: 32px 20px 20px;
|
||
overflow: hidden;
|
||
background: #fff;
|
||
}
|
||
|
||
&__process-timeline-line {
|
||
position: absolute;
|
||
top: 44px;
|
||
right: 8%;
|
||
left: 8%;
|
||
height: 2px;
|
||
background: #dcdfe6;
|
||
}
|
||
|
||
&__process-timeline-item {
|
||
position: relative;
|
||
z-index: 1;
|
||
flex: 1;
|
||
min-width: 0;
|
||
text-align: center;
|
||
}
|
||
|
||
&__process-timeline-dot {
|
||
display: block;
|
||
width: 14px;
|
||
height: 14px;
|
||
margin: 0 auto 10px;
|
||
border: 3px solid #fff;
|
||
border-radius: 50%;
|
||
background: #409eff;
|
||
box-shadow: 0 0 0 2px #409eff;
|
||
}
|
||
|
||
&__process-timeline-name {
|
||
color: #303133;
|
||
font-weight: 600;
|
||
}
|
||
|
||
&__process-timeline-desc {
|
||
margin-top: 8px;
|
||
color: #909399;
|
||
font-size: 12px;
|
||
line-height: 1.8;
|
||
}
|
||
|
||
&__dialog-pagination {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
margin-top: 12px;
|
||
}
|
||
|
||
&__map-toolbar {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-bottom: 12px;
|
||
|
||
.el-input {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
}
|
||
|
||
&__map {
|
||
width: 100%;
|
||
height: 420px;
|
||
border: 1px solid #eff1f7;
|
||
}
|
||
|
||
&__map-info {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
margin-top: 10px;
|
||
color: #606266;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
&__date-range {
|
||
display: flex;
|
||
align-items: center;
|
||
width: 100%;
|
||
min-width: 0;
|
||
}
|
||
|
||
&__date {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
&__date-separator {
|
||
flex: 0 0 auto;
|
||
padding: 0 8px;
|
||
color: #303133;
|
||
}
|
||
|
||
&__inline-number {
|
||
display: flex;
|
||
align-items: center;
|
||
width: 100%;
|
||
|
||
.el-input {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
span {
|
||
flex: 0 0 auto;
|
||
margin-left: 8px;
|
||
color: #303133;
|
||
}
|
||
}
|
||
|
||
&__required-column {
|
||
&::before {
|
||
margin-right: 4px;
|
||
color: #f56c6c;
|
||
content: '*';
|
||
}
|
||
}
|
||
|
||
&__attachment {
|
||
width: 100%;
|
||
}
|
||
|
||
&__attachment-description {
|
||
margin-bottom: 12px;
|
||
color: #606266;
|
||
line-height: 1.6;
|
||
white-space: pre-wrap;
|
||
word-break: break-word;
|
||
}
|
||
|
||
&__attachment-description-item + &__attachment-description-item {
|
||
margin-top: 4px;
|
||
}
|
||
|
||
&__attachment-description-label {
|
||
color: #303133;
|
||
font-weight: 600;
|
||
}
|
||
|
||
&__attachment-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
&__attachment-upload {
|
||
display: flex;
|
||
justify-content: flex-start;
|
||
margin-top: 12px;
|
||
|
||
:deep(.vehicle-attachment-upload) {
|
||
width: auto;
|
||
}
|
||
|
||
:deep(.el-upload) {
|
||
display: inline-flex;
|
||
}
|
||
}
|
||
|
||
&__attachment-table {
|
||
width: 100%;
|
||
min-width: 0;
|
||
|
||
:deep(.el-table__header th) {
|
||
background: #f5f7fa;
|
||
color: #303133;
|
||
font-weight: 600;
|
||
}
|
||
|
||
:deep(.el-table__cell) {
|
||
border-color: #eff1f7;
|
||
}
|
||
}
|
||
|
||
&__change-table {
|
||
width: 100%;
|
||
|
||
:deep(.el-table__header th) {
|
||
background: #f5f7fa;
|
||
color: #303133;
|
||
font-weight: 600;
|
||
}
|
||
|
||
:deep(.el-table__cell) {
|
||
border-color: #eff1f7;
|
||
}
|
||
}
|
||
|
||
&__detail-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
|
||
.section-card {
|
||
:deep(.el-descriptions__label) {
|
||
width: 140px;
|
||
color: #606266;
|
||
font-weight: 600;
|
||
text-align: right;
|
||
}
|
||
|
||
:deep(.el-descriptions__content) {
|
||
min-height: 42px;
|
||
white-space: pre-wrap;
|
||
word-break: break-word;
|
||
}
|
||
}
|
||
}
|
||
|
||
&__detail-page {
|
||
padding: 12px;
|
||
background: #f5f6fa;
|
||
}
|
||
|
||
&__detail-footer {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
padding-top: 12px;
|
||
}
|
||
|
||
&__waybill-detail-summary {
|
||
:deep(.el-card__body) {
|
||
padding: 18px 22px;
|
||
}
|
||
}
|
||
|
||
&__waybill-heading {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
margin-bottom: 18px;
|
||
|
||
strong {
|
||
font-size: 20px;
|
||
}
|
||
}
|
||
|
||
&__waybill-route-change-link {
|
||
margin-left: auto;
|
||
}
|
||
|
||
&__waybill-summary-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, minmax(120px, 1fr)) minmax(360px, 1.5fr);
|
||
gap: 16px 24px;
|
||
}
|
||
|
||
&__waybill-summary-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
min-width: 0;
|
||
}
|
||
|
||
&__waybill-summary-item.is-plan {
|
||
grid-column: 1;
|
||
grid-row: 2;
|
||
}
|
||
|
||
&__waybill-label {
|
||
color: #909399;
|
||
}
|
||
|
||
&__waybill-value {
|
||
overflow: hidden;
|
||
color: #303133;
|
||
font-weight: 500;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
|
||
&.is-link {
|
||
color: #409eff;
|
||
}
|
||
}
|
||
|
||
&__waybill-route {
|
||
display: grid;
|
||
grid-template-columns: minmax(100px, 1fr) minmax(36px, 0.7fr) minmax(100px, 1fr);
|
||
grid-row: 1 / span 3;
|
||
grid-column: 5;
|
||
align-items: start;
|
||
min-width: 0;
|
||
padding: 4px 0;
|
||
}
|
||
|
||
&__waybill-route-item {
|
||
display: flex;
|
||
min-width: 0;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
text-align: center;
|
||
}
|
||
|
||
&__waybill-route-marker {
|
||
display: inline-flex;
|
||
width: 36px;
|
||
height: 36px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #fff;
|
||
border-radius: 8px;
|
||
background: #409eff;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
|
||
.is-end & {
|
||
background: #67c23a;
|
||
}
|
||
}
|
||
|
||
&__waybill-route-line {
|
||
width: 100%;
|
||
height: 6px;
|
||
margin-top: 15px;
|
||
border-radius: 1px;
|
||
background: #e4e7ed;
|
||
}
|
||
|
||
&__waybill-route-detail {
|
||
display: flex;
|
||
min-width: 0;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 5px;
|
||
margin-top: 8px;
|
||
width: 100%;
|
||
}
|
||
|
||
&__waybill-route-name {
|
||
max-width: 100%;
|
||
overflow: hidden;
|
||
color: #303133;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
line-height: 1.35;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
&__waybill-route-addr {
|
||
display: block;
|
||
max-width: 100%;
|
||
overflow: hidden;
|
||
color: #606266;
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
&__waybill-route-contact {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
justify-content: center;
|
||
gap: 2px 8px;
|
||
max-width: 100%;
|
||
color: #909399;
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
&__waybill-attachments {
|
||
grid-column: 1 / 5;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
|
||
div {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 16px;
|
||
}
|
||
}
|
||
|
||
&__waybill-carrier-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
gap: 18px 32px;
|
||
padding: 4px 20px 8px;
|
||
text-align: left;
|
||
}
|
||
|
||
&__waybill-detail-field {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 12px;
|
||
min-width: 0;
|
||
text-align: left;
|
||
|
||
span {
|
||
flex: 0 0 auto;
|
||
color: #909399;
|
||
}
|
||
|
||
strong {
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
color: #303133;
|
||
font-weight: 500;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
}
|
||
|
||
&__waybill-detail-field--remark {
|
||
grid-column: 3 / -1;
|
||
}
|
||
|
||
&__waybill-detail-field--remark-full {
|
||
grid-column: 1 / -1;
|
||
}
|
||
|
||
&__waybill-fee-grid {
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
|
||
.waybill-manage-page__waybill-fee-value {
|
||
color: #409eff;
|
||
font-size: 16px;
|
||
}
|
||
}
|
||
|
||
&__waybill-detail-bottom {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 8px;
|
||
}
|
||
|
||
&__waybill-map-empty {
|
||
display: grid;
|
||
min-height: 230px;
|
||
place-items: center;
|
||
color: #909399;
|
||
border: 1px solid #eff1f7;
|
||
}
|
||
|
||
&__waybill-locate {
|
||
position: relative;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
|
||
&__waybill-locate-meta {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 8px 16px;
|
||
padding: 10px 12px;
|
||
border: 1px solid #eff1f7;
|
||
background: #fafafa;
|
||
color: #606266;
|
||
font-size: 13px;
|
||
|
||
span {
|
||
margin-right: 8px;
|
||
color: #909399;
|
||
}
|
||
|
||
strong {
|
||
color: #303133;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
&__waybill-locate-address {
|
||
grid-column: 1 / -1;
|
||
}
|
||
|
||
&__waybill-locate-map {
|
||
width: 100%;
|
||
min-height: 320px;
|
||
height: 320px;
|
||
border: 1px solid #eff1f7;
|
||
background: #f5f7fa;
|
||
}
|
||
|
||
&__waybill-locate-map--track {
|
||
min-height: 420px;
|
||
height: 420px;
|
||
}
|
||
|
||
&__waybill-map-tip {
|
||
position: absolute;
|
||
left: 50%;
|
||
top: 50%;
|
||
z-index: 2;
|
||
transform: translate(-50%, -50%);
|
||
padding: 8px 14px;
|
||
border-radius: 4px;
|
||
background: rgba(255, 255, 255, 0.92);
|
||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||
color: #909399;
|
||
font-size: 13px;
|
||
text-align: center;
|
||
pointer-events: none;
|
||
}
|
||
|
||
&__waybill-track-toolbar {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
align-items: center;
|
||
margin-bottom: 10px;
|
||
color: #606266;
|
||
font-size: 13px;
|
||
}
|
||
|
||
&__waybill-track-total {
|
||
color: #909399;
|
||
}
|
||
|
||
&__waybill-track-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
&__waybill-process-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
&__voucher-image-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||
gap: 8px;
|
||
min-height: 120px;
|
||
}
|
||
|
||
&__punch-record {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
|
||
&__punch-record-title {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
}
|
||
|
||
&__punch-record-line {
|
||
font-size: 13px;
|
||
color: #606266;
|
||
|
||
span + span {
|
||
margin-left: 12px;
|
||
}
|
||
}
|
||
|
||
&__punch-record-photos,
|
||
&__driver-upload-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||
gap: 10px;
|
||
margin-top: 4px;
|
||
min-height: 80px;
|
||
}
|
||
|
||
&__driver-upload-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;
|
||
}
|
||
}
|
||
|
||
&__driver-upload-label {
|
||
font-size: 12px;
|
||
line-height: 1.4;
|
||
color: #606266;
|
||
text-align: center;
|
||
word-break: break-all;
|
||
}
|
||
|
||
&__voucher-image-item {
|
||
position: relative;
|
||
aspect-ratio: 1;
|
||
overflow: hidden;
|
||
cursor: pointer;
|
||
border: 1px solid #eff1f7;
|
||
border-radius: 4px;
|
||
|
||
.el-image {
|
||
display: block;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
&--folder {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 6px;
|
||
padding: 12px;
|
||
background: #fff;
|
||
|
||
.el-image {
|
||
width: 64px;
|
||
height: 64px;
|
||
flex: none;
|
||
}
|
||
}
|
||
}
|
||
|
||
&__voucher-folder-toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
min-height: 32px;
|
||
margin-bottom: 8px;
|
||
color: #606266;
|
||
font-size: 13px;
|
||
}
|
||
|
||
&__voucher-folder-name {
|
||
width: 100%;
|
||
overflow: hidden;
|
||
color: #606266;
|
||
font-size: 13px;
|
||
text-align: center;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
&__route-change-dialog {
|
||
:deep(.el-dialog__body) {
|
||
padding-top: 12px;
|
||
}
|
||
|
||
.section-card {
|
||
margin-bottom: 12px;
|
||
}
|
||
}
|
||
|
||
&__route-change-original {
|
||
margin-bottom: 6px;
|
||
color: #909399;
|
||
font-size: 12px;
|
||
}
|
||
|
||
&__route-change-location {
|
||
color: #409eff;
|
||
cursor: pointer;
|
||
}
|
||
|
||
&__route-change-hint {
|
||
color: #909399;
|
||
font-size: 12px;
|
||
}
|
||
|
||
&__route-change-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
&__route-change-item {
|
||
display: grid;
|
||
grid-template-columns: 42px minmax(0, 1fr) minmax(120px, auto) 24px;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 10px 12px;
|
||
border: 1px solid #eff1f7;
|
||
border-radius: 4px;
|
||
background: #fff;
|
||
cursor: move;
|
||
}
|
||
|
||
&__route-change-type {
|
||
display: inline-flex;
|
||
width: 28px;
|
||
height: 28px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #fff;
|
||
border-radius: 50%;
|
||
background: #409eff;
|
||
}
|
||
|
||
&__route-change-tags {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
&__route-change-record-content {
|
||
white-space: pre-wrap;
|
||
}
|
||
|
||
&__common-address-toolbar {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-bottom: 12px;
|
||
|
||
.el-input {
|
||
width: 260px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 1200px) {
|
||
&__waybill-summary-grid {
|
||
grid-template-columns: repeat(4, minmax(130px, 1fr));
|
||
}
|
||
|
||
&__waybill-attachments {
|
||
grid-column: 1 / -1;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 800px) {
|
||
&__waybill-summary-grid,
|
||
&__waybill-detail-bottom {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
&__waybill-route {
|
||
grid-template-columns: minmax(0, 1fr);
|
||
gap: 16px;
|
||
}
|
||
|
||
&__waybill-route-line {
|
||
width: 10px;
|
||
height: 32px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
&__waybill-attachments {
|
||
grid-column: auto;
|
||
}
|
||
|
||
&__waybill-carrier-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
&__waybill-detail-field--remark {
|
||
grid-column: auto;
|
||
}
|
||
|
||
&__waybill-detail-field--remark-full {
|
||
grid-column: auto;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 1280px) {
|
||
&__route-address-row {
|
||
grid-template-columns: 240px minmax(240px, 1fr) 40px 58px minmax(140px, 1fr);
|
||
}
|
||
|
||
&__route-address-row &__route-label:nth-of-type(2),
|
||
&__route-address-row .el-input:last-child {
|
||
grid-column: auto;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 960px) {
|
||
&__route-address-row {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
&__route-label {
|
||
text-align: left;
|
||
}
|
||
|
||
&__dialog-search-actions {
|
||
justify-content: flex-start;
|
||
}
|
||
}
|
||
}
|
||
.section-card {
|
||
margin-bottom: 0 !important;
|
||
}
|
||
|
||
/* 编辑弹窗:含分组的 Avue form 渲染为白卡风格 */
|
||
:deep(.avue-form__group:has(.dialog-section-title)) {
|
||
background: #fff;
|
||
border-radius: 6px;
|
||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||
padding: 16px;
|
||
margin-bottom: 12px;
|
||
|
||
.avue-form__group--title {
|
||
padding: 0 !important;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.avue-form__row {
|
||
padding: 0 !important;
|
||
}
|
||
}
|
||
|
||
:global(.waybill-manage-form-page-dialog) {
|
||
width: 100% !important;
|
||
min-height: 100%;
|
||
box-sizing: border-box;
|
||
//padding: 20px 24px 96px;
|
||
background: #f5f6fa;
|
||
margin: 0 !important;
|
||
border-radius: 0;
|
||
box-shadow: none;
|
||
}
|
||
|
||
:global(.waybill-manage-form-page-dialog .el-dialog__body) {
|
||
max-height: none;
|
||
padding: 20px 24px 96px;
|
||
background: #f5f6fa;
|
||
}
|
||
|
||
:global(.el-overlay:has(.waybill-manage-form-page-dialog)) {
|
||
position: fixed !important;
|
||
inset: 92px 0 0 230px !important;
|
||
z-index: 100 !important;
|
||
overflow: auto;
|
||
background: transparent;
|
||
}
|
||
|
||
:global(.el-overlay:has(.waybill-manage-form-page-dialog) .el-overlay-dialog) {
|
||
min-height: 100%;
|
||
overflow: visible;
|
||
}
|
||
|
||
:global(.waybill-manage-form-page-dialog .el-dialog__header) {
|
||
padding: 18px 24px;
|
||
margin-right: 0;
|
||
background: #fff;
|
||
border-bottom: 1px solid #eff1f7;
|
||
}
|
||
|
||
:global(.waybill-manage-form-page-dialog .avue-form__menu) {
|
||
position: fixed !important;
|
||
right: 0;
|
||
bottom: 0;
|
||
left: 230px;
|
||
z-index: 1001;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
min-height: 64px;
|
||
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(.waybill-manage-form-page-dialog .waybill-manage-page__freight-footer-summary) {
|
||
position: static;
|
||
order: 1;
|
||
height: auto;
|
||
margin-right: auto;
|
||
}
|
||
|
||
:global(
|
||
.waybill-manage-form-page-dialog
|
||
.avue-form__menu
|
||
> .waybill-manage-page__standalone-menu-actions
|
||
) {
|
||
display: flex;
|
||
order: 2;
|
||
gap: 8px;
|
||
}
|
||
|
||
:global(.waybill-manage-form-page-dialog .avue-form__menu > .el-button) {
|
||
order: 3;
|
||
}
|
||
|
||
:global(.avue--collapse .el-overlay:has(.waybill-manage-form-page-dialog)) {
|
||
left: 60px !important;
|
||
}
|
||
|
||
:global(.avue--collapse .waybill-manage-form-page-dialog .avue-form__menu) {
|
||
left: 60px;
|
||
}
|
||
|
||
:global(.avue-layout--horizontal .el-overlay:has(.waybill-manage-form-page-dialog)) {
|
||
top: 92px !important;
|
||
left: 0 !important;
|
||
}
|
||
|
||
:global(.avue-layout--horizontal .waybill-manage-form-page-dialog .avue-form__menu) {
|
||
left: 0;
|
||
}
|
||
|
||
/* 独立表单页 footer 的 Avue 提交/清空按钮自带图标(el-icon-check / el-icon-delete),
|
||
Avue 源码用 `|| 'el-icon-xxx'` 兜底,无法通过 option 配置去除,统一用 CSS 隐藏只保留文字 */
|
||
:global(.waybill-manage-form-page-dialog .avue-form__menu .el-icon) {
|
||
display: none;
|
||
}
|
||
|
||
// 运单独立表单页:标题分块使用白底卡片,间距 12px,与 section-card 风格一致
|
||
:global(.waybill-manage-form-page-dialog:not(.el-dialog)) {
|
||
padding-bottom: 96px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
:global(.waybill-manage-page--form-page .avue-form) {
|
||
background: transparent;
|
||
padding: 0;
|
||
}
|
||
|
||
:global(.waybill-manage-page--form-page .avue-form > .el-form > .avue-form__group),
|
||
:global(.waybill-manage-page--form-page .avue-group) {
|
||
background: #fff;
|
||
border-radius: 6px;
|
||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
|
||
margin-bottom: 12px;
|
||
padding: 14px 16px 4px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
:global(.waybill-manage-page--form-page .avue-form > .el-form > .avue-form__group:last-child),
|
||
:global(.waybill-manage-page--form-page .avue-group:last-child) {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
// 分组后的表单:卡片由 .avue-group 承担,内层 .avue-form__group 去卡片化,避免白卡叠白卡
|
||
:global(.waybill-manage-page--form-page .avue-form .avue-form__group:has(.dialog-section-title)) {
|
||
background: transparent;
|
||
border-radius: 0;
|
||
box-shadow: none;
|
||
padding: 0;
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
:global(.waybill-manage-page--form-page .avue-group .el-collapse) {
|
||
border-top: none;
|
||
border-bottom: none;
|
||
}
|
||
|
||
:global(.waybill-manage-page--form-page .avue-group .el-collapse-item__header) {
|
||
display: none;
|
||
}
|
||
|
||
:global(.waybill-manage-page--form-page .avue-group .el-collapse-item__wrap) {
|
||
border-bottom: none;
|
||
will-change: auto;
|
||
}
|
||
|
||
:global(.waybill-manage-page--form-page .avue-group .el-collapse-item__content) {
|
||
padding-bottom: 0;
|
||
}
|
||
|
||
:global(.waybill-manage-page__mileage-dialog .waybill-manage-page__mileage-form) {
|
||
padding: 4px 20px 0;
|
||
}
|
||
|
||
:global(.waybill-manage-page__mileage-dialog .waybill-manage-page__mileage-form .el-form-item) {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
:global(.waybill-manage-page__mileage-dialog .waybill-manage-page__mileage-form .el-input),
|
||
:global(.waybill-manage-page__mileage-dialog .waybill-manage-page__mileage-form .el-textarea) {
|
||
width: 100%;
|
||
}
|
||
|
||
.waybill-manage-page__status-cell {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.waybill-manage-page__reject-tip-icon {
|
||
color: #e6a23c;
|
||
cursor: pointer;
|
||
font-size: 16px;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
:global(.waybill-manage-page__reassign-drawer .el-drawer__body) {
|
||
padding: 12px 20px 0;
|
||
}
|
||
|
||
:global(.waybill-manage-page__reassign-drawer .waybill-manage-page__reassign-form) {
|
||
width: 100%;
|
||
}
|
||
|
||
.waybill-manage-page__reassign-row {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
column-gap: 16px;
|
||
row-gap: 8px;
|
||
margin-bottom: 12px;
|
||
align-items: start;
|
||
}
|
||
|
||
.waybill-manage-page__reassign-span-1 {
|
||
grid-column: span 1;
|
||
margin-bottom: 0;
|
||
width: 100%;
|
||
}
|
||
|
||
.waybill-manage-page__reassign-meta {
|
||
grid-column: span 1;
|
||
display: flex;
|
||
align-items: flex-start;
|
||
min-width: 0;
|
||
line-height: 32px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.waybill-manage-page__reassign-meta--wide {
|
||
grid-column: span 3;
|
||
}
|
||
|
||
.waybill-manage-page__reassign-meta-label {
|
||
flex: none;
|
||
width: 72px;
|
||
color: #606266;
|
||
text-align: right;
|
||
padding-right: 12px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.waybill-manage-page__reassign-meta-value {
|
||
flex: 1;
|
||
min-width: 0;
|
||
color: #303133;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.waybill-manage-page__reassign-footer {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 12px;
|
||
}
|
||
|
||
.el-input__icon {
|
||
color: #1e90ff !important;
|
||
}
|
||
</style>
|