ff9fb9168b
- 为 waybill-manage-page、transport-plan-page 等组件增加 routePathLocked 变量锁定路由路径 - 添加 isOwnedRouteActive 计算属性,防止缓存实例错误读取其它路由的 detailId 导致误请求 - watcher 和 openDetail 方法中添加路由守卫,避免多余的请求和误开详情标签 - 修改宿主视图绑定 detailId,避免直接绑定全局 $route.query 导致缓存污染 - 删除 router/views/index.js 中合同管理详情路由重复定义,防止内容区空白问题 - 优化 waybill-import-dialog 组件样式和结构,增加 section-card 包裹表单区域 - 调整相关组件 data、computed、watch 使用,确保第一次立即 watcher 正确读取路由数据 - 验证所有改动后相关页面 HTTP 200,标签页行为正常,解决「运单管理不存在」和合同详情空白问题
9897 lines
353 KiB
Vue
9897 lines
353 KiB
Vue
<!-- 运输计划专用页面:列表、计划表单、调度和详情均在此维护。 -->
|
||
<template>
|
||
<basic-container
|
||
:class="[
|
||
'transport-plan-page',
|
||
{
|
||
'transport-plan-page--form-page': isStandaloneFormPage,
|
||
},
|
||
]"
|
||
>
|
||
<component
|
||
:is="crudContainer"
|
||
:option="pageFormOption"
|
||
:form-page-title="isStandaloneFormPage ? formPageTitle : undefined"
|
||
:table-loading="loading"
|
||
:data="data"
|
||
v-model:page="page"
|
||
v-model="form"
|
||
:status="isStandaloneFormPage ? 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 || config.enableTransportPlanImport) &&
|
||
hasPermission(`${config.permission}_import`)
|
||
"
|
||
@click="handleImport"
|
||
>{{ config.importText || '批量导入' }}
|
||
</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="danger"
|
||
icon="el-icon-delete"
|
||
plain
|
||
v-if="config.batchDelete !== false && hasPermission(`${config.permission}_delete`)"
|
||
@click="handleDelete"
|
||
>批量删除
|
||
</el-button>
|
||
</template>
|
||
|
||
<template #businessStatus="{ row }">
|
||
<el-tag :type="statusTagType(row.businessStatus)" class="status-text">
|
||
{{ displayStatus(row, 'businessStatus') }}
|
||
</el-tag>
|
||
</template>
|
||
|
||
<template #goodsInfo="{ row }">
|
||
<span>{{ formatTransportPlanGoodsInfo(row) || '-' }}</span>
|
||
</template>
|
||
|
||
<template #contractName="{ row }">
|
||
<div class="transport-plan-page__contract-name">
|
||
<span>{{ row.contractName || '-' }}</span>
|
||
</div>
|
||
</template>
|
||
|
||
<template #departureAddress="{ row }">
|
||
<el-tooltip
|
||
v-if="row.departureAddress || row.departureName"
|
||
:content="row.departureAddress || row.departureName"
|
||
placement="top"
|
||
>
|
||
<span>{{ formatTransportPlanListAddress(row, 'departure') }}</span>
|
||
</el-tooltip>
|
||
<span v-else>-</span>
|
||
</template>
|
||
|
||
<template #arrivalAddress="{ row }">
|
||
<el-tooltip
|
||
v-if="row.arrivalAddress || row.arrivalName"
|
||
:content="row.arrivalAddress || row.arrivalName"
|
||
placement="top"
|
||
>
|
||
<span>{{ formatTransportPlanListAddress(row, 'arrival') }}</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 #projectName-form>
|
||
<el-select
|
||
v-model="selectedProjectId"
|
||
class="transport-plan-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="transport-plan-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="transport-plan-page__field"
|
||
placeholder="请输入合同名称"
|
||
clearable
|
||
maxlength="100"
|
||
show-word-limit
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</template>
|
||
|
||
<template #planName-form>
|
||
<el-input
|
||
v-model="form.planName"
|
||
class="transport-plan-page__field"
|
||
placeholder="请输入计划名称"
|
||
clearable
|
||
maxlength="50"
|
||
show-word-limit
|
||
:disabled="dialogReadonly || isTemplateCreate"
|
||
/>
|
||
</template>
|
||
|
||
<template #shippingInfoTitle-form>
|
||
<div
|
||
class="dialog-section-title dialog-section-title--action transport-plan-page__shipping-title"
|
||
>
|
||
<span>收发货信息</span>
|
||
<el-link
|
||
v-if="!dialogReadonly"
|
||
class="transport-plan-page__shipping-route-action"
|
||
type="primary"
|
||
:disabled="transportAddressSelectDisabled"
|
||
@click="openTransportRouteDialog"
|
||
>
|
||
选择线路
|
||
</el-link>
|
||
</div>
|
||
</template>
|
||
|
||
<template #departureAddress-form>
|
||
<div class="transport-plan-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="transport-plan-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="transport-plan-page__map-suffix"
|
||
@click.stop="openTransportMapDialog('departure')"
|
||
>
|
||
<Location />
|
||
</el-icon>
|
||
</template>
|
||
</el-input>
|
||
<el-tooltip content="选择常用地址" placement="top">
|
||
<el-button
|
||
class="transport-plan-page__address-pick-btn"
|
||
type="primary"
|
||
:icon="OfficeBuilding"
|
||
link
|
||
:disabled="transportAddressSelectDisabled"
|
||
@click="openTransportAddressPicker('departure')"
|
||
/>
|
||
</el-tooltip>
|
||
<span class="transport-plan-page__route-label">联系人</span>
|
||
<el-input
|
||
v-model="form.departureContact"
|
||
placeholder="请输入"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
<span class="transport-plan-page__route-label">联系方式</span>
|
||
<el-input v-model="form.departurePhone" placeholder="请输入" :disabled="dialogReadonly" />
|
||
</div>
|
||
</template>
|
||
|
||
<template #arrivalAddress-form>
|
||
<div class="transport-plan-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="transport-plan-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="transport-plan-page__map-suffix"
|
||
@click.stop="openTransportMapDialog('arrival')"
|
||
>
|
||
<Location />
|
||
</el-icon>
|
||
</template>
|
||
</el-input>
|
||
<el-tooltip content="选择常用地址" placement="top">
|
||
<el-button
|
||
class="transport-plan-page__address-pick-btn"
|
||
type="primary"
|
||
:icon="OfficeBuilding"
|
||
link
|
||
:disabled="transportAddressSelectDisabled"
|
||
@click="openTransportAddressPicker('arrival')"
|
||
/>
|
||
</el-tooltip>
|
||
<span class="transport-plan-page__route-label">联系人</span>
|
||
<el-input v-model="form.arrivalContact" placeholder="请输入" :disabled="dialogReadonly" />
|
||
<span class="transport-plan-page__route-label">联系方式</span>
|
||
<el-input v-model="form.arrivalPhone" placeholder="请输入" :disabled="dialogReadonly" />
|
||
</div>
|
||
</template>
|
||
|
||
<template #goodsInfoTitle-form>
|
||
<div
|
||
v-if="config.enableTransportPlanForm"
|
||
class="dialog-section-title dialog-section-title--action transport-plan-page__goods-title"
|
||
>
|
||
<span>货物信息</span>
|
||
<div v-if="!dialogReadonly" class="transport-plan-page__section-actions">
|
||
<!-- <el-link type="primary" @click="cargoImportBox = true">导入货物</el-link> -->
|
||
<el-link type="primary" @click="openCommonCargoDialog">常用货物</el-link>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<template #goodsJson-form>
|
||
<div v-if="config.enableTransportPlanForm" class="transport-plan-page__cargo-wrap">
|
||
<el-table
|
||
:data="transportCargoRows"
|
||
border
|
||
class="transport-plan-page__cargo-table"
|
||
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="transport-plan-page__required-column">货物类型</span>
|
||
</template>
|
||
<template #default="{ row }">
|
||
<el-cascader
|
||
v-model="row.cargoTypePath"
|
||
:options="transportCargoTypeOptions"
|
||
:props="transportCargoTypeCascaderProps"
|
||
:placeholder="row.cargoType || '请选择'"
|
||
clearable
|
||
filterable
|
||
:disabled="dialogReadonly"
|
||
:filter-method="filterBillingCargoType"
|
||
@visible-change="visible => visible && ensureBillingCargoTypeOptions()"
|
||
@change="value => handleTransportCargoTypeChange(row, value)"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="货物名称" min-width="240" align="center">
|
||
<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="transport-plan-page__cargo-autocomplete-item">
|
||
<span>{{ formatBillingCargoTypeLabel(item) }}</span>
|
||
</div>
|
||
</template>
|
||
</el-autocomplete>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="数量" min-width="120" align="center">
|
||
<template #default="{ row }">
|
||
<el-input
|
||
v-model="row.quantity"
|
||
placeholder="请输入"
|
||
:disabled="dialogReadonly"
|
||
@input="value => handleTransportCargoQuantityInput(row, value)"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="数量单位" min-width="120" align="center">
|
||
<template #default="{ row }">
|
||
<el-select v-model="row.quantityUnit" clearable :disabled="dialogReadonly">
|
||
<el-option
|
||
v-for="item in quantityUnitOptions"
|
||
:key="item"
|
||
:label="item"
|
||
:value="item"
|
||
/>
|
||
</el-select>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="包装" min-width="120" 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 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="transport-plan-page__cargo-actions">
|
||
<el-link
|
||
v-if="!dialogReadonly"
|
||
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 class="transport-plan-page__cargo-total">
|
||
运输数量合计:{{ transportCargoQuantityTotal }}
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<template #attachmentTitle-form>
|
||
<div class="dialog-section-title">{{ config.attachmentTitle || '其它附件' }}</div>
|
||
</template>
|
||
|
||
<template #attachmentsJson-form>
|
||
<div class="transport-plan-page__attachment">
|
||
<div class="transport-plan-page__attachment-head">
|
||
<el-button
|
||
type="primary"
|
||
:disabled="!attachmentRows.length"
|
||
@click="handleBatchDownload"
|
||
>
|
||
批量下载
|
||
</el-button>
|
||
</div>
|
||
<el-table
|
||
:data="attachmentRows"
|
||
border
|
||
class="transport-plan-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="transport-plan-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="isStandaloneFormPage && !showTransportPlanCreateActions"
|
||
class="transport-plan-page__standalone-menu-actions"
|
||
>
|
||
<el-button class="transport-plan-page__form-close" @click="closeCrudDialog">
|
||
取消
|
||
</el-button>
|
||
</div>
|
||
<div v-if="showTransportPlanCreateActions" class="transport-plan-page__create-actions">
|
||
<el-button :disabled="Boolean(transportPlanCreateActionLoading)" @click="closeCrudDialog">
|
||
取消
|
||
</el-button>
|
||
<el-button
|
||
type="primary"
|
||
plain
|
||
:loading="transportPlanCreateActionLoading === 'draft'"
|
||
:disabled="Boolean(transportPlanCreateActionLoading)"
|
||
@click="handleTransportPlanCreateAction('draft')"
|
||
>
|
||
暂存
|
||
</el-button>
|
||
<el-button
|
||
type="primary"
|
||
:loading="transportPlanCreateActionLoading === 'create'"
|
||
:disabled="Boolean(transportPlanCreateActionLoading)"
|
||
@click="handleTransportPlanCreateAction('create')"
|
||
>
|
||
确认创建
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
|
||
<template #menu="{ row, index }">
|
||
<div class="transport-plan-page__row-actions">
|
||
<el-link type="primary" v-if="showDetailButton(row)" @click="openDetail(row)"
|
||
>详情</el-link
|
||
>
|
||
<el-link
|
||
type="primary"
|
||
v-if="showViewButton(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="canCopy(row)" @click="handleCopy(row)"> 复制 </el-link>
|
||
<el-link type="primary" v-if="canComplete(row)" @click="handleAction('complete', row)">
|
||
{{ completeActionLabel }}
|
||
</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>
|
||
|
||
<empty-pagination
|
||
v-show="!isStandaloneFormPage"
|
||
:page="page"
|
||
@size-change="sizeChange"
|
||
@current-change="currentChange"
|
||
@load="onLoad(page, query)"
|
||
/>
|
||
|
||
<el-dialog
|
||
v-model="detailBox"
|
||
:title="isTransportPlanDetailLayout ? '' : `${config.title}详情`"
|
||
append-to-body
|
||
top="10px"
|
||
:width="isTransportPlanDetailLayout ? 'calc(100% - 16px)' : '96%'"
|
||
show-close
|
||
:class="[
|
||
'transport-plan-page__detail-dialog',
|
||
$attrs.option?.dialogCustomClass,
|
||
{ 'is-transport-plan-detail': isTransportPlanDetailLayout },
|
||
]"
|
||
>
|
||
<div v-loading="detailLoading" class="transport-plan-page__detail-content">
|
||
<template v-if="isTransportPlanDetailLayout">
|
||
<section class="transport-plan-page__transport-plan-detail-head">
|
||
<div class="transport-plan-page__transport-plan-detail-title-row">
|
||
<h2>
|
||
{{ detailRow.planName || detailRow.planNo || '运输计划' }}
|
||
<span>|</span>
|
||
{{ detailRow.planNo || '-' }}
|
||
</h2>
|
||
<span class="detail-status-text is-dispatching">
|
||
{{ displayStatus(detailRow, 'businessStatus') }}
|
||
</span>
|
||
<span class="detail-status-text detail-transport-type">
|
||
{{
|
||
detailRow.transportTypeName ||
|
||
formatTransportPlanDetailTransportType(detailRow.transportType)
|
||
}}
|
||
</span>
|
||
</div>
|
||
<div class="transport-plan-page__transport-plan-detail-overview">
|
||
<div class="transport-plan-page__transport-plan-detail-fields">
|
||
<div class="transport-plan-page__transport-plan-detail-field">
|
||
<span>客户</span>
|
||
<strong>{{ detailRow.customerName || '-' }}</strong>
|
||
</div>
|
||
<div class="transport-plan-page__transport-plan-detail-field">
|
||
<span>项目</span>
|
||
<strong>{{ detailRow.projectName || '-' }}</strong>
|
||
</div>
|
||
<div class="transport-plan-page__transport-plan-detail-field">
|
||
<span>合同编号</span>
|
||
<strong>{{ detailRow.contractNo || '-' }}</strong>
|
||
</div>
|
||
<div class="transport-plan-page__transport-plan-detail-field">
|
||
<span>货物信息</span>
|
||
<strong class="is-normal">{{ transportPlanGoodsText }}</strong>
|
||
</div>
|
||
<div class="transport-plan-page__transport-plan-detail-field">
|
||
<span>计划执行时间</span>
|
||
<strong class="is-normal">{{ transportPlanDateRange }}</strong>
|
||
</div>
|
||
<div class="transport-plan-page__transport-plan-detail-field is-attachments">
|
||
<span>附件</span>
|
||
<div class="transport-plan-page__transport-plan-detail-attachments">
|
||
<el-link
|
||
v-for="file in attachmentRows"
|
||
:key="file.url || file.name || file.originalName"
|
||
type="primary"
|
||
@click="previewAttachment(file)"
|
||
>
|
||
{{ attachmentName(file) }}
|
||
</el-link>
|
||
<span v-if="!attachmentRows.length">-</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="transport-plan-page__transport-plan-detail-route">
|
||
<div class="transport-plan-page__transport-plan-detail-route-point">
|
||
<b class="is-start">起</b>
|
||
<div>
|
||
<strong>{{ formatTransportPlanDetailRouteName('departure') }}</strong>
|
||
<p>{{ detailRow.departureAddress || '-' }}</p>
|
||
<p>
|
||
{{
|
||
[detailRow.departureContact, detailRow.departurePhone]
|
||
.filter(Boolean)
|
||
.join(' - ') || '-'
|
||
}}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<span class="transport-plan-page__transport-plan-detail-route-line" />
|
||
<div class="transport-plan-page__transport-plan-detail-route-point">
|
||
<b class="is-end">终</b>
|
||
<div>
|
||
<strong>{{ formatTransportPlanDetailRouteName('arrival') }}</strong>
|
||
<p>{{ detailRow.arrivalAddress || '-' }}</p>
|
||
<p>
|
||
{{
|
||
[detailRow.arrivalContact, detailRow.arrivalPhone]
|
||
.filter(Boolean)
|
||
.join(' - ') || '-'
|
||
}}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="transport-plan-page__transport-plan-waybill-card">
|
||
<div class="transport-plan-page__transport-plan-waybill-heading">
|
||
<h3>运单记录</h3>
|
||
<span>总量{{ transportPlanDispatchSummary.total }}吨</span>
|
||
<span
|
||
>,已调度 <em>{{ transportPlanDispatchSummary.assigned.toFixed(2) }}</em> 吨</span
|
||
>
|
||
<span
|
||
>,剩余 <em>{{ transportPlanDispatchSummary.remaining.toFixed(2) }}</em> 吨</span
|
||
>
|
||
</div>
|
||
<el-table
|
||
:data="transportPlanWaybillPageRows"
|
||
border
|
||
height="calc(100vh - 475px)"
|
||
empty-text="暂无运单记录"
|
||
class="transport-plan-page__transport-plan-waybill-table"
|
||
>
|
||
<el-table-column
|
||
type="index"
|
||
label="序号"
|
||
width="96"
|
||
align="center"
|
||
:index="transportPlanWaybillIndex"
|
||
/>
|
||
<el-table-column label="运单号" width="192">
|
||
<template #default="{ row }">
|
||
<el-link
|
||
v-if="row.id || row.waybillNo"
|
||
type="primary"
|
||
@click="openTransportPlanWaybillDetail(row)"
|
||
>
|
||
{{ row.waybillNo || '-' }}
|
||
</el-link>
|
||
<span v-else>-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="vehicleNo" label="车牌号/航班号/船号/班列号" width="200" />
|
||
<el-table-column prop="driverName" label="司机" width="128" />
|
||
<el-table-column prop="carrierName" label="承运商" width="192" />
|
||
<el-table-column label="运输方式" width="192">
|
||
<template #default="{ row }">
|
||
{{ formatTransportPlanDetailTransportType(row.transportType) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="carrierType" label="承运类型" width="192" />
|
||
<el-table-column prop="goodsInfo" label="货物信息" min-width="320" />
|
||
<el-table-column label="发货地址" width="220" show-overflow-tooltip>
|
||
<template #default="{ row }">
|
||
{{ formatTransportPlanProvinceCityDistrict(row.departureAddress) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="状态" width="80" align="center" fixed="right">
|
||
<template #default="{ row }">
|
||
<span
|
||
:class="['transport-plan-page__transport-plan-waybill-status', row.statusClass]"
|
||
>
|
||
{{ row.statusText || '-' }}
|
||
</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="80" align="center" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-link type="primary" @click="openTransportPlanWaybillDetail(row)">
|
||
详情
|
||
</el-link>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div class="transport-plan-page__transport-plan-waybill-pagination">
|
||
<el-pagination
|
||
v-model:current-page="transportPlanWaybillPage.currentPage"
|
||
v-model:page-size="transportPlanWaybillPage.pageSize"
|
||
:page-sizes="transportPlanWaybillPage.pageSizes"
|
||
layout="total, sizes, prev, pager, next"
|
||
:total="transportPlanWaybillRows.length"
|
||
@current-change="handleTransportPlanWaybillCurrentChange"
|
||
@size-change="handleTransportPlanWaybillSizeChange"
|
||
/>
|
||
</div>
|
||
</section>
|
||
</template>
|
||
<template v-else>
|
||
<section-card
|
||
v-for="section in detailSections"
|
||
:key="section.title"
|
||
:title="section.title"
|
||
>
|
||
<el-descriptions :column="2" border>
|
||
<el-descriptions-item
|
||
v-for="field in section.fields"
|
||
:key="field[0]"
|
||
:label="field[1]"
|
||
:span="field[2] || 1"
|
||
>
|
||
{{ formatDetailValue(detailRow, field[0]) }}
|
||
</el-descriptions-item>
|
||
</el-descriptions>
|
||
</section-card>
|
||
<section-card v-if="config.enableTransportPlanForm" title="货物信息">
|
||
<el-table :data="transportCargoRows" empty-text="暂无货物信息">
|
||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||
<el-table-column prop="cargoName" label="货物名称" min-width="180" />
|
||
<el-table-column prop="cargoType" label="货物类型" min-width="150" />
|
||
<el-table-column prop="quantity" label="数量" min-width="100" />
|
||
<el-table-column prop="quantityUnit" label="数量单位" min-width="110" />
|
||
<el-table-column prop="packageType" label="包装" min-width="110" />
|
||
<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="materialCode" label="物料编码" min-width="140" />
|
||
<el-table-column prop="deviceCode" label="设备编码" min-width="140" />
|
||
<el-table-column prop="remark" label="备注" min-width="160" />
|
||
</el-table>
|
||
</section-card>
|
||
<section-card
|
||
v-if="config.enableAttachmentTable"
|
||
:title="config.attachmentTitle || '附件'"
|
||
>
|
||
<div
|
||
v-if="config.detailAttachmentDescriptionPlain"
|
||
class="transport-plan-page__attachment-description"
|
||
>
|
||
<div
|
||
v-for="(row, index) in attachmentRows"
|
||
:key="row.id || row.uid || `${attachmentName(row)}-${index}`"
|
||
class="transport-plan-page__attachment-description-item"
|
||
>
|
||
<span class="transport-plan-page__attachment-description-label">
|
||
{{ attachmentName(row) }}:
|
||
</span>
|
||
<span>{{ row.description || '-' }}</span>
|
||
</div>
|
||
</div>
|
||
<el-table :data="attachmentRows" empty-text="暂无附件">
|
||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||
<el-table-column label="文件名" min-width="260">
|
||
<template #default="{ row }">
|
||
<el-link type="primary" @click="previewAttachment(row)">
|
||
{{ attachmentName(row) }}
|
||
</el-link>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
v-if="!config.detailAttachmentDescriptionPlain"
|
||
prop="description"
|
||
label="附件描述"
|
||
min-width="220"
|
||
/>
|
||
<el-table-column label="文件大小" width="110">
|
||
<template #default="{ row }">{{ formatFileSize(row.size) }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="uploadUserName" label="上传人" width="140" />
|
||
<el-table-column
|
||
prop="uploadTime"
|
||
label="上传时间"
|
||
:width="config.detailAttachmentUploadTimeWidth || 180"
|
||
sortable
|
||
/>
|
||
</el-table>
|
||
</section-card>
|
||
</template>
|
||
</div>
|
||
<template #footer>
|
||
<el-button type="primary" @click="detailBox = false">关闭</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<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-if="config.enableTransportPlanImport"
|
||
v-model="transportPlanImportBox"
|
||
:title="config.transportPlanImportTitle || `导入${config.title}`"
|
||
append-to-body
|
||
width="96%"
|
||
class="transport-plan-page__transport-plan-import-dialog"
|
||
@closed="resetTransportPlanImport"
|
||
>
|
||
<el-form
|
||
ref="transportPlanImportFormRef"
|
||
:model="transportPlanImportForm"
|
||
:rules="transportPlanImportRules"
|
||
label-position="right"
|
||
label-width="auto"
|
||
class="transport-plan-page__transport-plan-import-form"
|
||
>
|
||
<el-row :gutter="24">
|
||
<el-col :span="8">
|
||
<el-form-item label="项目" prop="projectId" required>
|
||
<el-select
|
||
v-model="transportPlanImportForm.projectId"
|
||
placeholder="模糊查询后选择"
|
||
filterable
|
||
clearable
|
||
:loading="projectLoading"
|
||
@visible-change="visible => visible && loadProjectOptions()"
|
||
@change="handleTransportPlanImportProjectChange"
|
||
>
|
||
<el-option
|
||
v-for="item in projectOptions"
|
||
:key="item.id"
|
||
:label="item.projectName"
|
||
:value="item.id"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<el-form-item label="客户" prop="customerName" required>
|
||
<el-input
|
||
v-model="transportPlanImportForm.customerName"
|
||
disabled
|
||
placeholder="选择项目后带出"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<el-form-item label="客户合同" prop="contractId" required>
|
||
<el-select
|
||
v-model="transportPlanImportForm.contractId"
|
||
placeholder="选择项目后选择"
|
||
filterable
|
||
clearable
|
||
:loading="transportPlanImportContractLoading"
|
||
:disabled="!transportPlanImportForm.projectId"
|
||
@change="handleTransportPlanImportContractChange"
|
||
>
|
||
<el-option
|
||
v-for="item in transportPlanImportContractOptions"
|
||
:key="item.id"
|
||
:label="item.contractName"
|
||
:value="item.id"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-form-item
|
||
label="计划明细表"
|
||
prop="file"
|
||
required
|
||
class="transport-plan-page__import-file-item"
|
||
>
|
||
<el-upload
|
||
action="#"
|
||
accept=".xls,.xlsx"
|
||
:auto-upload="false"
|
||
:limit="1"
|
||
:file-list="transportPlanImportFiles"
|
||
:on-change="handleTransportPlanImportFileChange"
|
||
:on-remove="handleTransportPlanImportFileRemove"
|
||
>
|
||
<el-button type="primary">添加附件</el-button>
|
||
</el-upload>
|
||
<span class="transport-plan-page__import-file-tip"
|
||
>请上传计划明细表,仅支持 Excel 格式</span
|
||
>
|
||
<el-link type="primary" @click="handleTransportPlanTemplate">下载模板</el-link>
|
||
</el-form-item>
|
||
</el-form>
|
||
<section v-if="transportPlanImportRows.length" class="transport-plan-page__import-preview">
|
||
<div class="transport-plan-page__import-preview-head">
|
||
<div class="dialog-section-title">数据明细</div>
|
||
<el-button
|
||
type="danger"
|
||
plain
|
||
:disabled="!transportPlanImportSelection.length"
|
||
@click="removeSelectedTransportPlanImportRows"
|
||
>
|
||
批量删除
|
||
</el-button>
|
||
</div>
|
||
<el-tabs
|
||
type="border-card"
|
||
v-model="transportPlanImportTab"
|
||
class="transport-plan-page__import-preview-tabs"
|
||
>
|
||
<el-tab-pane :label="`计划单数(${transportPlanImportRows.length})`" name="all" />
|
||
<el-tab-pane :label="`疑似重复(${transportPlanImportDuplicateCount})`" name="duplicate" />
|
||
</el-tabs>
|
||
<div class="transport-plan-page__import-preview-table">
|
||
<el-table
|
||
:data="transportPlanImportVisibleRows"
|
||
border
|
||
row-key="_key"
|
||
empty-text="暂无明细数据"
|
||
@selection-change="handleTransportPlanImportSelectionChange"
|
||
>
|
||
<el-table-column type="selection" width="52" fixed="left" />
|
||
<el-table-column type="index" label="序号" width="70" fixed="left" />
|
||
<el-table-column label="计划单号" min-width="150">
|
||
<template #default>待生成</template>
|
||
</el-table-column>
|
||
<el-table-column label="计划名称" min-width="180">
|
||
<template #default="{ row }">
|
||
<el-input v-if="isEditingTransportPlanImportRow(row)" v-model="row.planName" />
|
||
<span v-else>{{ row.planName || '-' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="运输类型" min-width="160">
|
||
<template #default="{ row }">
|
||
<el-input v-if="isEditingTransportPlanImportRow(row)" v-model="row.transportType" />
|
||
<span v-else>{{ row.transportType || '-' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="货物名称" min-width="160">
|
||
<template #default="{ row }">
|
||
<el-input v-if="isEditingTransportPlanImportRow(row)" v-model="row.cargoName" />
|
||
<span v-else>{{ row.cargoName || '-' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="货物类型" min-width="160">
|
||
<template #default="{ row }">
|
||
<el-input v-if="isEditingTransportPlanImportRow(row)" v-model="row.cargoType" />
|
||
<span v-else>{{ row.cargoType || '-' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="数量" min-width="120">
|
||
<template #default="{ row }">
|
||
<el-input v-if="isEditingTransportPlanImportRow(row)" v-model="row.quantity" />
|
||
<span v-else>{{ row.quantity || '-' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="计量单位" min-width="130">
|
||
<template #default="{ row }">
|
||
<el-select v-if="isEditingTransportPlanImportRow(row)" v-model="row.quantityUnit">
|
||
<el-option
|
||
v-for="item in quantityUnitOptions"
|
||
:key="item"
|
||
:label="item"
|
||
:value="item"
|
||
/>
|
||
</el-select>
|
||
<span v-else>{{ row.quantityUnit || '-' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="发货地址" min-width="260">
|
||
<template #default="{ row }">
|
||
<el-input
|
||
v-if="isEditingTransportPlanImportRow(row)"
|
||
v-model="row.departureAddress"
|
||
/>
|
||
<span v-else>{{ row.departureAddress || '-' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="发货联系人" min-width="140">
|
||
<template #default="{ row }">
|
||
<el-input
|
||
v-if="isEditingTransportPlanImportRow(row)"
|
||
v-model="row.departureContact"
|
||
/>
|
||
<span v-else>{{ row.departureContact || '-' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="发货联系人电话" min-width="160">
|
||
<template #default="{ row }">
|
||
<el-input
|
||
v-if="isEditingTransportPlanImportRow(row)"
|
||
v-model="row.departurePhone"
|
||
/>
|
||
<span v-else>{{ row.departurePhone || '-' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="到货地址" min-width="260">
|
||
<template #default="{ row }">
|
||
<el-input
|
||
v-if="isEditingTransportPlanImportRow(row)"
|
||
v-model="row.arrivalAddress"
|
||
/>
|
||
<span v-else>{{ row.arrivalAddress || '-' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="收货联系人" min-width="140">
|
||
<template #default="{ row }">
|
||
<el-input
|
||
v-if="isEditingTransportPlanImportRow(row)"
|
||
v-model="row.arrivalContact"
|
||
/>
|
||
<span v-else>{{ row.arrivalContact || '-' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="收货联系人电话" min-width="160">
|
||
<template #default="{ row }">
|
||
<el-input v-if="isEditingTransportPlanImportRow(row)" v-model="row.arrivalPhone" />
|
||
<span v-else>{{ row.arrivalPhone || '-' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="开始时间" min-width="140">
|
||
<template #default="{ row }">
|
||
<el-input v-if="isEditingTransportPlanImportRow(row)" v-model="row.planStartDate" />
|
||
<span v-else>{{ row.planStartDate || '-' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="结束时间" min-width="140">
|
||
<template #default="{ row }">
|
||
<el-input v-if="isEditingTransportPlanImportRow(row)" v-model="row.planEndDate" />
|
||
<span v-else>{{ row.planEndDate || '-' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="备注" min-width="180">
|
||
<template #default="{ row }">
|
||
<el-input v-if="isEditingTransportPlanImportRow(row)" v-model="row.remark" />
|
||
<span v-else>{{ row.remark || '-' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="170" fixed="right">
|
||
<template #default="{ row }">
|
||
<template v-if="isEditingTransportPlanImportRow(row)">
|
||
<el-link type="primary" @click="saveTransportPlanImportRow(row)">保存</el-link>
|
||
<el-link type="primary" @click="cancelTransportPlanImportRow(row)">取消</el-link>
|
||
</template>
|
||
<template v-else>
|
||
<el-link type="primary" @click="editTransportPlanImportRow(row)">编辑</el-link>
|
||
<el-link type="danger" @click="removeTransportPlanImportRow(row)">删除</el-link>
|
||
</template>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
</section>
|
||
<template #footer>
|
||
<el-button
|
||
type="primary"
|
||
:loading="transportPlanImportLoading"
|
||
@click="submitTransportPlanImport"
|
||
>提交</el-button
|
||
>
|
||
<el-button @click="transportPlanImportBox = 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="dispatchDialogTitle"
|
||
append-to-body
|
||
v-model="dispatchBox"
|
||
width="calc(100vw - 32px)"
|
||
top="16px"
|
||
class="transport-plan-page__dispatch-dialog"
|
||
@closed="resetDispatchDialog"
|
||
>
|
||
<div v-loading="dispatchLoading" class="transport-plan-page__dispatch-shell">
|
||
<div class="transport-plan-page__dispatch-top">
|
||
<div class="transport-plan-page__dispatch-heading">
|
||
<div class="transport-plan-page__dispatch-heading-title">
|
||
<span class="transport-plan-page__dispatch-heading-name">
|
||
{{ dispatchDialogTitle }}
|
||
</span>
|
||
<span class="transport-plan-page__dispatch-heading-no">
|
||
{{ dispatchDialogNo || '-' }}
|
||
</span>
|
||
<el-tag
|
||
:type="statusTagType(dispatchRow.businessStatus)"
|
||
effect="light"
|
||
class="status-text"
|
||
>
|
||
{{ dispatchDialogStatusText }}
|
||
</el-tag>
|
||
<el-tag type="primary" effect="light">
|
||
{{ formatDispatchTransportType(dispatchRow.transportType) || '公路整车' }}
|
||
</el-tag>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="transport-plan-page__dispatch-summary">
|
||
<section-card title="基本信息" class="transport-plan-page__dispatch-summary-card">
|
||
<div class="transport-plan-page__dispatch-summary-grid">
|
||
<div class="transport-plan-page__dispatch-summary-item">
|
||
<div class="transport-plan-page__dispatch-summary-label">客户</div>
|
||
<div class="transport-plan-page__dispatch-summary-value">
|
||
{{ dispatchRow.customerName || '-' }}
|
||
</div>
|
||
</div>
|
||
<div class="transport-plan-page__dispatch-summary-item">
|
||
<div class="transport-plan-page__dispatch-summary-label">项目</div>
|
||
<div class="transport-plan-page__dispatch-summary-value">
|
||
{{ dispatchRow.projectName || '-' }}
|
||
</div>
|
||
</div>
|
||
<div class="transport-plan-page__dispatch-summary-item">
|
||
<div class="transport-plan-page__dispatch-summary-label">合同编号</div>
|
||
<div class="transport-plan-page__dispatch-summary-value">
|
||
{{ dispatchRow.contractNo || '-' }}
|
||
</div>
|
||
</div>
|
||
<div class="transport-plan-page__dispatch-summary-item">
|
||
<div class="transport-plan-page__dispatch-summary-label">计划执行时间</div>
|
||
<div class="transport-plan-page__dispatch-summary-value">
|
||
{{
|
||
[dispatchRow.planStartDate, dispatchRow.planEndDate]
|
||
.filter(Boolean)
|
||
.join(' ~ ') || '-'
|
||
}}
|
||
</div>
|
||
</div>
|
||
<div class="transport-plan-page__dispatch-summary-item is-wide">
|
||
<div class="transport-plan-page__dispatch-summary-label">货物信息</div>
|
||
<div class="transport-plan-page__dispatch-summary-value">
|
||
{{ dispatchPlanCargoInfo || '-' }}
|
||
</div>
|
||
</div>
|
||
<div
|
||
class="transport-plan-page__dispatch-summary-item transport-plan-page__dispatch-summary-item--attachments"
|
||
>
|
||
<div class="transport-plan-page__dispatch-summary-label">附件</div>
|
||
<div class="transport-plan-page__dispatch-summary-value">
|
||
<el-link
|
||
v-for="(item, index) in dispatchAttachmentRows"
|
||
:key="dispatchAttachmentLabel(item) + index"
|
||
type="primary"
|
||
:href="dispatchAttachmentUrl(item) || undefined"
|
||
:underline="false"
|
||
target="_blank"
|
||
class="transport-plan-page__dispatch-attachment-link"
|
||
>
|
||
{{ dispatchAttachmentLabel(item) }}
|
||
</el-link>
|
||
<span v-if="!dispatchAttachmentRows.length">-</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section-card>
|
||
|
||
<section-card title="收发货路线" class="transport-plan-page__dispatch-route-card">
|
||
<div class="transport-plan-page__dispatch-route">
|
||
<div class="transport-plan-page__dispatch-route-item">
|
||
<span class="transport-plan-page__dispatch-route-badge is-start">起</span>
|
||
<div class="transport-plan-page__dispatch-route-body">
|
||
<div class="transport-plan-page__dispatch-route-title">
|
||
{{ parseDispatchRoutePoint(dispatchRow, 'start').name || '-' }}
|
||
</div>
|
||
<div class="transport-plan-page__dispatch-route-address">
|
||
{{
|
||
formatTransportPlanProvinceCityDistrict(
|
||
parseDispatchRoutePoint(dispatchRow, 'start').address
|
||
)
|
||
}}
|
||
</div>
|
||
<div class="transport-plan-page__dispatch-route-contact">
|
||
{{ parseDispatchRoutePoint(dispatchRow, 'start').contact || '-' }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="transport-plan-page__dispatch-route-vehicle">
|
||
<el-icon><Van /></el-icon>
|
||
</div>
|
||
<div class="transport-plan-page__dispatch-route-item">
|
||
<span class="transport-plan-page__dispatch-route-badge is-end">终</span>
|
||
<div class="transport-plan-page__dispatch-route-body">
|
||
<div class="transport-plan-page__dispatch-route-title">
|
||
{{ parseDispatchRoutePoint(dispatchRow, 'end').name || '-' }}
|
||
</div>
|
||
<div class="transport-plan-page__dispatch-route-address">
|
||
{{
|
||
formatTransportPlanProvinceCityDistrict(
|
||
parseDispatchRoutePoint(dispatchRow, 'end').address
|
||
)
|
||
}}
|
||
</div>
|
||
<div class="transport-plan-page__dispatch-route-contact">
|
||
{{ parseDispatchRoutePoint(dispatchRow, 'end').contact || '-' }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section-card>
|
||
</div>
|
||
</div>
|
||
|
||
<section-card class="transport-plan-page__dispatch-list-card">
|
||
<template #title>
|
||
<span>待调度列表</span>
|
||
<span class="transport-plan-page__dispatch-list-summary">
|
||
{{ dispatchSummaryText }}
|
||
</span>
|
||
</template>
|
||
<template #extra>
|
||
<el-button
|
||
type="primary"
|
||
:disabled="!dispatchHasAddableQuantity"
|
||
@click="handleDispatchAddRow"
|
||
>
|
||
新增
|
||
</el-button>
|
||
</template>
|
||
<el-table
|
||
:data="dispatchRows"
|
||
border
|
||
height="100%"
|
||
row-key="_key"
|
||
class="transport-plan-page__dispatch-table"
|
||
>
|
||
<el-table-column type="index" label="序号" width="70" align="center" fixed="left" />
|
||
<el-table-column label="运输方式" min-width="180" align="center" show-overflow-tooltip>
|
||
<template #default="{ row }">
|
||
{{ formatDispatchTransportType(row.transportType) || '-' }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="carrierType"
|
||
label="承运类型"
|
||
min-width="150"
|
||
align="center"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column
|
||
prop="carrierName"
|
||
label="承运商"
|
||
min-width="150"
|
||
align="center"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column
|
||
prop="driverName"
|
||
label="司机"
|
||
min-width="260"
|
||
align="center"
|
||
show-overflow-tooltip
|
||
class-name="transport-plan-page__dispatch-driver-column"
|
||
>
|
||
<template #default="{ row }">
|
||
<span class="transport-plan-page__dispatch-driver">{{
|
||
formatDispatchDriver(row)
|
||
}}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="vehicleNo"
|
||
label="车牌号/航班号/船号/班列号"
|
||
min-width="200"
|
||
align="center"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column
|
||
prop="cargoType"
|
||
label="货物类型"
|
||
min-width="170"
|
||
align="center"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column
|
||
prop="cargoInfo"
|
||
label="货物信息"
|
||
min-width="280"
|
||
align="center"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column label="发货地址" min-width="220" align="center" show-overflow-tooltip>
|
||
<template #default="{ row }">
|
||
{{ formatTransportPlanProvinceCityDistrict(row.departureAddress) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="收货地址" min-width="220" align="center" show-overflow-tooltip>
|
||
<template #default="{ row }">
|
||
{{ formatTransportPlanProvinceCityDistrict(row.arrivalAddress) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="unitPrice" label="单价" min-width="120" align="center" />
|
||
<el-table-column prop="freight" label="运费" min-width="120" align="center" />
|
||
<el-table-column
|
||
prop="otherFeeTotal"
|
||
label="其他费用合计"
|
||
min-width="140"
|
||
align="center"
|
||
/>
|
||
<el-table-column prop="freightTotal" label="运费合计" min-width="120" align="center" />
|
||
<el-table-column
|
||
prop="dispatchUserName"
|
||
label="调度人"
|
||
min-width="140"
|
||
align="center"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column
|
||
prop="dispatchTime"
|
||
label="调度时间"
|
||
min-width="160"
|
||
align="center"
|
||
show-overflow-tooltip
|
||
sortable
|
||
/>
|
||
<el-table-column label="操作" width="100" align="center" fixed="right">
|
||
<template #default="{ row, $index }">
|
||
<div class="transport-plan-page__dispatch-actions">
|
||
<el-link type="primary" @click="handleDispatchEditRow(row, $index)">编辑</el-link>
|
||
<el-link type="danger" @click="removeDispatchRow($index)">删除</el-link>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</section-card>
|
||
</div>
|
||
<template #footer>
|
||
<div class="transport-plan-page__dispatch-footer">
|
||
<el-button @click="dispatchBox = false">关闭</el-button>
|
||
<el-button
|
||
type="primary"
|
||
:loading="dispatchSaving === 'submit'"
|
||
@click="handleDispatchSubmit('submit')"
|
||
>
|
||
确认生成
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog
|
||
:title="dispatchItemDialogTitle"
|
||
append-to-body
|
||
v-model="dispatchItemBox"
|
||
width="96%"
|
||
top="8vh"
|
||
class="transport-plan-page__dispatch-item-dialog"
|
||
@closed="resetDispatchItemDialog"
|
||
>
|
||
<el-form
|
||
:model="dispatchItemForm"
|
||
label-position="right"
|
||
label-width="auto"
|
||
class="transport-plan-page__dispatch-item-form"
|
||
>
|
||
<div class="dialog-section-title">收发货信息</div>
|
||
<div class="transport-plan-page__dispatch-shipping-form">
|
||
<el-form-item label="发货地址" required>
|
||
<div class="transport-plan-page__route-address-row">
|
||
<el-input
|
||
v-model="dispatchItemForm.departureName"
|
||
:placeholder="dispatchStationNamePlaceholder"
|
||
:readonly="dispatchStationMode"
|
||
@click="openDispatchPrimaryAddressPicker('departure')"
|
||
>
|
||
<template v-if="dispatchStationMode" #suffix>
|
||
<el-icon
|
||
class="transport-plan-page__map-suffix"
|
||
@click.stop="openDispatchSecondaryAddressPicker('departure')"
|
||
>
|
||
<Search />
|
||
</el-icon>
|
||
</template>
|
||
</el-input>
|
||
<el-input
|
||
v-model="dispatchItemForm.departureAddress"
|
||
:placeholder="dispatchAddressDetailPlaceholder"
|
||
:readonly="dispatchStationMode"
|
||
@click="handleDispatchSecondaryAddressInputClick('departure')"
|
||
>
|
||
<template v-if="!dispatchStationMode" #suffix>
|
||
<el-link
|
||
type="primary"
|
||
:underline="false"
|
||
class="transport-plan-page__address-choose-link"
|
||
@click.stop="openTransportMapDialog('departure')"
|
||
>选择</el-link
|
||
>
|
||
</template>
|
||
</el-input>
|
||
<span class="transport-plan-page__route-label">联系人</span>
|
||
<el-input v-model="dispatchItemForm.departureContact" placeholder="请输入" />
|
||
<span class="transport-plan-page__route-label">联系方式</span>
|
||
<el-input v-model="dispatchItemForm.departurePhone" placeholder="请输入" />
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="收货地址" required>
|
||
<div class="transport-plan-page__route-address-row">
|
||
<el-input
|
||
v-model="dispatchItemForm.arrivalName"
|
||
:placeholder="dispatchStationNamePlaceholder"
|
||
:readonly="dispatchStationMode"
|
||
@click="openDispatchPrimaryAddressPicker('arrival')"
|
||
>
|
||
<template v-if="dispatchStationMode" #suffix>
|
||
<el-icon
|
||
class="transport-plan-page__map-suffix"
|
||
@click.stop="openDispatchSecondaryAddressPicker('arrival')"
|
||
>
|
||
<Search />
|
||
</el-icon>
|
||
</template>
|
||
</el-input>
|
||
<el-input
|
||
v-model="dispatchItemForm.arrivalAddress"
|
||
:placeholder="dispatchAddressDetailPlaceholder"
|
||
:readonly="dispatchStationMode"
|
||
@click="handleDispatchSecondaryAddressInputClick('arrival')"
|
||
>
|
||
<template v-if="!dispatchStationMode" #suffix>
|
||
<el-link
|
||
type="primary"
|
||
:underline="false"
|
||
class="transport-plan-page__address-choose-link"
|
||
@click.stop="openTransportMapDialog('arrival')"
|
||
>选择</el-link
|
||
>
|
||
</template>
|
||
</el-input>
|
||
<span class="transport-plan-page__route-label">联系人</span>
|
||
<el-input v-model="dispatchItemForm.arrivalContact" placeholder="请输入" />
|
||
<span class="transport-plan-page__route-label">联系方式</span>
|
||
<el-input v-model="dispatchItemForm.arrivalPhone" placeholder="请输入" />
|
||
</div>
|
||
</el-form-item>
|
||
</div>
|
||
|
||
<div class="transport-plan-page__dispatch-mode-switch">
|
||
<el-segmented
|
||
v-model="dispatchItemForm.taskEntryMode"
|
||
:options="dispatchTaskEntryModeOptions"
|
||
@change="handleDispatchTaskEntryModeChange"
|
||
/>
|
||
</div>
|
||
|
||
<template v-if="dispatchItemForm.taskEntryMode === 'full'">
|
||
<div
|
||
class="dialog-section-title dialog-section-title--action transport-plan-page__goods-title"
|
||
>
|
||
<span>货物信息</span>
|
||
<div class="transport-plan-page__section-actions">
|
||
<!-- <el-link type="primary" @click="cargoImportBox = true">导入货物</el-link> -->
|
||
<el-link type="primary" @click="openCommonCargoDialog">常用货物</el-link>
|
||
</div>
|
||
</div>
|
||
<div class="transport-plan-page__cargo-wrap">
|
||
<el-table
|
||
:data="dispatchItemCargoRows"
|
||
border
|
||
class="transport-plan-page__cargo-table transport-plan-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="transport-plan-page__required-column">货物类型</span></template
|
||
>
|
||
<template #default="{ row }">
|
||
<el-autocomplete
|
||
v-model="row.cargoType"
|
||
placeholder="请输入货物类型"
|
||
clearable
|
||
:debounce="300"
|
||
:fetch-suggestions="fetchDispatchConfiguredCargoTypeSuggestions"
|
||
@select="item => handleDispatchConfiguredCargoTypeSelect(row, item)"
|
||
@change="value => handleDispatchConfiguredCargoTypeChange(row, value)"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column min-width="240" align="center">
|
||
<template #header
|
||
><span class="transport-plan-page__required-column">货物名称</span></template
|
||
>
|
||
<template #default="{ row }">
|
||
<el-autocomplete
|
||
v-model="row.cargoName"
|
||
placeholder="请输入货物名称"
|
||
clearable
|
||
:debounce="300"
|
||
:fetch-suggestions="
|
||
(queryString, callback) =>
|
||
fetchDispatchConfiguredCargoNameSuggestions(queryString, callback, row)
|
||
"
|
||
@select="item => handleDispatchConfiguredCargoNameSelect(row, item)"
|
||
@change="value => handleDispatchConfiguredCargoNameChange(row, value)"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="包装" min-width="130" align="center">
|
||
<template #default="{ row }">
|
||
<el-select v-model="row.packageType" clearable>
|
||
<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 label="剩余数量" min-width="130" align="center">
|
||
<template #default="{ row }">
|
||
{{
|
||
isDispatchPlanQuantityUnlimited(row)
|
||
? '-'
|
||
: formatDispatchQuantity(dispatchItemRemainingQuantity(row))
|
||
}}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column min-width="130" align="center">
|
||
<template #header
|
||
><span class="transport-plan-page__required-column">本次数量</span></template
|
||
>
|
||
<template #default="{ row }">
|
||
<el-input
|
||
v-model="row.quantity"
|
||
placeholder="请输入"
|
||
@input="value => handleDispatchCargoQuantityInput(row, value)"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column min-width="140" align="center">
|
||
<template #header
|
||
><span class="transport-plan-page__required-column">数量单位</span></template
|
||
>
|
||
<template #default="{ row }">
|
||
<el-select
|
||
v-model="row.quantityUnit"
|
||
clearable
|
||
@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="请输入" /></template
|
||
></el-table-column>
|
||
<el-table-column label="规格" min-width="130" align="center"
|
||
><template #default="{ row }"
|
||
><el-input v-model="row.specification" placeholder="请输入" /></template
|
||
></el-table-column>
|
||
<el-table-column label="型号" min-width="130" align="center"
|
||
><template #default="{ row }"
|
||
><el-input v-model="row.model" placeholder="请输入" /></template
|
||
></el-table-column>
|
||
<el-table-column label="物料编码" min-width="150" align="center"
|
||
><template #default="{ row }"
|
||
><el-input v-model="row.materialCode" placeholder="请输入" /></template
|
||
></el-table-column>
|
||
<el-table-column label="设备编码" min-width="150" align="center"
|
||
><template #default="{ row }"
|
||
><el-input v-model="row.deviceCode" placeholder="请输入" /></template
|
||
></el-table-column>
|
||
<el-table-column label="备注" min-width="180" align="center"
|
||
><template #default="{ row }"
|
||
><el-input v-model="row.remark" placeholder="请输入" /></template
|
||
></el-table-column>
|
||
<el-table-column label="操作" width="140" align="center" fixed="right">
|
||
<template #default="{ $index }">
|
||
<div class="transport-plan-page__cargo-actions">
|
||
<el-link
|
||
v-if="$index === dispatchItemCargoRows.length - 1"
|
||
type="primary"
|
||
@click="addDispatchCargoRow($index)"
|
||
>新增</el-link
|
||
>
|
||
<el-link type="danger" @click="removeDispatchCargoRow($index)">删除</el-link>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
<div class="dialog-section-title">费用信息</div>
|
||
<div
|
||
class="transport-plan-page__dispatch-item-grid transport-plan-page__dispatch-item-grid--full"
|
||
>
|
||
<template v-for="group in dispatchItemFreightGroups" :key="group.key">
|
||
<el-form-item label="单价">
|
||
<el-input
|
||
:model-value="dispatchFreightGroupUnitPrice(group)"
|
||
placeholder="请输入"
|
||
@input="value => handleDispatchFreightGroupNumberInput(group, value)"
|
||
@clear="() => handleDispatchFreightGroupNumberInput(group, '')"
|
||
>
|
||
<template #append
|
||
><el-select
|
||
:model-value="dispatchFreightGroupPriceUnit(group)"
|
||
class="transport-plan-page__dispatch-unit-select"
|
||
placeholder="元/吨"
|
||
@change="value => handleDispatchFreightGroupPriceUnitChange(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="dispatchFreightGroupQuantity(group)" disabled
|
||
><template #append>{{ fixedQuantityUnit }}</template></el-input
|
||
></el-form-item
|
||
>
|
||
<el-form-item label="运费"
|
||
><el-input
|
||
:model-value="dispatchFreightGroupAmount(group)"
|
||
placeholder="请输入运费"
|
||
@input="value => handleDispatchFreightGroupAmountInput(group, value)"
|
||
@clear="() => handleDispatchFreightGroupAmountInput(group, '')"
|
||
><template #suffix>{{ dispatchItemFreightCurrencyLabel }}</template></el-input
|
||
></el-form-item
|
||
>
|
||
</template>
|
||
<el-form-item label="其他运费合计"
|
||
><el-input
|
||
v-model="dispatchItemForm.otherFeeTotal"
|
||
placeholder="请输入"
|
||
@input="value => handleDispatchNumberInput('otherFeeTotal', value)"
|
||
><template #suffix>{{ dispatchItemFreightCurrencyLabel }}</template></el-input
|
||
></el-form-item
|
||
>
|
||
<el-form-item label="运费合计"
|
||
><el-input :model-value="dispatchItemFreightTotal" disabled
|
||
><template #suffix>{{ dispatchItemFreightCurrencyLabel }}</template></el-input
|
||
></el-form-item
|
||
>
|
||
</div>
|
||
</template>
|
||
<template v-else>
|
||
<div class="dialog-section-title transport-plan-page__dispatch-task-content-title">
|
||
任务信息
|
||
</div>
|
||
<div
|
||
class="transport-plan-page__dispatch-item-grid transport-plan-page__dispatch-item-grid--compact"
|
||
>
|
||
<el-form-item
|
||
label="承运类型"
|
||
required
|
||
class="transport-plan-page__dispatch-item-carrier-type"
|
||
>
|
||
<el-radio-group
|
||
v-model="dispatchItemForm.carrierType"
|
||
@change="handleDispatchCarrierTypeChange"
|
||
>
|
||
<el-radio-button v-for="item in taskCarrierTypes" :key="item" :label="item" />
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
<el-form-item :label="dispatchCarrierLabel" :required="dispatchCarrierRequired">
|
||
<el-select
|
||
v-model="dispatchCarrierSelection"
|
||
:placeholder="`请选择${dispatchCarrierLabel}`"
|
||
filterable
|
||
clearable
|
||
:loading="dispatchCarrierLoading"
|
||
@visible-change="visible => visible && loadDispatchCarrierOptions()"
|
||
@change="handleDispatchCarrierChange"
|
||
>
|
||
<el-option
|
||
v-for="item in dispatchCarrierOptions"
|
||
:key="
|
||
item.id || item.customerName || item.carrierName || item.fullName || item.name
|
||
"
|
||
:label="
|
||
item.label ||
|
||
item.customerName ||
|
||
item.carrierName ||
|
||
item.fullName ||
|
||
item.name
|
||
"
|
||
:value="
|
||
dispatchIsCarrierMode
|
||
? item.carrierContractId
|
||
: item.value ||
|
||
item.customerName ||
|
||
item.carrierName ||
|
||
item.fullName ||
|
||
item.name
|
||
"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="司机" :required="dispatchItemForm.carrierType !== '承运商'">
|
||
<el-autocomplete
|
||
v-model="dispatchItemForm.driverName"
|
||
placeholder="请输入"
|
||
clearable
|
||
:debounce="300"
|
||
:fetch-suggestions="fetchTaskDriverSuggestions"
|
||
:loading="taskDriverLoading"
|
||
@select="item => handleTaskDriverSelect('dispatch', item)"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item
|
||
:label="dispatchItemForm.carrierType === '承运商' ? '手机号' : '司机手机号'"
|
||
:required="dispatchItemForm.carrierType !== '承运商'"
|
||
>
|
||
<el-input v-model="dispatchItemForm.driverPhone" placeholder="请输入" />
|
||
</el-form-item>
|
||
<el-form-item label="车牌号" required>
|
||
<el-input v-model="dispatchItemForm.vehicleNo" placeholder="请输入" />
|
||
</el-form-item>
|
||
<el-form-item v-if="dispatchItemForm.carrierType !== '承运商'" label="挂车车牌号">
|
||
<el-input v-model="dispatchItemForm.trailerVehicleNo" placeholder="请输入" />
|
||
</el-form-item>
|
||
<el-form-item v-if="dispatchItemForm.carrierType !== '承运商'" label="押运人">
|
||
<el-autocomplete
|
||
v-model="dispatchItemForm.escortName"
|
||
placeholder="请输入"
|
||
clearable
|
||
:debounce="300"
|
||
:fetch-suggestions="fetchTaskEscortSuggestions"
|
||
:loading="taskEscortLoading"
|
||
@select="handleTaskEscortSelect"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item v-if="dispatchItemForm.carrierType !== '承运商'" label="押运人手机号">
|
||
<el-input v-model="dispatchItemForm.escortPhone" placeholder="请输入" />
|
||
</el-form-item>
|
||
<el-form-item label="货物类型" required>
|
||
<el-autocomplete
|
||
v-model="dispatchItemForm.cargoType"
|
||
placeholder="请输入货物类型"
|
||
clearable
|
||
:debounce="300"
|
||
:fetch-suggestions="fetchDispatchConfiguredCargoTypeSuggestions"
|
||
@select="item => handleDispatchConfiguredCargoTypeSelect(dispatchItemForm, item)"
|
||
@change="value => handleDispatchConfiguredCargoTypeChange(dispatchItemForm, value)"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="货物名称" required>
|
||
<el-autocomplete
|
||
v-model="dispatchItemForm.cargoName"
|
||
placeholder="请输入货物名称"
|
||
clearable
|
||
:debounce="300"
|
||
:fetch-suggestions="
|
||
(queryString, callback) =>
|
||
fetchDispatchConfiguredCargoNameSuggestions(
|
||
queryString,
|
||
callback,
|
||
dispatchItemForm
|
||
)
|
||
"
|
||
@select="item => handleDispatchConfiguredCargoNameSelect(dispatchItemForm, item)"
|
||
@change="value => handleDispatchConfiguredCargoNameChange(dispatchItemForm, value)"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="规格">
|
||
<el-input v-model="dispatchItemForm.specification" placeholder="请输入" />
|
||
</el-form-item>
|
||
<el-form-item label="型号">
|
||
<el-input v-model="dispatchItemForm.model" placeholder="请输入" />
|
||
</el-form-item>
|
||
<el-form-item label="本次数量" required>
|
||
<el-input v-model="dispatchItemForm.quantity" placeholder="请输入">
|
||
<template #append>
|
||
<el-select
|
||
v-model="dispatchItemForm.quantityUnit"
|
||
class="transport-plan-page__dispatch-unit-select"
|
||
clearable
|
||
>
|
||
<el-option
|
||
v-for="item in quantityUnitOptions"
|
||
:key="item"
|
||
:label="item"
|
||
:value="item"
|
||
/>
|
||
</el-select>
|
||
</template>
|
||
</el-input>
|
||
<div
|
||
v-if="!isDispatchPlanQuantityUnlimited(dispatchItemForm)"
|
||
class="transport-plan-page__dispatch-quantity-hint"
|
||
>
|
||
剩余数量:{{
|
||
formatDispatchQuantity(dispatchItemRemainingQuantity(dispatchItemForm))
|
||
}}
|
||
{{ dispatchItemForm.quantityUnit || '吨' }}
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="里程(公里)">
|
||
<el-input
|
||
v-model="dispatchItemForm.mileage"
|
||
placeholder="请输入"
|
||
maxlength="10"
|
||
@input="handleDispatchMileageInput"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="预计发货日期">
|
||
<el-date-picker
|
||
v-model="dispatchItemForm.estimatedStartTime"
|
||
type="date"
|
||
format="YYYY-MM-DD"
|
||
value-format="YYYY-MM-DD"
|
||
placeholder="请选择"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="预计完成日期">
|
||
<el-date-picker
|
||
v-model="dispatchItemForm.estimatedEndTime"
|
||
type="date"
|
||
format="YYYY-MM-DD"
|
||
value-format="YYYY-MM-DD"
|
||
placeholder="请选择"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="单价">
|
||
<el-input v-model="dispatchItemForm.unitPrice" placeholder="请输入">
|
||
<template #append>
|
||
<el-select
|
||
v-model="dispatchItemForm.priceUnit"
|
||
class="transport-plan-page__dispatch-unit-select"
|
||
placeholder="元/吨"
|
||
:loading="taskFeeUnitLoading"
|
||
@visible-change="visible => visible && loadTaskFeeUnitOptions()"
|
||
>
|
||
<el-option
|
||
v-if="!taskFeeUnitOptions.some(item => item.value === '元/吨')"
|
||
label="元/吨"
|
||
value="元/吨"
|
||
/>
|
||
<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="dispatchItemForm.freightAmount"
|
||
placeholder="请输入运费"
|
||
@input="value => handleDispatchNumberInput('freightAmount', value)"
|
||
>
|
||
<template #suffix>{{ dispatchItemFreightCurrencyLabel }}</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
<el-form-item label="其他费用合计">
|
||
<el-input v-model="dispatchItemForm.otherFeeTotal" placeholder="请输入">
|
||
<template #suffix>{{ dispatchItemFreightCurrencyLabel }}</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
<el-form-item label="运费合计">
|
||
<el-input :model-value="dispatchItemFreightTotal" disabled>
|
||
<template #suffix>{{ dispatchItemFreightCurrencyLabel }}</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
<el-form-item label="备注" class="transport-plan-page__dispatch-item-span-3">
|
||
<el-input
|
||
v-model="dispatchItemForm.remark"
|
||
type="textarea"
|
||
:rows="2"
|
||
maxlength="200"
|
||
show-word-limit
|
||
:disabled="dialogReadonly"
|
||
placeholder="请输入,200字以内"
|
||
/>
|
||
</el-form-item>
|
||
</div>
|
||
</template>
|
||
|
||
<div
|
||
v-if="dispatchItemForm.taskEntryMode === 'full'"
|
||
class="dialog-section-title transport-plan-page__dispatch-task-content-title"
|
||
>
|
||
任务信息
|
||
</div>
|
||
<div
|
||
v-if="dispatchItemForm.taskEntryMode === 'full'"
|
||
class="transport-plan-page__dispatch-item-grid transport-plan-page__dispatch-item-grid--carrier"
|
||
>
|
||
<el-form-item label="承运类型" required class="transport-plan-page__dispatch-item-span-4">
|
||
<el-radio-group
|
||
v-model="dispatchItemForm.carrierType"
|
||
@change="handleDispatchCarrierTypeChange"
|
||
>
|
||
<el-radio-button v-for="item in taskCarrierTypes" :key="item" :label="item" />
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
<el-form-item :label="dispatchCarrierLabel" :required="dispatchCarrierRequired">
|
||
<el-select
|
||
v-model="dispatchCarrierSelection"
|
||
:placeholder="`请选择${dispatchCarrierLabel}`"
|
||
filterable
|
||
clearable
|
||
:loading="dispatchCarrierLoading"
|
||
@visible-change="visible => visible && loadDispatchCarrierOptions()"
|
||
@change="handleDispatchCarrierChange"
|
||
>
|
||
<el-option
|
||
v-for="item in dispatchCarrierOptions"
|
||
:key="
|
||
item.id || item.customerName || item.carrierName || item.fullName || item.name
|
||
"
|
||
:label="
|
||
item.label || item.customerName || item.carrierName || item.fullName || item.name
|
||
"
|
||
:value="
|
||
dispatchIsCarrierMode
|
||
? item.carrierContractId
|
||
: item.value ||
|
||
item.customerName ||
|
||
item.carrierName ||
|
||
item.fullName ||
|
||
item.name
|
||
"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item
|
||
v-if="dispatchIsRoadTransport"
|
||
label="司机"
|
||
:required="dispatchItemForm.carrierType === '自运'"
|
||
>
|
||
<el-autocomplete
|
||
v-model="dispatchItemForm.driverName"
|
||
placeholder="请输入"
|
||
clearable
|
||
:debounce="300"
|
||
:fetch-suggestions="fetchTaskDriverSuggestions"
|
||
:loading="taskDriverLoading"
|
||
@select="item => handleTaskDriverSelect('dispatch', item)"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item
|
||
v-if="dispatchIsRoadTransport"
|
||
label="手机号"
|
||
:required="dispatchItemForm.carrierType === '自运'"
|
||
>
|
||
<el-input v-model="dispatchItemForm.driverPhone" placeholder="请输入" />
|
||
</el-form-item>
|
||
<el-form-item
|
||
v-if="dispatchItemForm.carrierType"
|
||
:label="dispatchIsNonRoadTransport ? '船/航/班列号' : '车牌号'"
|
||
required
|
||
>
|
||
<el-input v-model="dispatchItemForm.vehicleNo" placeholder="请输入" />
|
||
</el-form-item>
|
||
<el-form-item v-if="dispatchIsNonRoadTransport" label="船长">
|
||
<el-input v-model="dispatchItemForm.captainName" placeholder="请输入船长" />
|
||
</el-form-item>
|
||
<el-form-item v-if="dispatchIsNonRoadTransport && dispatchIsCarrierMode" label="联系电话">
|
||
<el-input v-model="dispatchItemForm.driverPhone" placeholder="请输入联系电话" />
|
||
</el-form-item>
|
||
<el-form-item v-if="dispatchIsNonRoadTransport" label="箱号">
|
||
<el-input v-model="dispatchItemForm.containerNo" placeholder="请输入箱号" />
|
||
</el-form-item>
|
||
<el-form-item v-if="dispatchIsNonRoadTransport" label="舱位">
|
||
<el-input v-model="dispatchItemForm.cabinNo" placeholder="请输入舱位" />
|
||
</el-form-item>
|
||
<el-form-item
|
||
v-if="dispatchIsRoadTransport && dispatchItemForm.carrierType !== '承运商'"
|
||
label="挂车车牌号"
|
||
>
|
||
<el-input v-model="dispatchItemForm.trailerVehicleNo" placeholder="请输入" />
|
||
</el-form-item>
|
||
<el-form-item
|
||
v-if="dispatchIsRoadTransport && dispatchItemForm.carrierType !== '承运商'"
|
||
label="押运人"
|
||
>
|
||
<el-autocomplete
|
||
v-model="dispatchItemForm.escortName"
|
||
placeholder="请输入"
|
||
clearable
|
||
:debounce="300"
|
||
:fetch-suggestions="fetchTaskEscortSuggestions"
|
||
:loading="taskEscortLoading"
|
||
@select="handleTaskEscortSelect"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item
|
||
v-if="dispatchIsRoadTransport && dispatchItemForm.carrierType !== '承运商'"
|
||
label="押运人手机号"
|
||
>
|
||
<el-input v-model="dispatchItemForm.escortPhone" placeholder="请输入" />
|
||
</el-form-item>
|
||
<el-form-item
|
||
v-if="dispatchIsRoadTransport && dispatchItemForm.carrierType !== '承运商'"
|
||
label="里程(km)"
|
||
>
|
||
<el-input
|
||
v-model="dispatchItemForm.mileage"
|
||
placeholder="请输入"
|
||
maxlength="10"
|
||
@input="handleDispatchMileageInput"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="预计发货日期">
|
||
<el-date-picker
|
||
v-model="dispatchItemForm.estimatedStartTime"
|
||
type="date"
|
||
format="YYYY-MM-DD"
|
||
value-format="YYYY-MM-DD"
|
||
placeholder="请选择"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="预计完成日期">
|
||
<el-date-picker
|
||
v-model="dispatchItemForm.estimatedEndTime"
|
||
type="date"
|
||
format="YYYY-MM-DD"
|
||
value-format="YYYY-MM-DD"
|
||
placeholder="请选择"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="备注" class="transport-plan-page__dispatch-item-span-2">
|
||
<el-input
|
||
v-model="dispatchItemForm.remark"
|
||
type="textarea"
|
||
:rows="2"
|
||
maxlength="200"
|
||
show-word-limit
|
||
:disabled="dialogReadonly"
|
||
placeholder="请输入,200字以内"
|
||
/>
|
||
</el-form-item>
|
||
</div>
|
||
|
||
<div class="dialog-section-title">附件</div>
|
||
<div class="transport-plan-page__attachment">
|
||
<div class="transport-plan-page__attachment-head">
|
||
<el-button
|
||
type="primary"
|
||
:disabled="!dispatchItemAttachmentRows.length"
|
||
@click="handleDispatchItemBatchDownload"
|
||
>
|
||
批量下载
|
||
</el-button>
|
||
</div>
|
||
<el-table
|
||
:data="dispatchItemAttachmentRows"
|
||
border
|
||
class="transport-plan-page__attachment-table"
|
||
@selection-change="handleDispatchItemAttachmentSelectionChange"
|
||
>
|
||
<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="center" show-overflow-tooltip>
|
||
<template #default="{ row }">
|
||
<el-link type="primary" @click="previewAttachment(row, dispatchItemAttachmentRows)">
|
||
{{ attachmentName(row) }}
|
||
</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="请输入附件描述" />
|
||
</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="{ $index }">
|
||
<el-link type="danger" @click="removeDispatchItemAttachment($index)">删除</el-link>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div class="transport-plan-page__attachment-upload">
|
||
<vehicle-attachment-upload
|
||
v-model="dispatchItemAttachmentRows"
|
||
:file-types="attachmentFileTypes"
|
||
:max-size="50"
|
||
:show-file-list="false"
|
||
button-text="上传附件"
|
||
@change="handleDispatchItemAttachmentChange"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="dispatchItemBox = false">取消</el-button>
|
||
<el-button type="primary" @click="saveDispatchItem">提交</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog
|
||
title="选择线路"
|
||
append-to-body
|
||
v-model="transportRouteBox"
|
||
width="1280px"
|
||
@opened="loadTransportRouteList"
|
||
>
|
||
<div class="transport-plan-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="transport-plan-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="transport-plan-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="transport-plan-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="transport-plan-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="transport-plan-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="transport-plan-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="transport-plan-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="transport-plan-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="transport-plan-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="transport-plan-page__map"></div>
|
||
</div>
|
||
<div class="transport-plan-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="transport-plan-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="billingCargoTypeOptions"
|
||
:props="billingCargoTypeCascaderProps"
|
||
placeholder="请选择"
|
||
clearable
|
||
filterable
|
||
:filter-method="filterBillingCargoType"
|
||
@visible-change="visible => visible && ensureBillingCargoTypeOptions()"
|
||
@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="transport-plan-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">
|
||
<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="transport-plan-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="transport-plan-page__cargo-import-dialog"
|
||
@closed="cargoImportRows = []"
|
||
>
|
||
<div class="transport-plan-page__cargo-import">
|
||
<div class="transport-plan-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>
|
||
</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 getCommonRouteList } from '@/api/business/common-route';
|
||
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 { applyTableMenuWidth } from '@/utils/table-menu';
|
||
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||
import { isMobile } from '@/utils/validate';
|
||
import { Location, OfficeBuilding, Search } 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 standaloneTransportPlanFormRoutes = {
|
||
'/business/transport-plan': '/business/transport-plan/form',
|
||
};
|
||
const standaloneTransportPlanFormPaths = [
|
||
...Object.keys(standaloneTransportPlanFormRoutes),
|
||
...Object.values(standaloneTransportPlanFormRoutes),
|
||
];
|
||
|
||
const attachmentViewerPlugins = [
|
||
imagePlugin(),
|
||
pdfPlugin({ workerSrc: pdfWorkerSrc, useFetchData: true }),
|
||
officePlugin({ pdf: { workerSrc: pdfWorkerSrc, useFetchData: true } }),
|
||
textPlugin(),
|
||
fallbackPlugin(),
|
||
];
|
||
|
||
// 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);
|
||
};
|
||
|
||
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));
|
||
const dialogClass = attrs.option?.dialogCustomClass || 'transport-plan-form-page-dialog';
|
||
const groupOnly = attrs.option?.column?.length === 0 && attrs.option?.group?.length;
|
||
return h(
|
||
'div',
|
||
{ class: [dialogClass, { 'transport-plan-form-page-dialog--group-only': groupOnly }] },
|
||
children
|
||
);
|
||
},
|
||
};
|
||
|
||
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 transportPlanImportColumns = [
|
||
['计划名称', 'planName'],
|
||
['运输方式', 'transportType'],
|
||
['计划开始日期', 'planStartDate'],
|
||
['计划结束日期', 'planEndDate'],
|
||
['货物名称', 'cargoName'],
|
||
['货物类型', 'cargoType'],
|
||
['数量', 'quantity'],
|
||
['数量单位', 'quantityUnit'],
|
||
['包装', 'packageType'],
|
||
['品牌', 'brand'],
|
||
['规格', 'specification'],
|
||
['型号', 'model'],
|
||
['物料编码', 'materialCode'],
|
||
['设备编码', 'deviceCode'],
|
||
['发货地址', 'departureAddress'],
|
||
['发货联系人', 'departureContact'],
|
||
['发货联系方式', 'departurePhone'],
|
||
['收货地址', 'arrivalAddress'],
|
||
['收货联系人', 'arrivalContact'],
|
||
['收货联系方式', 'arrivalPhone'],
|
||
['备注', 'remark'],
|
||
];
|
||
|
||
const TRANSPORT_PLAN_QUANTITY_UNIT = '吨';
|
||
|
||
const defaultTransportCargo = () => ({
|
||
cargoName: '',
|
||
cargoType: '',
|
||
cargoTypeCode: '',
|
||
cargoTypePath: [],
|
||
quantity: '',
|
||
quantityUnit: '',
|
||
unitPrice: '',
|
||
priceUnit: '元/吨',
|
||
freightAmount: '',
|
||
packageType: '',
|
||
brand: '',
|
||
specification: '',
|
||
model: '',
|
||
materialCode: '',
|
||
deviceCode: '',
|
||
remark: '',
|
||
});
|
||
|
||
const defaultDispatchRow = () => ({
|
||
taskEntryMode: 'simple',
|
||
transportType: '',
|
||
carrierType: '承运商',
|
||
carrierId: '',
|
||
carrierContractId: '',
|
||
carrierName: '',
|
||
driverId: '',
|
||
driverName: '',
|
||
driverPhone: '',
|
||
vehicleNo: '',
|
||
captainName: '',
|
||
cabinNo: '',
|
||
containerNo: '',
|
||
trailerVehicleNo: '',
|
||
escortName: '',
|
||
escortPhone: '',
|
||
mileage: '',
|
||
cargoType: '',
|
||
cargoTypeCode: '',
|
||
cargoTypePath: [],
|
||
cargoName: '',
|
||
cargoInfo: '',
|
||
quantity: '',
|
||
quantityUnit: '吨',
|
||
specification: '',
|
||
model: '',
|
||
packageType: '',
|
||
brand: '',
|
||
materialCode: '',
|
||
unitPrice: '',
|
||
priceUnit: '元/吨',
|
||
freightAmount: '',
|
||
otherFeeTotal: '',
|
||
freightCurrency: 'RMB',
|
||
freightJson: '',
|
||
goodsJson: '',
|
||
estimatedStartTime: '',
|
||
estimatedEndTime: '',
|
||
departureAddress: '',
|
||
departureName: '',
|
||
departureContact: '',
|
||
departurePhone: '',
|
||
arrivalAddress: '',
|
||
arrivalName: '',
|
||
arrivalContact: '',
|
||
arrivalPhone: '',
|
||
dispatchUserName: '',
|
||
dispatchTime: '',
|
||
attachmentsJson: '',
|
||
remark: '',
|
||
});
|
||
|
||
const taskCarrierTypes = ['承运商', '自运'];
|
||
|
||
export default {
|
||
components: {
|
||
PageAvueForm,
|
||
ElImageViewer,
|
||
OpenFileViewer,
|
||
},
|
||
name: 'TransportPlanPage',
|
||
props: {
|
||
api: {
|
||
type: Object,
|
||
required: true,
|
||
},
|
||
config: {
|
||
type: Object,
|
||
required: true,
|
||
},
|
||
crudOption: {
|
||
type: Object,
|
||
required: true,
|
||
},
|
||
// 可选:固定操作列宽度;不传则按按钮数量自动估算(沿用既有逻辑)
|
||
menuWidth: {
|
||
type: Number,
|
||
default: null,
|
||
},
|
||
detailId: {
|
||
type: [String, Number],
|
||
default: null,
|
||
},
|
||
standaloneFormPage: {
|
||
type: Boolean,
|
||
default: false,
|
||
},
|
||
},
|
||
data() {
|
||
return {
|
||
Location,
|
||
OfficeBuilding,
|
||
Search,
|
||
// 实例所属路由的路径快照,用于在 watcher / 跳转前判断当前路由是否还是自己的。
|
||
// 必须放 data:Options API 的顺序是 data → computed → watch(immediate) → created,
|
||
// 放 created 的话首个 immediate watcher 会读到空值,把首次加载也挡掉。
|
||
routePathLocked: this.$route.path,
|
||
form: {},
|
||
suppressTransportTypeClear: false,
|
||
query: {},
|
||
loading: true,
|
||
data: [],
|
||
dialogReadonly: false,
|
||
detailBox: false,
|
||
detailLoading: false,
|
||
detailRow: {},
|
||
transportPlanImportBox: false,
|
||
transportPlanImportLoading: false,
|
||
transportPlanImportFiles: [],
|
||
transportPlanImportSourceFile: null,
|
||
transportPlanImportRows: [],
|
||
transportPlanImportSelection: [],
|
||
transportPlanImportTab: 'all',
|
||
transportPlanImportEditingKey: '',
|
||
transportPlanImportEditSnapshot: null,
|
||
transportPlanImportContractOptions: [],
|
||
transportPlanImportContractLoading: false,
|
||
transportPlanImportForm: {
|
||
projectId: '',
|
||
projectName: '',
|
||
customerName: '',
|
||
contractId: '',
|
||
contractName: '',
|
||
file: null,
|
||
},
|
||
transportPlanImportRules: {
|
||
projectId: [{ required: true, message: '请选择项目', trigger: 'change' }],
|
||
customerName: [{ required: true, message: '请选择项目后带出客户', trigger: 'change' }],
|
||
contractId: [{ required: true, message: '请选择项目后带出客户合同', trigger: 'change' }],
|
||
file: [{ required: true, message: '请上传计划明细表', trigger: 'change' }],
|
||
},
|
||
crudDialogType: '',
|
||
defaultDialogButtons: null,
|
||
transportPlanCreateActionLoading: '',
|
||
option: (() => {
|
||
const opt = this.cloneOption(this.crudOption, estimateMenuButtonCount(this.config));
|
||
if (this.menuWidth != null) opt.menuWidth = this.menuWidth;
|
||
if (
|
||
this.standaloneFormPage &&
|
||
standaloneTransportPlanFormPaths.includes(this.$route.path)
|
||
) {
|
||
opt.dialogAppendToBody = false;
|
||
opt.dialogModal = false;
|
||
opt.dialogCustomClass = 'transport-plan-form-page-dialog';
|
||
opt.dialogTop = '0';
|
||
opt.dialogWidth = '100%';
|
||
} else if (!opt.dialogCustomClass) {
|
||
// 普通弹窗模式:灰底 body + 白卡表单(与独立表单页风格一致)
|
||
opt.dialogCustomClass = 'business-crud-form-dialog';
|
||
}
|
||
return opt;
|
||
})(),
|
||
selectedProjectId: '',
|
||
projectOptions: [],
|
||
projectLoading: false,
|
||
contractOptions: [],
|
||
contractLoading: false,
|
||
addressTypeOptions,
|
||
flowBox: false,
|
||
flowUrl: '',
|
||
processInstanceId: '',
|
||
dispatchBox: false,
|
||
dispatchLoading: false,
|
||
dispatchRow: {},
|
||
dispatchContractCurrency: 'RMB',
|
||
dispatchRows: [],
|
||
dispatchSaving: '',
|
||
dispatchCarrierOptions: [],
|
||
dispatchCarrierContractOptions: [],
|
||
dispatchCarrierLoading: false,
|
||
dispatchCarrierRequestId: 0,
|
||
dispatchItemBox: false,
|
||
dispatchItemIndex: -1,
|
||
dispatchItemForm: defaultDispatchRow(),
|
||
dispatchItemCargoRows: [],
|
||
dispatchItemAttachmentRows: [],
|
||
selectedDispatchItemAttachmentRows: [],
|
||
attachmentRows: [],
|
||
selectedAttachmentRows: [],
|
||
attachmentImagePreviewVisible: false,
|
||
attachmentImagePreviewUrls: [],
|
||
attachmentImagePreviewIndex: 0,
|
||
attachmentDocumentPreviewVisible: false,
|
||
attachmentPreviewFile: {},
|
||
attachmentViewerPlugins,
|
||
attachmentViewerToolbar: {
|
||
download: true,
|
||
fullscreen: true,
|
||
print: true,
|
||
rotate: true,
|
||
zoom: true,
|
||
},
|
||
transportCargoRows: [],
|
||
currencyOptions: [],
|
||
currencyLoading: false,
|
||
transportRouteBox: false,
|
||
transportRouteLoading: false,
|
||
transportRouteQuery: {},
|
||
transportRouteRows: [],
|
||
transportRoutePage: {
|
||
pageSize: 10,
|
||
currentPage: 1,
|
||
total: 0,
|
||
},
|
||
transportAddressBox: false,
|
||
transportAddressTarget: '',
|
||
transportAddressLoading: false,
|
||
transportAddressQuery: {},
|
||
transportTypeOptions: [],
|
||
transportTypeOptionsLoading: false,
|
||
transportAddressRows: [],
|
||
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: [],
|
||
commonCargoPage: {
|
||
pageSize: 10,
|
||
currentPage: 1,
|
||
total: 0,
|
||
},
|
||
taskCarrierTypes,
|
||
taskDriverOptions: [],
|
||
taskDriverLoading: false,
|
||
taskEscortOptions: [],
|
||
taskEscortLoading: false,
|
||
taskCargoOptionsMap: {},
|
||
taskCargoLoadingMap: {},
|
||
taskQuantityUnitOptions: [],
|
||
taskQuantityUnitLoading: false,
|
||
taskFeeUnitOptions: [],
|
||
taskFeeUnitLoading: false,
|
||
dispatchTransportTypeOptions: [],
|
||
dispatchTransportTypeLoading: false,
|
||
transportPlanDetailTransportTypeOptions: [],
|
||
transportPlanDetailTransportTypeLoading: false,
|
||
transportPlanWaybillPage: {
|
||
pageSize: 10,
|
||
pageSizes: [10, 20, 50, 100],
|
||
currentPage: 1,
|
||
},
|
||
cargoImportBox: false,
|
||
cargoImportLoading: false,
|
||
cargoImportRows: [],
|
||
billingCargoTypeOptions: [],
|
||
billingCargoTypeFlatOptions: [],
|
||
billingCargoTypeLoading: false,
|
||
billingCargoTypeRequest: null,
|
||
packageOptions,
|
||
quantityUnitOptions: ['吨', '千克', '立方米', '件', '车', '箱', '托盘'],
|
||
attachmentFileTypes: [
|
||
'pdf',
|
||
'bmp',
|
||
'jpeg',
|
||
'png',
|
||
'jpg',
|
||
'doc',
|
||
'docx',
|
||
'ppt',
|
||
'pptx',
|
||
'xlsx',
|
||
'xls',
|
||
'eml',
|
||
'msg',
|
||
'zip',
|
||
],
|
||
page: {
|
||
pageSize: 10,
|
||
pageSizes: [10, 20, 50, 100],
|
||
currentPage: 1,
|
||
total: 0,
|
||
},
|
||
selectionList: [],
|
||
standaloneFormKey: '',
|
||
};
|
||
},
|
||
created() {
|
||
this.loadProjectOptions();
|
||
if (this.shippingInfoFormEnabled) this.loadTransportTypeOptions();
|
||
if (this.isStandaloneFormPage) {
|
||
this.$nextTick(() => {
|
||
this.initStandaloneFormPage();
|
||
// 先完成独立表单初始化,再回填模板数据,避免被新增表单默认值覆盖。
|
||
this.$nextTick(() => this.consumeTemplateCreatePayload());
|
||
});
|
||
} else {
|
||
this.consumeTemplateCreatePayload();
|
||
}
|
||
},
|
||
computed: {
|
||
...mapGetters(['permission', 'userInfo']),
|
||
permissionList() {
|
||
return {
|
||
addBtn: this.canCreate,
|
||
};
|
||
},
|
||
detailSections() {
|
||
return this.config.detailSections || [];
|
||
},
|
||
isStandaloneBusinessPage() {
|
||
return this.standaloneFormPage && standaloneTransportPlanFormPaths.includes(this.$route.path);
|
||
},
|
||
isStandaloneFormPage() {
|
||
return this.isStandaloneBusinessPage && ['add', 'edit'].includes(this.$route.query.mode);
|
||
},
|
||
formPageTitle() {
|
||
if (this.isStandaloneFormPage) {
|
||
return (
|
||
this.$route.query.name ||
|
||
`${this.$route.query.mode === 'edit' ? '编辑' : '新增'}${this.config.title || ''}`
|
||
);
|
||
}
|
||
return '';
|
||
},
|
||
crudContainer() {
|
||
return this.isStandaloneFormPage ? 'PageAvueForm' : 'avue-crud';
|
||
},
|
||
pageFormOption() {
|
||
if (!this.isStandaloneFormPage) return this.option;
|
||
const isTransportPlanCreate =
|
||
this.config.enableTransportPlanCreateActions === true && this.$route.query.mode === 'add';
|
||
const base = {
|
||
...this.option,
|
||
boxType: this.$route.query.mode === 'edit' ? 'edit' : 'add',
|
||
menuBtn: true,
|
||
submitBtn: !isTransportPlanCreate,
|
||
emptyBtn: false,
|
||
};
|
||
return this.buildFormGroupOption(base);
|
||
},
|
||
isTransportPlanDetailLayout() {
|
||
return this.detailBox;
|
||
},
|
||
transportPlanGoodsText() {
|
||
const text = this.formatTransportPlanGoodsInfo(this.detailRow);
|
||
return text && text !== '-' ? text : '暂无货物信息';
|
||
},
|
||
transportPlanDateRange() {
|
||
const start = this.detailRow.planStartDate || this.detailRow.planStartTime || '';
|
||
const end = this.detailRow.planEndDate || this.detailRow.planEndTime || '';
|
||
if (!start && !end) return '-';
|
||
return `${String(start).slice(0, 10)}-${String(end || start).slice(0, 10)}`;
|
||
},
|
||
transportPlanWaybillRows() {
|
||
const sources = [
|
||
this.detailRow.waybillList,
|
||
this.detailRow.waybillRows,
|
||
this.detailRow.waybills,
|
||
this.detailRow.transportPlanWaybills,
|
||
this.detailRow.transportPlanWaybillList,
|
||
this.detailRow.dispatchRecords,
|
||
this.detailRow.dispatchRecordList,
|
||
this.detailRow.dispatchList,
|
||
this.detailRow.dispatchRows,
|
||
];
|
||
const rows = sources.reduce((result, source) => {
|
||
const parsed = this.parseTransportPlanWaybillSource(source);
|
||
return result.length ? result : parsed;
|
||
}, []);
|
||
const normalizedRows = rows.map((row, index) =>
|
||
this.normalizeTransportPlanWaybillRow(row, index)
|
||
);
|
||
return normalizedRows;
|
||
},
|
||
transportPlanWaybillPageRows() {
|
||
const start =
|
||
(this.transportPlanWaybillPage.currentPage - 1) * this.transportPlanWaybillPage.pageSize;
|
||
return this.transportPlanWaybillRows.slice(
|
||
start,
|
||
start + this.transportPlanWaybillPage.pageSize
|
||
);
|
||
},
|
||
transportPlanDispatchSummary() {
|
||
const goodsRows = this.parseJsonArray(this.detailRow.goodsJson);
|
||
const total =
|
||
goodsRows.reduce(
|
||
(sum, row) => sum + Number(row.quantity || row.cargoQuantity || row.goodsQuantity || 0),
|
||
0
|
||
) || Number(this.detailRow.totalQuantity || this.detailRow.quantity || 0);
|
||
const assigned =
|
||
this.transportPlanWaybillRows.reduce(
|
||
(sum, row) => sum + Number(row.quantity || row.cargoQuantity || row.goodsQuantity || 0),
|
||
0
|
||
) || Number(this.detailRow.dispatchedQuantity || this.detailRow.dispatchQuantity || 0);
|
||
return { total, assigned, remaining: Math.max(total - assigned, 0) };
|
||
},
|
||
isTemplateCreate() {
|
||
return this.config.permission === 'transport_plan' && Boolean(this.$route.query.fromTemplate);
|
||
},
|
||
completeActionLabel() {
|
||
return '完成调度';
|
||
},
|
||
transportPlanImportDuplicateCount() {
|
||
return this.transportPlanImportRows.filter(row => row._duplicate).length;
|
||
},
|
||
transportPlanImportVisibleRows() {
|
||
return this.transportPlanImportTab === 'duplicate'
|
||
? this.transportPlanImportRows.filter(row => row._duplicate)
|
||
: this.transportPlanImportRows;
|
||
},
|
||
actionButtonLikeSearch() {
|
||
return this.config.actionButtonLikeSearch === true;
|
||
},
|
||
contractSelectEnabled() {
|
||
return this.config.enableContractSelect || this.config.enableTransportPlanForm;
|
||
},
|
||
shippingInfoFormEnabled() {
|
||
return this.config.enableShippingInfoForm || this.config.enableTransportPlanForm;
|
||
},
|
||
dispatchTaskEntryModeOptions() {
|
||
const locked = this.hasMultipleConfiguredGoods(
|
||
this.dispatchItemForm,
|
||
this.dispatchItemCargoRows
|
||
);
|
||
return [
|
||
{ label: '精简录入', value: 'simple', disabled: locked },
|
||
{ label: '完整录入', value: 'full' },
|
||
];
|
||
},
|
||
dispatchPlanGoodsRows() {
|
||
return this.dispatchGoodsRows(this.dispatchRow.goodsJson);
|
||
},
|
||
dispatchPlanCargoInfo() {
|
||
const cargoInfo = this.dispatchPlanGoodsRows
|
||
.map(row => this.formatDispatchCargoInfo({}, row))
|
||
.filter(Boolean)
|
||
.join('; ');
|
||
return cargoInfo || this.dispatchRow.goodsInfo || '';
|
||
},
|
||
dispatchSummaryItems() {
|
||
const summaryMap = new Map();
|
||
const addQuantity = (unit, field, quantity) => {
|
||
const normalizedUnit = unit || TRANSPORT_PLAN_QUANTITY_UNIT;
|
||
if (!summaryMap.has(normalizedUnit)) {
|
||
summaryMap.set(normalizedUnit, { unit: normalizedUnit, total: 0, assigned: 0 });
|
||
}
|
||
summaryMap.get(normalizedUnit)[field] += this.parseDispatchQuantity(quantity);
|
||
};
|
||
const goodsRows = this.dispatchPlanGoodsRows.length
|
||
? this.dispatchPlanGoodsRows
|
||
: [this.dispatchRow];
|
||
|
||
goodsRows.forEach(row => {
|
||
addQuantity(
|
||
this.getDispatchQuantityUnit(row),
|
||
'total',
|
||
row.quantity ||
|
||
row.cargoQuantity ||
|
||
row.goodsQuantity ||
|
||
row.totalQuantity ||
|
||
row.totalCargoQuantity
|
||
);
|
||
});
|
||
if (!this.dispatchRows.length) {
|
||
this.parseJsonArray(this.dispatchRow.dispatchRows).forEach(row => {
|
||
if (!this.hasDispatchVehicleIdentifier(row)) return;
|
||
addQuantity(this.getDispatchQuantityUnit(row), 'assigned', row.quantity);
|
||
});
|
||
}
|
||
this.dispatchRows.forEach(row => {
|
||
if (!this.hasDispatchVehicleIdentifier(row)) return;
|
||
addQuantity(this.getDispatchQuantityUnit(row), 'assigned', row.quantity);
|
||
});
|
||
|
||
return Array.from(summaryMap.values()).map(item => ({
|
||
...item,
|
||
remaining: Math.max(item.total - item.assigned, 0),
|
||
}));
|
||
},
|
||
dispatchHasRemainingQuantity() {
|
||
return this.dispatchSummaryItems.some(item => item.remaining > 0);
|
||
},
|
||
dispatchTableQuantityItems() {
|
||
const quantityMap = new Map();
|
||
this.dispatchRows.forEach(row => {
|
||
this.getDispatchGoodsSourceRows(row).forEach(goods => {
|
||
const unit = this.getDispatchQuantityUnit(goods);
|
||
quantityMap.set(
|
||
unit,
|
||
(quantityMap.get(unit) || 0) +
|
||
this.parseDispatchQuantity(
|
||
goods.quantity || goods.cargoQuantity || goods.goodsQuantity
|
||
)
|
||
);
|
||
});
|
||
});
|
||
return this.dispatchSummaryItems.map(item => ({
|
||
...item,
|
||
listed: quantityMap.get(item.unit) || 0,
|
||
}));
|
||
},
|
||
dispatchHasAddableQuantity() {
|
||
const items = this.dispatchTableQuantityItems;
|
||
if (!items.length) return true;
|
||
// 计划总量为 0 时不限制新增调度
|
||
if (items.every(item => item.total <= 0)) return true;
|
||
return items.some(item => item.total > 0 && item.listed < item.total - 1e-8);
|
||
},
|
||
dispatchSummaryText() {
|
||
const formatSummary = field =>
|
||
this.dispatchSummaryItems
|
||
.map(item => `${this.formatDispatchQuantity(item[field])}${item.unit}`)
|
||
.join('、');
|
||
const unlimited = this.dispatchSummaryItems.every(item => item.total <= 0);
|
||
if (unlimited) {
|
||
return `已调度 ${formatSummary('assigned') || `0${TRANSPORT_PLAN_QUANTITY_UNIT}`}`;
|
||
}
|
||
return `共${formatSummary('total')} | 已调度 ${formatSummary(
|
||
'assigned'
|
||
)},剩余${formatSummary('remaining')}`;
|
||
},
|
||
dispatchAttachmentRows() {
|
||
return this.parseJsonArray(this.dispatchRow.attachmentsJson);
|
||
},
|
||
dispatchDialogTitle() {
|
||
return '计划调度';
|
||
},
|
||
dispatchItemDialogTitle() {
|
||
const mode = this.dispatchItemForm.taskEntryMode === 'full' ? '完整录入' : '精简录入';
|
||
return this.dispatchItemIndex < 0 ? `新增/编辑(${mode})` : `编辑调度信息(${mode})`;
|
||
},
|
||
dispatchTransportMode() {
|
||
return this.resolveTransportMode(this.dispatchItemForm.transportType);
|
||
},
|
||
fixedQuantityUnit() {
|
||
return TRANSPORT_PLAN_QUANTITY_UNIT;
|
||
},
|
||
dispatchIsRoadTransport() {
|
||
return this.dispatchTransportMode === 'road';
|
||
},
|
||
dispatchIsNonRoadTransport() {
|
||
return ['water', 'rail', 'air'].includes(this.dispatchTransportMode);
|
||
},
|
||
dispatchIsCarrierMode() {
|
||
return this.dispatchItemForm.carrierType === '承运商';
|
||
},
|
||
dispatchCarrierRequired() {
|
||
return this.isDispatchCarrierRequired(this.dispatchItemForm.carrierType);
|
||
},
|
||
dispatchCarrierLabel() {
|
||
return '承运商';
|
||
},
|
||
dispatchCarrierSelection: {
|
||
get() {
|
||
return this.dispatchIsCarrierMode
|
||
? this.dispatchItemForm.carrierContractId || ''
|
||
: this.dispatchItemForm.carrierName || '';
|
||
},
|
||
set(value) {
|
||
this.handleDispatchCarrierChange(value);
|
||
},
|
||
},
|
||
dispatchStationMode() {
|
||
return ['water', 'rail', 'air'].includes(this.dispatchTransportMode);
|
||
},
|
||
dispatchStationNamePlaceholder() {
|
||
if (!this.dispatchStationMode) return '行政区划自动带出';
|
||
const labelMap = {
|
||
water: '请选择港口/码头',
|
||
rail: '请选择车站',
|
||
air: '请选择空港',
|
||
};
|
||
return labelMap[this.dispatchTransportMode] || '请选择站点';
|
||
},
|
||
dispatchAddressDetailPlaceholder() {
|
||
return this.dispatchStationMode ? '选择站点后带出详细地址' : '请输入';
|
||
},
|
||
dispatchDialogNo() {
|
||
return this.dispatchRow.planNo || this.dispatchRow.loadingNo || '';
|
||
},
|
||
dispatchDialogStatusText() {
|
||
return this.displayStatus(this.dispatchRow, this.config.statusProp || 'businessStatus');
|
||
},
|
||
dispatchItemFreightTotal() {
|
||
if (this.dispatchItemForm.taskEntryMode !== 'full') {
|
||
const otherFeeTotal = Number(this.dispatchItemForm.otherFeeTotal || 0);
|
||
const freight = this.dispatchItemFreightSubtotal;
|
||
const hasFreight = freight !== '';
|
||
const hasOtherFee = this.dispatchItemForm.otherFeeTotal !== '';
|
||
if (!hasFreight && !hasOtherFee) return '';
|
||
const total = (hasFreight ? Number(freight) : 0) + (hasOtherFee ? otherFeeTotal : 0);
|
||
return Number.isInteger(total) ? String(total) : String(Number(total.toFixed(2)));
|
||
}
|
||
const otherFeeTotal = Number(this.dispatchItemForm.otherFeeTotal || 0);
|
||
const freightSubtotal = this.dispatchItemFreightSubtotal;
|
||
const hasOtherFee = this.dispatchItemForm.otherFeeTotal !== '';
|
||
if (freightSubtotal === '' && !hasOtherFee) return '';
|
||
const total = Number(freightSubtotal || 0) + (hasOtherFee ? otherFeeTotal : 0);
|
||
return Number.isInteger(total) ? String(total) : String(Number(total.toFixed(2)));
|
||
},
|
||
dispatchItemFreightSubtotal() {
|
||
if (this.dispatchItemForm.taskEntryMode !== 'full') {
|
||
if (
|
||
this.dispatchItemForm.freightAmount !== undefined &&
|
||
this.dispatchItemForm.freightAmount !== null &&
|
||
String(this.dispatchItemForm.freightAmount).trim() !== ''
|
||
) {
|
||
return this.formatDispatchAmount(this.dispatchItemForm.freightAmount);
|
||
}
|
||
const quantity = Number(this.dispatchItemForm.quantity || 0);
|
||
const unitPrice = Number(this.dispatchItemForm.unitPrice || 0);
|
||
if (!quantity || !unitPrice) return '';
|
||
const total = quantity * unitPrice;
|
||
return Number.isInteger(total) ? String(total) : String(Number(total.toFixed(2)));
|
||
}
|
||
const total = this.dispatchItemFreightGroups.reduce(
|
||
(sum, group) => sum + Number(this.dispatchFreightGroupAmount(group) || 0),
|
||
0
|
||
);
|
||
const hasEnteredAmount = this.dispatchItemFreightGroups.some(group =>
|
||
(group.rows || []).some(
|
||
cargo =>
|
||
cargo.freightAmount !== undefined &&
|
||
cargo.freightAmount !== null &&
|
||
String(cargo.freightAmount).trim() !== ''
|
||
)
|
||
);
|
||
if (!total && !hasEnteredAmount) return '';
|
||
return Number.isInteger(total) ? String(total) : String(Number(total.toFixed(2)));
|
||
},
|
||
dispatchItemFreightGroups() {
|
||
return [
|
||
{
|
||
key: '__all__',
|
||
quantityUnit: TRANSPORT_PLAN_QUANTITY_UNIT,
|
||
rows: this.dispatchItemCargoRows || [],
|
||
},
|
||
];
|
||
},
|
||
dispatchItemFreightCurrencyLabel() {
|
||
const value = this.dispatchItemForm.freightCurrency || 'RMB';
|
||
return this.currencyRemark(value);
|
||
},
|
||
isAdmin() {
|
||
const authority = this.userInfo && this.userInfo.authority;
|
||
return Array.isArray(authority)
|
||
? authority.includes('admin')
|
||
: String(authority || '').includes('admin');
|
||
},
|
||
canCreate() {
|
||
return this.hasPermission(`${this.config.permission}_add`);
|
||
},
|
||
showTransportPlanCreateActions() {
|
||
return (
|
||
this.config.enableTransportPlanCreateActions &&
|
||
this.crudDialogType === 'add' &&
|
||
!this.dialogReadonly
|
||
);
|
||
},
|
||
// 当前路由是否仍属于本实例(tab.js 按 fullPath 建实例,一个实例只对应一个 path)。
|
||
// 实例被 keep-alive 缓存后 $route 已跑到别的页面时,不能用「当前页面的 id」去查本页数据。
|
||
isOwnedRouteActive() {
|
||
return this.$route.path === this.routePathLocked;
|
||
},
|
||
billingCargoTypeCascaderProps() {
|
||
return {
|
||
label: 'cargoName',
|
||
value: 'id',
|
||
children: 'children',
|
||
disabled: (data, node) => node.level === 1,
|
||
leaf: 'leaf',
|
||
checkStrictly: true,
|
||
emitPath: true,
|
||
};
|
||
},
|
||
transportCargoTypeOptions() {
|
||
return this.billingCargoTypeOptions.filter(item => item.children?.length);
|
||
},
|
||
transportCargoTypeCascaderProps() {
|
||
return {
|
||
label: 'cargoName',
|
||
value: 'id',
|
||
children: 'children',
|
||
emitPath: true,
|
||
};
|
||
},
|
||
ids() {
|
||
return this.selectionList.map(item => item.id).join(',');
|
||
},
|
||
transportCargoQuantityTotal() {
|
||
if (!this.transportCargoRows.length) return '系统自动计算';
|
||
const total = this.transportCargoRows.reduce((sum, row) => {
|
||
const value = Number(row.quantity);
|
||
return Number.isFinite(value) ? sum + value : sum;
|
||
}, 0);
|
||
return Number.isInteger(total) ? String(total) : total.toFixed(2);
|
||
},
|
||
transportAddressSelectDisabled() {
|
||
return this.dialogReadonly || this.isBillingFieldEmpty(this.form.transportType);
|
||
},
|
||
transportMode() {
|
||
return this.resolveTransportMode(this.form.transportType);
|
||
},
|
||
transportStationMode() {
|
||
return ['water', 'rail', 'air'].includes(this.transportMode);
|
||
},
|
||
transportStationTypeLabel() {
|
||
const mode = this.dispatchItemBox ? this.dispatchTransportMode : this.transportMode;
|
||
const labelMap = {
|
||
water: '港口/码头',
|
||
rail: '铁路车站',
|
||
air: '空港机场',
|
||
};
|
||
return labelMap[mode] || '';
|
||
},
|
||
transportStationNamePlaceholder() {
|
||
if (!this.transportStationMode) return '行政区划自动带出';
|
||
const labelMap = {
|
||
water: '请选择港口/码头',
|
||
rail: '请选择车站',
|
||
air: '请选择空港',
|
||
};
|
||
return labelMap[this.transportMode] || '请选择站点';
|
||
},
|
||
transportAddressDetailPlaceholder() {
|
||
return this.transportStationMode ? '选择站点后带出详细地址' : '请输入';
|
||
},
|
||
},
|
||
watch: {
|
||
$route(to, from) {
|
||
if (!this.isStandaloneFormPage) 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.detailId,必须确认当前路由还是自己的,
|
||
// 否则跳到别的页面后会把「别人的 id」灌进来触发详情请求。
|
||
if (!this.isOwnedRouteActive) return;
|
||
if (id !== undefined && id !== null && id !== '') {
|
||
this.$nextTick(() => this.openDetail({ id }));
|
||
}
|
||
},
|
||
},
|
||
'form.transportType'(value, oldValue) {
|
||
if (!this.shippingInfoFormEnabled || this.suppressTransportTypeClear) return;
|
||
if (String(value ?? '') === String(oldValue ?? '')) return;
|
||
this.handleTransportTypeChange(value);
|
||
},
|
||
},
|
||
methods: {
|
||
initStandaloneFormPage() {
|
||
if (!this.isStandaloneFormPage) return;
|
||
const mode = this.$route.query.mode === 'edit' ? 'edit' : 'add';
|
||
const id = this.$route.query.id || '';
|
||
const key = `${mode}:${id}`;
|
||
if (this.standaloneFormKey === key) return;
|
||
this.standaloneFormKey = key;
|
||
if (mode === 'edit') this.form = { id };
|
||
this.beforeOpen(() => {}, mode);
|
||
},
|
||
buildFormGroupOption(option) {
|
||
const sectionTitleProps = [
|
||
'basicInfoTitle',
|
||
'shippingInfoTitle',
|
||
'goodsInfoTitle',
|
||
'attachmentTitle',
|
||
];
|
||
const groups = [];
|
||
let current = [];
|
||
[...(option.column || [])]
|
||
.sort((a, b) => (b.order || 0) - (a.order || 0))
|
||
.forEach(column => {
|
||
if (sectionTitleProps.includes(column.prop) && current.length) {
|
||
groups.push(current);
|
||
current = [];
|
||
}
|
||
current.push(column);
|
||
});
|
||
if (current.length) groups.push(current);
|
||
if (groups.length <= 1) return option;
|
||
|
||
return {
|
||
...option,
|
||
// Avue 会始终渲染一个主分组。将四个业务分区全部放进 group,
|
||
// 让它们都通过同一个 PlainAvueGroup 组件渲染,主分组由页面样式隐藏。
|
||
column: [],
|
||
group: groups.map((columns, index) => {
|
||
const title = columns.find(column => sectionTitleProps.includes(column.prop));
|
||
return {
|
||
prop: title?.prop || `transportPlanGroup${index + 1}`,
|
||
label: '',
|
||
arrow: false,
|
||
column: title ? columns.filter(column => column.prop !== title.prop) : columns,
|
||
};
|
||
}),
|
||
};
|
||
},
|
||
openBusinessForm(mode, row = {}) {
|
||
if (this.isStandaloneBusinessPage) {
|
||
this.$router.push({
|
||
path: standaloneTransportPlanFormRoutes[this.$route.path] || this.$route.path,
|
||
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);
|
||
},
|
||
formatTransportPlanGoodsInfo(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.parseDispatchQuantity(
|
||
item.quantity ?? item.cargoQuantity ?? item.goodsQuantity
|
||
);
|
||
return result;
|
||
}, {});
|
||
return Object.values(groups)
|
||
.map(group => {
|
||
const quantity = this.formatDispatchQuantity(group.quantity);
|
||
return `${group.typeName}等${group.count}种货物 | ${quantity}${
|
||
group.unit ? ` ${group.unit}` : ''
|
||
}`;
|
||
})
|
||
.join('; ');
|
||
},
|
||
formatTransportPlanProvinceCityDistrict(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;
|
||
},
|
||
formatTransportPlanListAddress(row = {}, field) {
|
||
const address = String(row[`${field}Address`] || '').trim();
|
||
const name = String(row[`${field}Name`] || '').trim();
|
||
const fromAddress = this.extractTransportPlanProvinceCityDistrict(address);
|
||
if (fromAddress) return fromAddress;
|
||
const fromName = this.extractTransportPlanProvinceCityDistrict(name);
|
||
if (fromName) return fromName;
|
||
return name || address || '-';
|
||
},
|
||
extractTransportPlanProvinceCityDistrict(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('/') : '';
|
||
},
|
||
formatTransportPlanRoadCityDistrict(value) {
|
||
const text = String(value || '').trim();
|
||
if (!text) return '-';
|
||
const cityMatch = text.match(/市/);
|
||
if (!cityMatch) return this.formatTransportPlanProvinceCityDistrict(text);
|
||
|
||
const cityEnd = cityMatch.index + 1;
|
||
const cityWithProvince = text.slice(0, cityEnd);
|
||
const city =
|
||
cityWithProvince.match(/(?:省|自治区|特别行政区)([^省]+市)$/)?.[1] || cityWithProvince;
|
||
const districtText = text.slice(cityEnd);
|
||
const districtMatch = districtText.match(
|
||
/^(?:.*(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗))/
|
||
);
|
||
if (!districtMatch) return city;
|
||
|
||
const district = districtMatch[0];
|
||
return district ? `${city} ${district}` : city;
|
||
},
|
||
formatTransportPlanDetailRouteName(type) {
|
||
const nameProp = type === 'departure' ? 'departureName' : 'arrivalName';
|
||
const addressProp = type === 'departure' ? 'departureAddress' : 'arrivalAddress';
|
||
const name = this.detailRow[nameProp] || this.detailRow[addressProp];
|
||
const transportMode = this.resolveTransportMode(this.detailRow.transportType);
|
||
return transportMode === 'road'
|
||
? this.formatTransportPlanRoadCityDistrict(name)
|
||
: name || '-';
|
||
},
|
||
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`) &&
|
||
(!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`) &&
|
||
(!row.readonly || this.isReadonlyDeleteAllowed(row)) &&
|
||
this.inStatus(row, this.config.deleteStatus)
|
||
);
|
||
},
|
||
canCopy(row) {
|
||
return (
|
||
this.hasAction('copy') &&
|
||
this.hasPermission(`${this.config.permission}_copy`) &&
|
||
!row.readonly
|
||
);
|
||
},
|
||
canComplete(row) {
|
||
if (
|
||
!this.hasAction('complete') ||
|
||
!this.hasPermission(`${this.config.permission}_complete`) ||
|
||
row.readonly
|
||
) {
|
||
return false;
|
||
}
|
||
const status = this.statusValue(row);
|
||
return this.config.permission === 'transport_plan'
|
||
? status === 'dispatching'
|
||
: status === 'processing';
|
||
},
|
||
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.isStandaloneFormPage) 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.$nextTick(() => {
|
||
this.suppressTransportTypeClear = false;
|
||
});
|
||
this.form.planName = this.form.planName || this.form.templateName || '';
|
||
this.selectedProjectId = this.form.projectId || '';
|
||
this.attachmentRows = this.parseJsonArray(this.form.attachmentsJson);
|
||
this.selectedAttachmentRows = [];
|
||
if (this.contractSelectEnabled) {
|
||
this.syncCurrentContractOption();
|
||
this.loadContractOptionsForProject();
|
||
}
|
||
if (this.config.enableProjectSelect) this.syncCurrentProjectOption();
|
||
if (this.config.enableTransportPlanForm) this.initTransportPlanFormRows();
|
||
},
|
||
canOperation(operation, row) {
|
||
const permission = operation.permission || `${this.config.permission}_${operation.action}`;
|
||
if (!this.hasPermission(permission) || 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';
|
||
},
|
||
showViewButton(row) {
|
||
if (!this.hasPermission(`${this.config.permission}_view`)) return false;
|
||
if (this.statusValue(row) === 'draft') return false;
|
||
return !this.showDetailButton(row);
|
||
},
|
||
formatDetailValue(row, prop) {
|
||
if (!row) return '-';
|
||
if (prop === 'businessStatus') return this.displayStatus(row, prop);
|
||
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;
|
||
},
|
||
normalizeTransportPlanWaybillRow(row = {}, index = 0) {
|
||
const status = row.businessStatus || row.status || row.waybillStatus || '';
|
||
const statusMap = {
|
||
draft: ['草稿', 'is-draft'],
|
||
pending: ['待执行', 'is-pending'],
|
||
waiting: ['待执行', 'is-pending'],
|
||
processing: ['进行中', 'is-processing'],
|
||
running: ['进行中', 'is-processing'],
|
||
completed: ['已完成', 'is-completed'],
|
||
cancelled: ['已取消', 'is-cancelled'],
|
||
};
|
||
const statusItem = statusMap[status] || [
|
||
row.businessStatusName || row.statusName || status || '-',
|
||
'',
|
||
];
|
||
return {
|
||
...row,
|
||
_detailIndex: index,
|
||
vehicleNo:
|
||
row.vehicleNo || row.vehicleNumber || row.flightNo || row.shipNo || row.trainNo || '',
|
||
driverName: row.driverName || row.driver || '',
|
||
carrierName: row.carrierName || row.carrier || '',
|
||
transportType: row.transportTypeName || row.transportType || '',
|
||
carrierType: row.carrierTypeName || row.carrierType || '',
|
||
goodsInfo: row.goodsInfo || row.cargoInfo || this.formatDetailValue(row, 'goodsInfo'),
|
||
departureAddress: row.departureAddress || row.fromAddress || '',
|
||
statusText: statusItem[0],
|
||
statusClass: statusItem[1],
|
||
};
|
||
},
|
||
parseTransportPlanWaybillSource(value) {
|
||
if (Array.isArray(value)) return value;
|
||
const parsed = this.parseJsonArray(value);
|
||
if (parsed.length) return parsed;
|
||
if (!value || typeof value !== 'object') return [];
|
||
if (Array.isArray(value.records)) return value.records;
|
||
if (Array.isArray(value.rows)) return value.rows;
|
||
if (Array.isArray(value.data)) return value.data;
|
||
if (value.data && typeof value.data === 'object') {
|
||
return this.parseTransportPlanWaybillSource(value.data);
|
||
}
|
||
return [];
|
||
},
|
||
openTransportPlanWaybillDetail(row) {
|
||
if (!row?.id) {
|
||
this.$message.info('当前运单暂无详情数据');
|
||
return;
|
||
}
|
||
this.$router.push({ path: '/business/waybill-manage/detail', query: { id: row.id } });
|
||
},
|
||
transportPlanWaybillIndex(index) {
|
||
return (
|
||
(this.transportPlanWaybillPage.currentPage - 1) * this.transportPlanWaybillPage.pageSize +
|
||
index +
|
||
1
|
||
);
|
||
},
|
||
handleTransportPlanWaybillCurrentChange(currentPage) {
|
||
this.transportPlanWaybillPage.currentPage = currentPage;
|
||
},
|
||
handleTransportPlanWaybillSizeChange(pageSize) {
|
||
this.transportPlanWaybillPage.pageSize = pageSize;
|
||
this.transportPlanWaybillPage.currentPage = 1;
|
||
},
|
||
loadTransportPlanDetailTransportTypeOptions() {
|
||
if (
|
||
this.transportPlanDetailTransportTypeOptions.length ||
|
||
this.transportPlanDetailTransportTypeLoading
|
||
) {
|
||
return Promise.resolve(this.transportPlanDetailTransportTypeOptions);
|
||
}
|
||
this.transportPlanDetailTransportTypeLoading = true;
|
||
return getDictionary({ code: 'transport_type' })
|
||
.then(res => {
|
||
this.transportPlanDetailTransportTypeOptions = this.normalizeDictOptions(
|
||
res.data?.data || []
|
||
);
|
||
return this.transportPlanDetailTransportTypeOptions;
|
||
})
|
||
.finally(() => {
|
||
this.transportPlanDetailTransportTypeLoading = false;
|
||
});
|
||
},
|
||
ensureTransportPlanDetailTransportTypeName(detail = {}) {
|
||
const transportTypes = [
|
||
detail.transportType,
|
||
...this.transportPlanWaybillRows.map(row => row.transportType),
|
||
]
|
||
.map(value => String(value || '').trim())
|
||
.filter(Boolean);
|
||
if (transportTypes.some(value => !/[\u4e00-\u9fff]/.test(value))) {
|
||
this.loadTransportPlanDetailTransportTypeOptions();
|
||
}
|
||
},
|
||
formatTransportPlanDetailTransportType(value) {
|
||
const transportType = String(value || '').trim();
|
||
if (!transportType) return '-';
|
||
const item = this.transportPlanDetailTransportTypeOptions.find(
|
||
option => String(option.value || '').toLowerCase() === transportType.toLowerCase()
|
||
);
|
||
return item?.label || transportType;
|
||
},
|
||
openDetail(row) {
|
||
this.detailBox = true;
|
||
this.detailLoading = true;
|
||
this.detailRow = { ...row };
|
||
this.transportPlanWaybillPage.currentPage = 1;
|
||
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;
|
||
if (!detail.contractId) {
|
||
this.detailRow = detail;
|
||
this.ensureTransportPlanDetailTransportTypeName(detail);
|
||
this.applyFormDetail(detail);
|
||
return null;
|
||
}
|
||
return getContractDetail(detail.contractId)
|
||
.then(contractRes => {
|
||
const contract = contractRes?.data?.data || contractRes?.data || {};
|
||
this.detailRow = {
|
||
...detail,
|
||
customerName:
|
||
contract.partyA || contract.customerName || detail.customerName || '',
|
||
};
|
||
this.ensureTransportPlanDetailTransportTypeName(this.detailRow);
|
||
this.applyFormDetail(this.detailRow);
|
||
})
|
||
.catch(() => {
|
||
this.detailRow = detail;
|
||
this.ensureTransportPlanDetailTransportTypeName(detail);
|
||
this.applyFormDetail(detail);
|
||
});
|
||
})
|
||
.finally(() => {
|
||
this.detailLoading = false;
|
||
});
|
||
},
|
||
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;
|
||
},
|
||
syncCustomCreateDialogButtons(type) {
|
||
if (!this.config.enableTransportPlanCreateActions) {
|
||
return;
|
||
}
|
||
if (!this.defaultDialogButtons) {
|
||
this.defaultDialogButtons = {
|
||
saveBtn: this.option.saveBtn,
|
||
cancelBtn: this.option.cancelBtn,
|
||
};
|
||
}
|
||
const customCreate = type === 'add';
|
||
this.option.saveBtn = customCreate ? false : this.defaultDialogButtons.saveBtn;
|
||
this.option.cancelBtn = customCreate ? false : this.defaultDialogButtons.cancelBtn;
|
||
},
|
||
statusTagType(status) {
|
||
if (
|
||
[
|
||
1,
|
||
'pending',
|
||
'waiting_dispatch',
|
||
'processing',
|
||
'dispatching',
|
||
'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) {
|
||
const submitRow = { ...row };
|
||
if (
|
||
!this.validateMobileFields([
|
||
[submitRow.departurePhone, '发货联系方式'],
|
||
[submitRow.arrivalPhone, '收货联系方式'],
|
||
])
|
||
) {
|
||
return false;
|
||
}
|
||
if (!this.validateTransportPlanForm(submitRow)) return false;
|
||
submitRow.goodsJson = JSON.stringify(this.transportCargoRows);
|
||
submitRow.attachmentsJson = JSON.stringify(this.attachmentRows);
|
||
delete submitRow.basicInfoTitle;
|
||
delete submitRow.shippingInfoTitle;
|
||
delete submitRow.goodsInfoTitle;
|
||
delete submitRow.attachmentTitle;
|
||
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;
|
||
},
|
||
validateTransportPlanForm(row) {
|
||
const requiredFields = this.config.transportFormRequiredFields || [
|
||
['projectName', '项目'],
|
||
['contractName', '客户合同'],
|
||
['planName', '计划名称'],
|
||
['transportType', '运输方式'],
|
||
['planStartDate', '计划开始日期'],
|
||
['planEndDate', '计划结束日期'],
|
||
['departureAddress', '发货地址'],
|
||
['arrivalAddress', '收货地址'],
|
||
];
|
||
const emptyField = requiredFields.find(([prop]) => this.isBillingFieldEmpty(row[prop]));
|
||
if (emptyField) {
|
||
this.$message.warning(`请选择或输入${emptyField[1]}`);
|
||
return false;
|
||
}
|
||
if (row.planStartDate && row.planEndDate && row.planEndDate < row.planStartDate) {
|
||
this.$message.warning('计划结束日期不能早于计划开始日期');
|
||
return false;
|
||
}
|
||
if (row.remark && row.remark.length > 200) {
|
||
this.$message.warning('备注不能超过200个字符');
|
||
return false;
|
||
}
|
||
if (row.basicRemark && row.basicRemark.length > 200) {
|
||
this.$message.warning('基本信息备注不能超过200个字符');
|
||
return false;
|
||
}
|
||
for (const [index, cargo] of this.transportCargoRows.entries()) {
|
||
if (!cargo.cargoType) {
|
||
this.$message.warning(`第${index + 1}行货物类型不能为空`);
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
},
|
||
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.currencyOptions.find(
|
||
item => String(item.value).toUpperCase() === String(value || '').toUpperCase()
|
||
);
|
||
return option?.remark || this.currencyName(option?.label || value);
|
||
},
|
||
normalizeCurrencyValue(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.currencyOptions.find(
|
||
item => String(item.value || '').toUpperCase() === code
|
||
);
|
||
return option?.value || code || 'RMB';
|
||
},
|
||
getContractCurrency(contract = {}) {
|
||
return this.normalizeCurrencyValue(
|
||
contract.settlementCurrency || contract.settlementCurrencyCode || contract.currency || 'RMB'
|
||
);
|
||
},
|
||
loadCurrencyOptions() {
|
||
if (this.currencyOptions.length || this.currencyLoading) {
|
||
return Promise.resolve(this.currencyOptions);
|
||
}
|
||
this.currencyLoading = true;
|
||
return getSystemDictionary({ code: 'currency_type' })
|
||
.then(res => {
|
||
const currencyNameMap = {
|
||
CNY: '人民币',
|
||
RMB: '人民币',
|
||
USD: '美元',
|
||
EUR: '欧元',
|
||
JPY: '日元',
|
||
GBP: '英镑',
|
||
};
|
||
this.currencyOptions = 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.currencyOptions.find(
|
||
item => String(item.value).toUpperCase() === normalized
|
||
);
|
||
if (exact) return exact.value;
|
||
if (normalized === 'CNY') {
|
||
const rmb = this.currencyOptions.find(
|
||
item => String(item.value).toUpperCase() === 'RMB'
|
||
);
|
||
return rmb?.value || value;
|
||
}
|
||
return value;
|
||
};
|
||
this.dispatchItemForm.freightCurrency = resolveCurrencyValue(
|
||
this.dispatchItemForm.freightCurrency
|
||
);
|
||
return this.currencyOptions;
|
||
})
|
||
.finally(() => {
|
||
this.currencyLoading = false;
|
||
});
|
||
},
|
||
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.isStandaloneBusinessPage) 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;
|
||
}
|
||
this.getSaveRequest()(submitRow).then(
|
||
() => {
|
||
this.onLoad(this.page);
|
||
this.$message({ type: 'success', message: '操作成功!' });
|
||
done();
|
||
if (this.isStandaloneBusinessPage) this.closeCrudDialog();
|
||
},
|
||
error => {
|
||
window.console.log(error);
|
||
this.stopSubmitLoading(loading);
|
||
}
|
||
);
|
||
},
|
||
getSaveRequest() {
|
||
return this.api.submit;
|
||
},
|
||
handleTransportPlanCreateAction(action) {
|
||
if (this.transportPlanCreateActionLoading) {
|
||
return;
|
||
}
|
||
const statusProp = this.config.statusProp || 'businessStatus';
|
||
const submitRow = this.normalizeRow({
|
||
...this.form,
|
||
[statusProp]: action === 'draft' ? 'draft' : 'waiting_dispatch',
|
||
});
|
||
if (!submitRow) return;
|
||
this.transportPlanCreateActionLoading = action;
|
||
this.api
|
||
.submit(submitRow)
|
||
.then(() => {
|
||
this.$message.success(action === 'draft' ? '暂存成功' : '创建成功');
|
||
this.onLoad(this.page, this.query);
|
||
this.closeCrudDialog();
|
||
})
|
||
.finally(() => {
|
||
this.transportPlanCreateActionLoading = '';
|
||
});
|
||
},
|
||
closeCrudDialog() {
|
||
if (this.isStandaloneBusinessPage) {
|
||
this.$router.$avueRouter?.closeTag?.();
|
||
const listPath =
|
||
Object.entries(standaloneTransportPlanFormRoutes).find(
|
||
([, formPath]) => formPath === this.$route.path
|
||
)?.[0] || this.$route.path;
|
||
this.$router.push({ path: listPath, 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.syncCustomCreateDialogButtons(type);
|
||
this.dialogReadonly = type === 'view';
|
||
if (type === 'add') {
|
||
this.form = {
|
||
...(this.config.defaultForm || {}),
|
||
};
|
||
this.selectedProjectId = '';
|
||
this.attachmentRows = [];
|
||
this.selectedAttachmentRows = [];
|
||
if (this.contractSelectEnabled) {
|
||
this.contractOptions = [];
|
||
this.form.contractId = '';
|
||
this.form.contractName = '';
|
||
}
|
||
this.transportCargoRows = [];
|
||
this.initTransportPlanFormRows();
|
||
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] = '';
|
||
});
|
||
this.form.mileage = this.normalizeMileageValue(this.form.mileage);
|
||
this.$nextTick(() => {
|
||
this.suppressTransportTypeClear = false;
|
||
});
|
||
this.selectedProjectId = this.form.projectId || '';
|
||
this.attachmentRows = this.parseJsonArray(this.form.attachmentsJson);
|
||
this.selectedAttachmentRows = [];
|
||
this.initTransportPlanFormRows();
|
||
if (this.config.enableProjectSelect) {
|
||
this.syncCurrentProjectOption();
|
||
}
|
||
if (this.contractSelectEnabled) {
|
||
this.syncCurrentContractOption();
|
||
}
|
||
},
|
||
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 {};
|
||
}
|
||
},
|
||
parseDispatchQuantity(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;
|
||
},
|
||
getDispatchQuantityUnit(row = {}) {
|
||
return row.quantityUnit || row.goodsQuantityUnit || row.unit || TRANSPORT_PLAN_QUANTITY_UNIT;
|
||
},
|
||
getDispatchVehicleIdentifier(row = {}) {
|
||
return (
|
||
[row.vehicleNo, row.vehicleNumber, row.flightNo, row.shipNo, row.trainNo].find(value =>
|
||
String(value ?? '').trim()
|
||
) || ''
|
||
);
|
||
},
|
||
hasDispatchVehicleIdentifier(row = {}) {
|
||
return Boolean(this.getDispatchVehicleIdentifier(row));
|
||
},
|
||
getDispatchSummaryItem(unit) {
|
||
return this.dispatchSummaryItems.find(item => item.unit === unit);
|
||
},
|
||
getDispatchRemainingQuantity(unit) {
|
||
return this.getDispatchSummaryItem(unit)?.remaining || 0;
|
||
},
|
||
/** 计划总量为 0 时不限制调度数量 */
|
||
isDispatchPlanQuantityUnlimited(row = {}) {
|
||
const unit = this.getDispatchQuantityUnit(row);
|
||
const hasCargoIdentity = Boolean(
|
||
String(row.cargoName || row.goodsName || row.name || '').trim() ||
|
||
String(row.cargoType || row.secondCargoTypeName || row.goodsType || row.type || '').trim()
|
||
);
|
||
const planGoodsRows = this.dispatchPlanGoodsRows;
|
||
if (planGoodsRows.length && hasCargoIdentity) {
|
||
const cargoKey = this.getDispatchCargoIdentity(row);
|
||
const matchedPlanRows = planGoodsRows.filter(
|
||
goods => this.getDispatchCargoIdentity(goods) === cargoKey
|
||
);
|
||
if (!matchedPlanRows.length) {
|
||
return (this.getDispatchSummaryItem(unit)?.total || 0) <= 0;
|
||
}
|
||
const total = matchedPlanRows.reduce(
|
||
(sum, goods) =>
|
||
sum +
|
||
this.parseDispatchQuantity(
|
||
goods.quantity || goods.cargoQuantity || goods.goodsQuantity
|
||
),
|
||
0
|
||
);
|
||
return total <= 0;
|
||
}
|
||
if (!planGoodsRows.length) {
|
||
return Number(this.dispatchRow.totalQuantity || this.dispatchRow.quantity || 0) <= 0;
|
||
}
|
||
return (this.getDispatchSummaryItem(unit)?.total || 0) <= 0;
|
||
},
|
||
formatDispatchQuantity(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);
|
||
this.syncCurrentProjectOption();
|
||
})
|
||
.finally(() => {
|
||
this.projectLoading = false;
|
||
});
|
||
},
|
||
syncCurrentProjectOption() {
|
||
if (!this.form.projectId || !this.form.projectName) 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 = [];
|
||
}
|
||
return;
|
||
}
|
||
this.form.projectId = project.id;
|
||
this.form.projectName = project.projectName || '';
|
||
this.form.projectCode = project.projectCode || '';
|
||
this.form.undertakeDeptName = project.undertakeDeptName || '';
|
||
if (this.contractSelectEnabled || this.config.enableTransportPlanForm) {
|
||
this.form.customerName =
|
||
project.customerName ||
|
||
project.customerNames ||
|
||
project.customer ||
|
||
this.form.customerName ||
|
||
'';
|
||
}
|
||
if (this.config.enableTransportPlanForm) {
|
||
this.form.transportType =
|
||
project.transportType ||
|
||
project.transportMode ||
|
||
project.transportWay ||
|
||
this.form.transportType ||
|
||
'';
|
||
}
|
||
if (this.contractSelectEnabled) {
|
||
this.form.contractId = '';
|
||
this.form.contractName = '';
|
||
this.loadContractOptionsForProject(true);
|
||
}
|
||
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();
|
||
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 = '';
|
||
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.$refs.crud?.validateField?.('contractName');
|
||
},
|
||
loadDispatchContractCurrency(plan = {}) {
|
||
const findContract = () =>
|
||
this.contractOptions.find(item => String(item.id) === String(plan.contractId)) ||
|
||
this.contractOptions.find(item => item.contractName === plan.contractName);
|
||
const applyCurrency = contract => {
|
||
this.dispatchContractCurrency = this.getContractCurrency(
|
||
contract &&
|
||
(contract.settlementCurrency || contract.settlementCurrencyCode || contract.currency)
|
||
? contract
|
||
: { settlementCurrency: plan.settlementCurrency }
|
||
);
|
||
return this.dispatchContractCurrency;
|
||
};
|
||
const current = findContract();
|
||
if (current) return Promise.resolve(applyCurrency(current));
|
||
if (!plan.projectId || !plan.contractId) {
|
||
applyCurrency();
|
||
return Promise.resolve(this.dispatchContractCurrency);
|
||
}
|
||
return getContractList(1, 9999, {
|
||
projectId: plan.projectId,
|
||
projectName: plan.projectName,
|
||
...(this.config.contractQueryParams || {}),
|
||
})
|
||
.then(res => {
|
||
this.contractOptions = extractRecords(res);
|
||
const contract = findContract();
|
||
return applyCurrency(contract);
|
||
})
|
||
.catch(() => {
|
||
applyCurrency();
|
||
return this.dispatchContractCurrency;
|
||
});
|
||
},
|
||
handleDispatchTaskEntryModeChange(value) {
|
||
if (
|
||
value === 'simple' &&
|
||
this.hasMultipleConfiguredGoods(this.dispatchItemForm, this.dispatchItemCargoRows)
|
||
) {
|
||
this.dispatchItemForm.taskEntryMode = 'full';
|
||
this.$message.warning('当前配置了多条货物信息,不能切换为精简录入');
|
||
return;
|
||
}
|
||
this.dispatchItemForm.taskEntryMode = value || 'simple';
|
||
if (this.dispatchItemForm.taskEntryMode === 'full' && !this.dispatchItemCargoRows.length) {
|
||
this.dispatchItemCargoRows = [this.normalizeTransportCargoRow()];
|
||
}
|
||
},
|
||
handleFreightNumberInput(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();
|
||
},
|
||
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(target, item = {}) {
|
||
const value = item.driverName || item.name || '';
|
||
const driverPhone = item.mobile || item.phone || item.driverPhone || '';
|
||
const drivingVehicle = String(item.drivingVehicle || '').trim();
|
||
if (target === 'dispatch') {
|
||
this.dispatchItemForm.driverId = item.id || '';
|
||
this.dispatchItemForm.driverName = value;
|
||
if (driverPhone) this.dispatchItemForm.driverPhone = driverPhone;
|
||
if (drivingVehicle) this.dispatchItemForm.vehicleNo = drivingVehicle;
|
||
return;
|
||
}
|
||
return;
|
||
},
|
||
handleTaskEscortSelect(item = {}) {
|
||
const value = item.driverName || item.name || '';
|
||
const escortPhone = item.mobile || item.phone || item.driverPhone || '';
|
||
this.dispatchItemForm.escortName = value;
|
||
if (escortPhone) this.dispatchItemForm.escortPhone = escortPhone;
|
||
},
|
||
getTaskCargoTypeOptionsKey(path = []) {
|
||
const normalizedPath = this.normalizeBillingCargoTypePath(path);
|
||
if (!normalizedPath.length) return '';
|
||
const cargoType = this.findBillingCargoTypeByPath(normalizedPath);
|
||
return String(cargoType?.cargoCode || cargoType?.code || normalizedPath.join('/'));
|
||
},
|
||
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.normalizeBillingCargoTypePath(path);
|
||
const labels = this.getBillingCargoTypePathLabels(normalizedPath);
|
||
const secondCargoType = this.findBillingCargoTypeByPath(normalizedPath);
|
||
return {
|
||
secondCargoTypeName: labels[1] || '',
|
||
secondCargoTypeCode: secondCargoType?.cargoCode || secondCargoType?.code || '',
|
||
};
|
||
},
|
||
loadTaskCargoOptionsByPath(path = [], cargoName = '') {
|
||
const normalizedPath = this.normalizeBillingCargoTypePath(path);
|
||
if (!normalizedPath.length) return Promise.resolve([]);
|
||
return this.ensureBillingCargoTypeOptions().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.normalizeBillingCargoTypePath(path);
|
||
// 移除空查询时返回空数组的限制,允许点击时显示货物列表
|
||
let loadingKey = '';
|
||
this.ensureBillingCargoTypeOptions()
|
||
.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.formatBillingCargoTypeLabel(item),
|
||
}))
|
||
);
|
||
return key;
|
||
});
|
||
})
|
||
.catch(error => {
|
||
window.console.log(error);
|
||
callback([]);
|
||
})
|
||
.finally(() => {
|
||
if (loadingKey) this.setTaskCargoLoadingByKey(loadingKey, false);
|
||
});
|
||
},
|
||
async handleTaskCargoRowSelect(row, item = {}) {
|
||
const value = this.formatBillingCargoTypeLabel(item);
|
||
row.cargoName = value;
|
||
row.specification = item.specification || item.spec || row.specification || '';
|
||
row.model = item.model || item.modelName || row.model || '';
|
||
|
||
// 确保货物类型树已加载
|
||
try {
|
||
await this.ensureBillingCargoTypeOptions();
|
||
} catch (error) {
|
||
console.warn('货物类型树加载失败', error);
|
||
this.syncTransportCargoJson();
|
||
return;
|
||
}
|
||
|
||
// 从常用货物返回的数据中查找货物类型并反选
|
||
const matchedCargoType = this.findBillingCargoTypeOption(item);
|
||
|
||
if (matchedCargoType?.path?.length) {
|
||
// 找到匹配的货物类型,回填到当前行
|
||
const fullPath = this.normalizeBillingCargoTypePath(matchedCargoType.path);
|
||
row.cargoTypePath = fullPath;
|
||
row.cargoType = matchedCargoType.cargoName || row.cargoType || '';
|
||
row.cargoTypeCode =
|
||
matchedCargoType.cargoCode || matchedCargoType.code || matchedCargoType.id || '';
|
||
|
||
console.log('货物名称选择后反选货物类型:', {
|
||
cargoName: row.cargoName,
|
||
cargoType: row.cargoType,
|
||
cargoTypePath: row.cargoTypePath,
|
||
matchedCargoType: matchedCargoType,
|
||
});
|
||
} else {
|
||
console.warn('未找到匹配的货物类型,item数据:', item);
|
||
}
|
||
|
||
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 || '',
|
||
}));
|
||
},
|
||
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)) : '';
|
||
},
|
||
handleDispatchMileageInput(value) {
|
||
this.dispatchItemForm.mileage = String(value || '')
|
||
.replace(/\D/g, '')
|
||
.slice(0, 10);
|
||
},
|
||
initTransportPlanFormRows() {
|
||
if (!this.config.enableTransportPlanForm) return;
|
||
const goodsRows = this.parseJsonArray(this.form.goodsJson);
|
||
this.transportCargoRows = goodsRows.map(row => this.normalizeTransportCargoRow(row));
|
||
if (!this.transportCargoRows.length) {
|
||
this.transportCargoRows.push(this.normalizeTransportCargoRow());
|
||
}
|
||
this.syncTransportCargoJson();
|
||
this.syncCurrentContractOption();
|
||
this.restoreTransportCargoTypePaths();
|
||
},
|
||
restoreTransportCargoTypePaths() {
|
||
if (!this.transportCargoRows.length) return Promise.resolve();
|
||
return this.ensureBillingCargoTypeOptions().then(() => {
|
||
this.transportCargoRows.forEach(row => {
|
||
if (this.normalizeBillingCargoTypePath(row.cargoTypePath).length) return;
|
||
const cargoType = this.findBillingCargoTypeByCodeOrName({
|
||
cargoTypeCode: row.cargoTypeCode,
|
||
cargoType: row.cargoType,
|
||
});
|
||
if (!cargoType?.path) return;
|
||
row.cargoTypePath = cargoType.path;
|
||
row.cargoType =
|
||
this.getBillingCargoTypePathLabels(cargoType.path).at(-1) || row.cargoType;
|
||
row.cargoTypeCode = cargoType.cargoCode || cargoType.code || row.cargoTypeCode;
|
||
});
|
||
this.syncTransportCargoJson();
|
||
});
|
||
},
|
||
normalizeTransportCargoRow(row = {}) {
|
||
const cargo = {
|
||
...defaultTransportCargo(),
|
||
...row,
|
||
cargoTypePath: this.normalizeBillingCargoTypePath(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', 'freightAmount'].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.config.enableTransportPlanForm) 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.config.enableTransportPlanForm) {
|
||
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.normalizeBillingCargoTypePath(value);
|
||
const cargoType = this.findBillingCargoTypeByPath(path);
|
||
const labels = this.getBillingCargoTypePathLabels(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.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);
|
||
}
|
||
},
|
||
ensureTransportTypeBeforeAddress() {
|
||
if (this.dispatchItemBox) {
|
||
const transportType =
|
||
this.dispatchItemForm.transportType || this.dispatchRow?.transportType || '';
|
||
if (!this.isBillingFieldEmpty(transportType)) return true;
|
||
this.$message.warning('请先选择运输方式');
|
||
return false;
|
||
}
|
||
if (!this.shippingInfoFormEnabled || !this.isBillingFieldEmpty(this.form.transportType)) {
|
||
return true;
|
||
}
|
||
this.$message.warning('请先选择运输方式');
|
||
return false;
|
||
},
|
||
openTransportRouteDialog() {
|
||
if (!this.ensureTransportTypeBeforeAddress()) return;
|
||
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.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;
|
||
const stationMode = this.dispatchItemBox ? this.dispatchStationMode : this.transportStationMode;
|
||
if (!stationMode) return;
|
||
this.transportStationTarget = target;
|
||
this.transportStationQuery = {};
|
||
this.transportStationPage.currentPage = 1;
|
||
this.transportStationBox = true;
|
||
},
|
||
getTransportStationListRequest() {
|
||
const mode = this.dispatchItemBox ? this.dispatchTransportMode : this.transportMode;
|
||
const requestMap = {
|
||
water: getPortTerminalList,
|
||
rail: getRailwayStationList,
|
||
air: getAirportMasterList,
|
||
};
|
||
return requestMap[mode];
|
||
},
|
||
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.resetTransportAddressQuery();
|
||
this.transportAddressPage.currentPage = 1;
|
||
this.transportAddressBox = true;
|
||
};
|
||
// Avue 远程字典可能尚未写入 option,先加载后再解析运输方式,避免按空类型查询全部地址。
|
||
if (!this.getTransportFixedAddressType()) {
|
||
this.loadTransportTypeOptions().finally(open);
|
||
return;
|
||
}
|
||
open();
|
||
},
|
||
openTransportAddressPicker(target) {
|
||
// 常用地址按钮统一使用地址库弹窗,按运输方式筛选并锁定地址类型。
|
||
this.openTransportAddressDialog(target);
|
||
},
|
||
resetTransportAddressQuery() {
|
||
const addressType = this.getTransportFixedAddressType();
|
||
this.transportAddressQuery = addressType ? { addressType } : {};
|
||
},
|
||
getTransportFixedAddressType() {
|
||
if (!this.config.fixedTransportAddressType) return '';
|
||
const typeMap = {
|
||
road: '常规地址',
|
||
rail: '铁路车站',
|
||
water: '港口/码头',
|
||
air: '空港机场',
|
||
};
|
||
const mode = this.dispatchItemBox ? this.dispatchTransportMode : this.transportMode;
|
||
return typeMap[mode] || '';
|
||
},
|
||
getTransportAddressForm() {
|
||
return this.dispatchItemBox ? this.dispatchItemForm : this.form;
|
||
},
|
||
openDispatchPrimaryAddressPicker(target) {
|
||
if (this.dispatchStationMode) {
|
||
this.openTransportStationDialog(target);
|
||
}
|
||
},
|
||
openDispatchSecondaryAddressPicker(target) {
|
||
if (this.dispatchStationMode) {
|
||
this.openTransportStationDialog(target);
|
||
return;
|
||
}
|
||
this.openTransportMapDialog(target);
|
||
},
|
||
handleDispatchSecondaryAddressInputClick(target) {
|
||
if (this.dispatchStationMode) {
|
||
this.openDispatchSecondaryAddressPicker(target);
|
||
}
|
||
},
|
||
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.applyTransportAddress(this.transportAddressTarget, {
|
||
...row,
|
||
preferRegionName: true,
|
||
});
|
||
this.transportAddressBox = false;
|
||
},
|
||
applyTransportAddress(target, address = {}) {
|
||
const prefix = target === 'departure' ? 'departure' : 'arrival';
|
||
const addressForm = this.getTransportAddressForm();
|
||
addressForm[`${prefix}Name`] = address.preferRegionName
|
||
? address.regionName || address.addressName || ''
|
||
: address.addressName || address.regionName || '';
|
||
addressForm[`${prefix}Address`] = address.detailAddress || address.address || '';
|
||
addressForm[`${prefix}AddressCode`] = address.addressCode || '';
|
||
addressForm[`${prefix}SiteCode`] =
|
||
address.addressType === '常规地址'
|
||
? '/'
|
||
: address.siteCodeDisplay || address.siteCode || '';
|
||
addressForm[`${prefix}Longitude`] = address.longitude || '';
|
||
addressForm[`${prefix}Latitude`] = address.latitude || '';
|
||
addressForm[`${prefix}Contact`] =
|
||
address.contactName || addressForm[`${prefix}Contact`] || '';
|
||
addressForm[`${prefix}Phone`] = address.contactPhone || addressForm[`${prefix}Phone`] || '';
|
||
// 地址值在本轮响应式更新后再清理校验状态,避免 Avue 以旧空值重新触发必填提示。
|
||
this.$nextTick(() => {
|
||
this.$refs.crud?.clearValidate?.([`${prefix}Address`]);
|
||
});
|
||
},
|
||
openTransportMapDialog(target) {
|
||
if (this.dialogReadonly && !this.dispatchItemBox) return;
|
||
if (!this.ensureTransportTypeBeforeAddress()) return;
|
||
const prefix = target === 'departure' ? 'departure' : 'arrival';
|
||
const addressForm = this.getTransportAddressForm();
|
||
this.transportMapTarget = prefix;
|
||
this.transportMapKeyword =
|
||
addressForm[`${prefix}Address`] || addressForm[`${prefix}Name`] || '';
|
||
this.transportMapSelected = this.buildTransportMapSelection({
|
||
lng: addressForm[`${prefix}Longitude`],
|
||
lat: addressForm[`${prefix}Latitude`],
|
||
address: addressForm[`${prefix}Address`],
|
||
regionName: addressForm[`${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;
|
||
const addressForm = this.getTransportAddressForm();
|
||
addressForm[`${prefix}Address`] =
|
||
this.transportMapSelected.detailAddress || addressForm[`${prefix}Address`];
|
||
addressForm[`${prefix}Name`] =
|
||
this.transportMapSelected.regionName || addressForm[`${prefix}Name`];
|
||
addressForm[`${prefix}Longitude`] = this.transportMapSelected.longitude;
|
||
addressForm[`${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.commonCargoQuery = {};
|
||
this.commonCargoPage.currentPage = 1;
|
||
this.commonCargoBox = true;
|
||
},
|
||
loadCommonCargoList() {
|
||
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();
|
||
},
|
||
handleCommonCargoTypeChange(value) {
|
||
const path = this.normalizeBillingCargoTypePath(value);
|
||
const labels = this.getBillingCargoTypePathLabels(path);
|
||
const firstCargoType = this.billingCargoTypeFlatOptions.find(
|
||
item => String(item.id) === String(path[0] || '') && item.path?.length === 1
|
||
);
|
||
const secondCargoType = this.findBillingCargoTypeByPath(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) {
|
||
await this.ensureBillingCargoTypeOptions();
|
||
const cargo = this.mapCommonCargoRow(row);
|
||
if (this.dispatchItemBox && this.dispatchItemForm.taskEntryMode === 'full') {
|
||
this.addDispatchCargoRow(this.dispatchItemCargoRows.length - 1, cargo);
|
||
} else {
|
||
this.addTransportCargoRow(-1, cargo);
|
||
}
|
||
this.commonCargoBox = false;
|
||
},
|
||
mapCommonCargoRow(row = {}) {
|
||
const cargoTypePath = this.resolveCommonCargoTypePath(row);
|
||
const cargoType = this.findBillingCargoTypeByPath(cargoTypePath);
|
||
const labels = this.getBillingCargoTypePathLabels(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 => {
|
||
if (this.dispatchItemBox && this.dispatchItemForm.taskEntryMode === 'full') {
|
||
this.addDispatchCargoRow(this.dispatchItemCargoRows.length - 1, row);
|
||
} else {
|
||
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');
|
||
});
|
||
},
|
||
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);
|
||
});
|
||
},
|
||
handleDispatchItemAttachmentChange(list) {
|
||
const userName = this.userInfo?.realName || this.userInfo?.userName || '';
|
||
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||
this.dispatchItemAttachmentRows = (list || []).map(item => ({
|
||
...item,
|
||
description: item.description || '',
|
||
uploadUserName: item.uploadUserName || userName,
|
||
uploadTime: item.uploadTime || uploadTime,
|
||
}));
|
||
this.selectedDispatchItemAttachmentRows = [];
|
||
},
|
||
removeDispatchItemAttachment(index) {
|
||
this.dispatchItemAttachmentRows.splice(index, 1);
|
||
this.selectedDispatchItemAttachmentRows = this.selectedDispatchItemAttachmentRows.filter(
|
||
item => this.dispatchItemAttachmentRows.includes(item)
|
||
);
|
||
},
|
||
handleDispatchItemBatchDownload() {
|
||
const rows = this.selectedDispatchItemAttachmentRows.length
|
||
? this.selectedDispatchItemAttachmentRows
|
||
: this.dispatchItemAttachmentRows;
|
||
rows.forEach(row => this.downloadAttachment(row));
|
||
},
|
||
handleDispatchItemAttachmentSelectionChange(rows) {
|
||
this.selectedDispatchItemAttachmentRows = rows || [];
|
||
},
|
||
ensureBillingCargoTypeOptions() {
|
||
if (!this.config.enableTransportPlanForm) {
|
||
return Promise.resolve([]);
|
||
}
|
||
if (this.billingCargoTypeOptions.length) {
|
||
return Promise.resolve(this.billingCargoTypeOptions);
|
||
}
|
||
if (this.billingCargoTypeRequest) {
|
||
return this.billingCargoTypeRequest;
|
||
}
|
||
this.billingCargoTypeLoading = true;
|
||
this.billingCargoTypeRequest = getCargoTypeList(1, 9999)
|
||
.then(res => {
|
||
this.billingCargoTypeOptions = this.buildBillingCargoTypeTree(extractRecords(res));
|
||
this.billingCargoTypeFlatOptions = this.flattenBillingCargoTypeOptions(
|
||
this.billingCargoTypeOptions
|
||
);
|
||
return this.billingCargoTypeOptions;
|
||
})
|
||
.finally(() => {
|
||
this.billingCargoTypeLoading = false;
|
||
this.billingCargoTypeRequest = null;
|
||
});
|
||
return this.billingCargoTypeRequest;
|
||
},
|
||
buildBillingCargoTypeTree(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.formatBillingCargoTypeLabel(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.normalizeBillingCargoTypeNode(item, 1)).filter(Boolean);
|
||
},
|
||
normalizeBillingCargoTypeNode(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.normalizeBillingCargoTypeNode(item, level + 1, path))
|
||
.filter(Boolean)
|
||
: [];
|
||
return {
|
||
...cargoType,
|
||
id,
|
||
cargoName: this.formatBillingCargoTypeLabel(cargoType),
|
||
cargoCode: cargoType.cargoCode || cargoType.code || '',
|
||
path,
|
||
leaf: level === 2,
|
||
children: children.length ? children : undefined,
|
||
};
|
||
},
|
||
flattenBillingCargoTypeOptions(options = []) {
|
||
const result = [];
|
||
const collectOptions = list => {
|
||
(list || []).forEach(item => {
|
||
result.push(item);
|
||
if (item.children?.length) collectOptions(item.children);
|
||
});
|
||
};
|
||
collectOptions(options);
|
||
return result;
|
||
},
|
||
formatBillingCargoTypeLabel(item = {}) {
|
||
return item.cargoName || item.name || item.typeName || item.label || '';
|
||
},
|
||
filterBillingCargoType(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)
|
||
);
|
||
},
|
||
getBillingCargoTypePathLabels(path = []) {
|
||
let options = this.billingCargoTypeOptions;
|
||
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);
|
||
},
|
||
findBillingCargoTypeByPath(path = []) {
|
||
const value = Array.isArray(path) && path.length >= 2 ? String(path[1]) : '';
|
||
if (!value) return null;
|
||
return (
|
||
this.billingCargoTypeFlatOptions.find(
|
||
item => String(item.id) === value && item.path?.length >= 2
|
||
) || null
|
||
);
|
||
},
|
||
findBillingCargoTypeByCodeOrName(condition = {}) {
|
||
const code = String(condition.cargoTypeCode || '').trim();
|
||
const name = String(condition.cargoType || '').trim();
|
||
return (
|
||
this.billingCargoTypeFlatOptions.find(item => {
|
||
const cargoCode = String(item.cargoCode || item.code || item.id || '');
|
||
return item.path?.length >= 2 && code && cargoCode === code;
|
||
}) ||
|
||
this.billingCargoTypeFlatOptions.find(item => {
|
||
const cargoName = this.formatBillingCargoTypeLabel(item);
|
||
return item.path?.length >= 2 && name && (cargoName === name || name.endsWith(cargoName));
|
||
}) ||
|
||
null
|
||
);
|
||
},
|
||
findBillingCargoTypeOption(row = {}) {
|
||
const path = this.normalizeBillingCargoTypePath(row.cargoTypePath);
|
||
if (path.length) {
|
||
const pathKey = path.join('/');
|
||
const pathMatch = this.billingCargoTypeFlatOptions.find(item => item.path?.join('/') === pathKey);
|
||
if (pathMatch) return pathMatch;
|
||
}
|
||
|
||
// 优先通过二级类型编码查找(最准确)
|
||
if (row.secondCargoTypeCode) {
|
||
const secondMatch = this.billingCargoTypeFlatOptions.find(
|
||
item => String(item.cargoCode || item.code || '').trim() === String(row.secondCargoTypeCode).trim()
|
||
);
|
||
if (secondMatch) {
|
||
console.log('通过 secondCargoTypeCode 找到货物类型:', secondMatch);
|
||
return secondMatch;
|
||
}
|
||
}
|
||
|
||
// 其他候选编码
|
||
const codeCandidates = [row.cargoTypeCode, row.firstCargoTypeCode]
|
||
.map(value => String(value || '').trim())
|
||
.filter(Boolean);
|
||
const nameCandidates = [
|
||
row.secondCargoTypeName,
|
||
row.cargoType,
|
||
row.goodsType,
|
||
row.firstCargoTypeName,
|
||
]
|
||
.map(value => String(value || '').trim())
|
||
.filter(Boolean);
|
||
|
||
return (
|
||
this.billingCargoTypeFlatOptions.find(item =>
|
||
codeCandidates.some(
|
||
code => code === String(item.cargoCode || item.code || item.id || '').trim()
|
||
)
|
||
) ||
|
||
this.billingCargoTypeFlatOptions.find(item =>
|
||
nameCandidates.some(name => name === String(item.cargoName || '').trim())
|
||
)
|
||
);
|
||
},
|
||
resolveCommonCargoTypePath(row = {}) {
|
||
const path = this.normalizeBillingCargoTypePath(row.cargoTypePath);
|
||
if (path.length) return path;
|
||
const cargoType =
|
||
this.findBillingCargoTypeByCodeOrName({
|
||
cargoTypeCode: row.secondCargoTypeCode || row.firstCargoTypeCode || row.cargoTypeCode,
|
||
cargoType: row.secondCargoTypeName || row.firstCargoTypeName || row.cargoType,
|
||
}) ||
|
||
this.findBillingCargoTypeByCodeOrName({
|
||
cargoTypeCode: row.firstCargoTypeCode || '',
|
||
cargoType: row.firstCargoTypeName || '',
|
||
});
|
||
return cargoType?.path || [];
|
||
},
|
||
normalizeBillingCargoTypePath(path) {
|
||
return Array.isArray(path) && path.length >= 2
|
||
? path.slice(0, 2).map(value => String(value))
|
||
: [];
|
||
},
|
||
isBillingFieldEmpty(value) {
|
||
return value === undefined || value === null || String(value).trim() === '';
|
||
},
|
||
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);
|
||
},
|
||
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: 0,
|
||
});
|
||
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: 0 });
|
||
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();
|
||
});
|
||
});
|
||
},
|
||
handleImport() {
|
||
if (this.config.enableTransportPlanImport) {
|
||
this.$router.push('/business/transport-plan/import');
|
||
return;
|
||
}
|
||
this.$message.warning('运输计划导入接口未配置');
|
||
},
|
||
resetTransportPlanImport() {
|
||
this.transportPlanImportForm = {
|
||
projectId: '',
|
||
projectName: '',
|
||
customerName: '',
|
||
contractId: '',
|
||
contractName: '',
|
||
file: null,
|
||
};
|
||
this.transportPlanImportFiles = [];
|
||
this.transportPlanImportSourceFile = null;
|
||
this.transportPlanImportRows = [];
|
||
this.transportPlanImportSelection = [];
|
||
this.transportPlanImportTab = 'all';
|
||
this.transportPlanImportEditingKey = '';
|
||
this.transportPlanImportEditSnapshot = null;
|
||
this.transportPlanImportContractOptions = [];
|
||
this.transportPlanImportContractLoading = false;
|
||
this.$refs.transportPlanImportFormRef?.clearValidate();
|
||
},
|
||
handleTransportPlanImportProjectChange(projectId) {
|
||
const project = this.projectOptions.find(item => String(item.id) === String(projectId));
|
||
this.transportPlanImportForm.customerName =
|
||
project?.customerName || project?.customerNames || project?.customer || '';
|
||
this.transportPlanImportForm.projectName = project?.projectName || '';
|
||
this.transportPlanImportForm.contractId = '';
|
||
this.transportPlanImportForm.contractName = '';
|
||
this.transportPlanImportContractOptions = [];
|
||
if (!projectId) return;
|
||
this.transportPlanImportContractLoading = true;
|
||
getContractList(1, 9999, {
|
||
projectId,
|
||
projectName: project?.projectName,
|
||
...(this.config.contractQueryParams || {}),
|
||
})
|
||
.then(res => {
|
||
this.transportPlanImportContractOptions = extractRecords(res);
|
||
const contract = this.transportPlanImportContractOptions[0];
|
||
this.transportPlanImportForm.customerName =
|
||
this.transportPlanImportForm.customerName || contract?.customerName || '';
|
||
this.transportPlanImportForm.contractId = contract?.id || '';
|
||
this.transportPlanImportForm.contractName = contract?.contractName || '';
|
||
})
|
||
.finally(() => {
|
||
this.transportPlanImportContractLoading = false;
|
||
});
|
||
},
|
||
handleTransportPlanImportContractChange(contractId) {
|
||
const contract = this.transportPlanImportContractOptions.find(
|
||
item => String(item.id) === String(contractId)
|
||
);
|
||
this.transportPlanImportForm.contractName = contract?.contractName || '';
|
||
},
|
||
async handleTransportPlanImportFileChange(file, fileList) {
|
||
const isExcel = /\.(xls|xlsx)$/i.test(file.name || '');
|
||
if (!isExcel) {
|
||
this.$message.error('请上传 .xls 或 .xlsx 格式文件');
|
||
this.transportPlanImportFiles = [];
|
||
this.transportPlanImportForm.file = null;
|
||
return;
|
||
}
|
||
try {
|
||
const XLSX = await import('xlsx');
|
||
const workbook = XLSX.read(await file.raw.arrayBuffer(), {
|
||
type: 'array',
|
||
cellDates: false,
|
||
});
|
||
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
|
||
const values = XLSX.utils.sheet_to_json(worksheet, {
|
||
header: 1,
|
||
defval: '',
|
||
raw: false,
|
||
blankrows: false,
|
||
});
|
||
const headers = values[0] || [];
|
||
const headerMap = headers.reduce((result, label, index) => {
|
||
const normalized = String(label || '')
|
||
.replace(/^\*/, '')
|
||
.trim();
|
||
if (normalized) result[normalized] = index;
|
||
return result;
|
||
}, {});
|
||
const rows = values.slice(1).reduce((result, cells, index) => {
|
||
const row = {
|
||
_key: `${Date.now()}-${index}-${Math.random().toString(36).slice(2)}`,
|
||
};
|
||
transportPlanImportColumns.forEach(([label, prop]) => {
|
||
row[prop] = String(cells[headerMap[label]] ?? '').trim();
|
||
});
|
||
if (transportPlanImportColumns.some(([, prop]) => row[prop])) result.push(row);
|
||
return result;
|
||
}, []);
|
||
if (!rows.length) {
|
||
this.$message.warning('未读取到计划明细数据');
|
||
this.transportPlanImportFiles = [];
|
||
this.transportPlanImportForm.file = null;
|
||
return;
|
||
}
|
||
this.transportPlanImportFiles = fileList.slice(-1);
|
||
this.transportPlanImportSourceFile = file.raw;
|
||
this.transportPlanImportForm.file = file.raw;
|
||
this.transportPlanImportRows = rows;
|
||
this.markTransportPlanImportDuplicates();
|
||
this.$refs.transportPlanImportFormRef?.validateField('file');
|
||
this.$message.success(`已读取${rows.length}条计划明细`);
|
||
} catch (error) {
|
||
this.transportPlanImportFiles = [];
|
||
this.transportPlanImportSourceFile = null;
|
||
this.transportPlanImportForm.file = null;
|
||
this.transportPlanImportRows = [];
|
||
this.$message.error('计划明细表读取失败,请检查文件格式');
|
||
}
|
||
},
|
||
handleTransportPlanImportFileRemove() {
|
||
this.transportPlanImportFiles = [];
|
||
this.transportPlanImportSourceFile = null;
|
||
this.transportPlanImportForm.file = null;
|
||
this.transportPlanImportRows = [];
|
||
this.transportPlanImportSelection = [];
|
||
this.transportPlanImportEditingKey = '';
|
||
this.transportPlanImportEditSnapshot = null;
|
||
},
|
||
transportPlanImportDuplicateKey(row) {
|
||
return transportPlanImportColumns
|
||
.map(([, prop]) => String(row[prop] || '').trim())
|
||
.join('\u0001');
|
||
},
|
||
markTransportPlanImportDuplicates() {
|
||
const countMap = this.transportPlanImportRows.reduce((result, row) => {
|
||
const key = this.transportPlanImportDuplicateKey(row);
|
||
result[key] = (result[key] || 0) + 1;
|
||
return result;
|
||
}, {});
|
||
this.transportPlanImportRows.forEach(row => {
|
||
row._duplicate = countMap[this.transportPlanImportDuplicateKey(row)] > 1;
|
||
});
|
||
},
|
||
handleTransportPlanImportSelectionChange(rows) {
|
||
this.transportPlanImportSelection = rows;
|
||
},
|
||
isEditingTransportPlanImportRow(row) {
|
||
return this.transportPlanImportEditingKey === row._key;
|
||
},
|
||
editTransportPlanImportRow(row) {
|
||
if (this.transportPlanImportEditingKey && this.transportPlanImportEditingKey !== row._key) {
|
||
this.$message.warning('请先保存或取消当前编辑');
|
||
return;
|
||
}
|
||
this.transportPlanImportEditingKey = row._key;
|
||
this.transportPlanImportEditSnapshot = { ...row };
|
||
},
|
||
saveTransportPlanImportRow() {
|
||
this.transportPlanImportEditingKey = '';
|
||
this.transportPlanImportEditSnapshot = null;
|
||
this.markTransportPlanImportDuplicates();
|
||
},
|
||
cancelTransportPlanImportRow(row) {
|
||
Object.assign(row, this.transportPlanImportEditSnapshot || {});
|
||
this.transportPlanImportEditingKey = '';
|
||
this.transportPlanImportEditSnapshot = null;
|
||
this.markTransportPlanImportDuplicates();
|
||
},
|
||
removeTransportPlanImportRow(row) {
|
||
this.transportPlanImportRows = this.transportPlanImportRows.filter(
|
||
item => item._key !== row._key
|
||
);
|
||
this.transportPlanImportSelection = this.transportPlanImportSelection.filter(
|
||
item => item._key !== row._key
|
||
);
|
||
this.markTransportPlanImportDuplicates();
|
||
},
|
||
removeSelectedTransportPlanImportRows() {
|
||
const selectedKeys = new Set(this.transportPlanImportSelection.map(row => row._key));
|
||
this.transportPlanImportRows = this.transportPlanImportRows.filter(
|
||
row => !selectedKeys.has(row._key)
|
||
);
|
||
this.transportPlanImportSelection = [];
|
||
this.markTransportPlanImportDuplicates();
|
||
},
|
||
validateTransportPlanImportRows() {
|
||
if (!this.transportPlanImportRows.length) {
|
||
this.$message.warning('请保留至少一条计划明细');
|
||
return false;
|
||
}
|
||
const requiredFields = [
|
||
['planName', '计划名称'],
|
||
['transportType', '运输方式'],
|
||
['planStartDate', '计划开始日期'],
|
||
['planEndDate', '计划结束日期'],
|
||
['cargoType', '货物类型'],
|
||
['departureAddress', '发货地址'],
|
||
['arrivalAddress', '收货地址'],
|
||
];
|
||
for (const [index, row] of this.transportPlanImportRows.entries()) {
|
||
const emptyField = requiredFields.find(([prop]) => !String(row[prop] || '').trim());
|
||
if (emptyField) {
|
||
this.$message.warning(`第${index + 1}行${emptyField[1]}不能为空`);
|
||
return false;
|
||
}
|
||
const isValidDate = value => {
|
||
const match = String(value || '').match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||
if (!match) return false;
|
||
const [, year, month, day] = match;
|
||
const date = new Date(Number(year), Number(month) - 1, Number(day));
|
||
return (
|
||
date.getFullYear() === Number(year) &&
|
||
date.getMonth() === Number(month) - 1 &&
|
||
date.getDate() === Number(day)
|
||
);
|
||
};
|
||
if (!isValidDate(row.planStartDate) || !isValidDate(row.planEndDate)) {
|
||
this.$message.warning(`第${index + 1}行计划日期格式必须为 YYYY-MM-DD`);
|
||
return false;
|
||
}
|
||
if (row.planEndDate < row.planStartDate) {
|
||
this.$message.warning(`第${index + 1}行计划结束日期不能早于计划开始日期`);
|
||
return false;
|
||
}
|
||
if (
|
||
row.quantity !== '' &&
|
||
(!Number.isFinite(Number(row.quantity)) || Number(row.quantity) < 0)
|
||
) {
|
||
this.$message.warning(`第${index + 1}行数量必须为非负数`);
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
},
|
||
async buildTransportPlanImportFile() {
|
||
const XLSX = await import('xlsx');
|
||
const headers = transportPlanImportColumns.map(([label]) => {
|
||
return [
|
||
'计划名称',
|
||
'运输方式',
|
||
'计划开始日期',
|
||
'计划结束日期',
|
||
'货物类型',
|
||
'发货地址',
|
||
'收货地址',
|
||
].includes(label)
|
||
? `*${label}`
|
||
: label;
|
||
});
|
||
const values = [
|
||
headers,
|
||
...this.transportPlanImportRows.map(row =>
|
||
transportPlanImportColumns.map(([, prop]) => String(row[prop] ?? ''))
|
||
),
|
||
];
|
||
const workbook = XLSX.utils.book_new();
|
||
XLSX.utils.book_append_sheet(workbook, XLSX.utils.aoa_to_sheet(values), '运输计划导入模板');
|
||
const binary = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
|
||
const fileName = (
|
||
this.transportPlanImportSourceFile?.name || '运输计划导入模板.xlsx'
|
||
).replace(/\.xls$/i, '.xlsx');
|
||
return new File([binary], fileName, {
|
||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||
});
|
||
},
|
||
handleTransportPlanTemplate() {
|
||
exportBlob(this.config.transportPlanTemplateUrl, {}, { feedback: true }).then(res => {
|
||
downloadXls(res.data, '运输计划导入模板.xlsx');
|
||
});
|
||
},
|
||
async submitTransportPlanImport() {
|
||
const valid = await this.$refs.transportPlanImportFormRef?.validate().catch(() => false);
|
||
if (!valid || this.transportPlanImportLoading) return;
|
||
if (!this.validateTransportPlanImportRows()) return;
|
||
if (typeof this.api.importTransportPlan !== 'function') {
|
||
this.$message.error('运输计划导入接口不可用');
|
||
return;
|
||
}
|
||
this.transportPlanImportLoading = true;
|
||
try {
|
||
const file = await this.buildTransportPlanImportFile();
|
||
const res = await this.api.importTransportPlan({ ...this.transportPlanImportForm, file });
|
||
const contentType = res.headers?.['content-type'] || res.data?.type || '';
|
||
if (
|
||
contentType.includes('application/vnd.ms-excel') ||
|
||
contentType.includes('spreadsheetml')
|
||
) {
|
||
downloadXls(
|
||
res.data,
|
||
`运输计划导入失败明细${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`
|
||
);
|
||
this.$message.warning('部分数据导入失败,已下载失败明细');
|
||
} else {
|
||
const text = await res.data.text();
|
||
const result = text ? JSON.parse(text) : {};
|
||
if (result.code !== 200) throw new Error(result.msg || '导入失败');
|
||
this.$message.success('导入完成');
|
||
}
|
||
this.transportPlanImportBox = false;
|
||
this.onLoad(this.page, this.query);
|
||
} catch (error) {
|
||
this.$message.error(error.message || '导入失败');
|
||
} finally {
|
||
this.transportPlanImportLoading = false;
|
||
}
|
||
},
|
||
handleCopy(row) {
|
||
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);
|
||
});
|
||
},
|
||
handleAction(action, row) {
|
||
const actionName = {
|
||
complete: this.completeActionLabel,
|
||
}[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);
|
||
});
|
||
},
|
||
handleCustomOperation(operation, row) {
|
||
if (operation.action === 'flow') {
|
||
this.handleFlow(row);
|
||
return;
|
||
}
|
||
if (operation.action === 'dispatch') {
|
||
this.openDispatchDialog(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;
|
||
},
|
||
openDispatchDialog(row) {
|
||
if (!row?.id) {
|
||
this.$message.warning('运输计划数据异常,无法调度');
|
||
return;
|
||
}
|
||
// 跳转到独立的调度页面
|
||
this.$router.push({
|
||
path: '/business/transport-plan-dispatch',
|
||
query: {
|
||
planId: row.id,
|
||
},
|
||
});
|
||
},
|
||
loadDispatchTransportTypeOptions() {
|
||
if (this.dispatchTransportTypeOptions.length || this.dispatchTransportTypeLoading) {
|
||
return Promise.resolve(this.dispatchTransportTypeOptions);
|
||
}
|
||
this.dispatchTransportTypeLoading = true;
|
||
return getDictionary({ code: 'transport_type' })
|
||
.then(res => {
|
||
this.dispatchTransportTypeOptions = this.normalizeDictOptions(res.data?.data || []);
|
||
return this.dispatchTransportTypeOptions;
|
||
})
|
||
.finally(() => {
|
||
this.dispatchTransportTypeLoading = false;
|
||
});
|
||
},
|
||
formatDispatchTransportType(value) {
|
||
const item = this.dispatchTransportTypeOptions.find(
|
||
option => String(option.value) === String(value ?? '')
|
||
);
|
||
return item?.label || value || '';
|
||
},
|
||
formatDispatchDriver(row = {}) {
|
||
return [row.driverName, row.driverPhone].filter(Boolean).join(' / ') || '-';
|
||
},
|
||
resetDispatchDialog() {
|
||
this.dispatchRow = {};
|
||
this.dispatchContractCurrency = 'RMB';
|
||
this.dispatchRows = [];
|
||
this.dispatchLoading = false;
|
||
this.dispatchSaving = '';
|
||
this.dispatchCarrierRequestId += 1;
|
||
this.dispatchCarrierOptions = [];
|
||
this.dispatchCarrierLoading = false;
|
||
this.dispatchItemBox = false;
|
||
this.dispatchItemIndex = -1;
|
||
this.dispatchItemForm = defaultDispatchRow();
|
||
this.dispatchItemCargoRows = [];
|
||
this.dispatchItemAttachmentRows = [];
|
||
this.selectedDispatchItemAttachmentRows = [];
|
||
},
|
||
buildDispatchRows(plan = {}) {
|
||
const persistedSources = [
|
||
plan.dispatchRows,
|
||
plan.dispatchRowList,
|
||
plan.dispatchRecords,
|
||
plan.dispatchRecordList,
|
||
plan.dispatchList,
|
||
plan.waybillList,
|
||
plan.waybillRows,
|
||
plan.waybills,
|
||
];
|
||
const persistedRows = persistedSources.reduce((result, source) => {
|
||
const rows = this.parseTransportPlanWaybillSource(source);
|
||
return result.length ? result : rows;
|
||
}, []);
|
||
const goodsRows = this.dispatchGoodsRows(plan.goodsJson);
|
||
if (!goodsRows.length) {
|
||
const normalizedPersistedRows = persistedRows.map((item, index) =>
|
||
this.normalizeDispatchRow(plan, item, index)
|
||
);
|
||
return normalizedPersistedRows.length
|
||
? normalizedPersistedRows
|
||
: [this.normalizeDispatchRow(plan, plan, 0)];
|
||
}
|
||
|
||
const assignedQuantities = new Map();
|
||
const pendingQuantities = new Map();
|
||
persistedRows.forEach(row => {
|
||
if (!this.hasDispatchVehicleIdentifier(row)) return;
|
||
this.getDispatchGoodsSourceRows(row).forEach(goods => {
|
||
const key = this.getDispatchCargoIdentity(goods);
|
||
assignedQuantities.set(
|
||
key,
|
||
(assignedQuantities.get(key) || 0) +
|
||
this.parseDispatchQuantity(
|
||
goods.quantity || goods.cargoQuantity || goods.goodsQuantity
|
||
)
|
||
);
|
||
});
|
||
});
|
||
|
||
const normalizedPersistedRows = [];
|
||
persistedRows.forEach((item, index) => {
|
||
if (this.hasDispatchVehicleIdentifier(item)) {
|
||
normalizedPersistedRows.push(this.normalizeDispatchRow(plan, item, index));
|
||
return;
|
||
}
|
||
const pendingGoodsRows = this.getDispatchGoodsSourceRows(item)
|
||
.map(goods => {
|
||
const totalQuantity = this.parseDispatchQuantity(
|
||
goods.quantity || goods.cargoQuantity || goods.goodsQuantity
|
||
);
|
||
if (totalQuantity <= 0) return goods;
|
||
const remainingQuantity = Math.max(
|
||
totalQuantity - (assignedQuantities.get(this.getDispatchCargoIdentity(goods)) || 0),
|
||
0
|
||
);
|
||
if (!remainingQuantity) return null;
|
||
return {
|
||
...goods,
|
||
quantity: this.formatDispatchQuantity(remainingQuantity),
|
||
};
|
||
})
|
||
.filter(Boolean);
|
||
if (!pendingGoodsRows.length) return;
|
||
pendingGoodsRows.forEach(goods => {
|
||
const key = this.getDispatchCargoIdentity(goods);
|
||
pendingQuantities.set(
|
||
key,
|
||
(pendingQuantities.get(key) || 0) +
|
||
this.parseDispatchQuantity(
|
||
goods.quantity || goods.cargoQuantity || goods.goodsQuantity
|
||
)
|
||
);
|
||
});
|
||
normalizedPersistedRows.push(
|
||
this.normalizeDispatchRow(
|
||
plan,
|
||
{
|
||
...item,
|
||
goodsJson: JSON.stringify(pendingGoodsRows),
|
||
quantity: pendingGoodsRows[0].quantity || item.quantity || '',
|
||
},
|
||
index
|
||
)
|
||
);
|
||
});
|
||
|
||
const remainingRows = goodsRows
|
||
.map((goods, index) => {
|
||
const totalQuantity = this.parseDispatchQuantity(
|
||
goods.quantity || goods.cargoQuantity || goods.goodsQuantity
|
||
);
|
||
const key = this.getDispatchCargoIdentity(goods);
|
||
const remainingQuantity = Math.max(
|
||
totalQuantity - (assignedQuantities.get(key) || 0) - (pendingQuantities.get(key) || 0),
|
||
0
|
||
);
|
||
if (!remainingQuantity) return null;
|
||
const pendingGoods = {
|
||
...goods,
|
||
quantity: this.formatDispatchQuantity(remainingQuantity),
|
||
};
|
||
const row = this.normalizeDispatchRow(
|
||
plan,
|
||
{
|
||
...pendingGoods,
|
||
goodsJson: JSON.stringify([pendingGoods]),
|
||
taskEntryMode: 'simple',
|
||
},
|
||
persistedRows.length + index
|
||
);
|
||
row.quantity = this.formatDispatchQuantity(remainingQuantity);
|
||
row.taskEntryMode = 'simple';
|
||
return row;
|
||
})
|
||
.filter(Boolean);
|
||
|
||
return [...normalizedPersistedRows, ...remainingRows];
|
||
},
|
||
getDispatchGoodsSourceRows(row = {}) {
|
||
const rows = this.dispatchGoodsRows(row.goodsJson);
|
||
return rows.length ? rows : [row];
|
||
},
|
||
getDispatchUnscheduledGoodsRows() {
|
||
const goodsRows = this.dispatchPlanGoodsRows;
|
||
if (!goodsRows.length) return [];
|
||
const assignedQuantities = new Map();
|
||
this.dispatchRows.forEach(row => {
|
||
if (!this.hasDispatchVehicleIdentifier(row)) return;
|
||
this.getDispatchGoodsSourceRows(row).forEach(goods => {
|
||
const key = this.getDispatchCargoIdentity(goods);
|
||
assignedQuantities.set(
|
||
key,
|
||
(assignedQuantities.get(key) || 0) +
|
||
this.parseDispatchQuantity(
|
||
goods.quantity || goods.cargoQuantity || goods.goodsQuantity
|
||
)
|
||
);
|
||
});
|
||
});
|
||
return goodsRows
|
||
.filter(goods => {
|
||
const totalQuantity = this.parseDispatchQuantity(
|
||
goods.quantity || goods.cargoQuantity || goods.goodsQuantity
|
||
);
|
||
const assignedQuantity =
|
||
assignedQuantities.get(this.getDispatchCargoIdentity(goods)) || 0;
|
||
return totalQuantity <= 0 || assignedQuantity < totalQuantity;
|
||
})
|
||
.map(goods => this.normalizeTransportCargoRow({ ...goods, quantity: '' }));
|
||
},
|
||
getDispatchCargoIdentity(row = {}) {
|
||
return [
|
||
row.cargoName || row.goodsName || row.name || '',
|
||
row.cargoType || row.secondCargoTypeName || row.goodsType || row.type || '',
|
||
this.getDispatchQuantityUnit(row),
|
||
]
|
||
.map(value => String(value || '').trim())
|
||
.join('|');
|
||
},
|
||
normalizeDispatchRow(plan = {}, item = {}, index = 0) {
|
||
const itemGoodsRows = this.dispatchGoodsRows(item.goodsJson);
|
||
const itemGoods = itemGoodsRows[0] || {};
|
||
const cargoInfo = this.formatDispatchCargoInfo({}, item);
|
||
const waybillFallback = this.getDispatchWaybillFallback(plan, item, index);
|
||
const feeFields = this.getDispatchFeeFields(plan, item);
|
||
return {
|
||
_key: `${plan.id || 'dispatch'}-${index}-${
|
||
item.id || item.cargoName || item.vehicleNo || 'row'
|
||
}`,
|
||
taskEntryMode: item.taskEntryMode || 'simple',
|
||
transportType: item.transportType || plan.transportType || '',
|
||
carrierType: item.carrierType || plan.carrierType || '承运商',
|
||
carrierId: item.carrierId || plan.carrierId || '',
|
||
carrierContractId: item.carrierContractId || plan.carrierContractId || '',
|
||
carrierName: item.carrierName || plan.carrierName || '',
|
||
driverName: item.driverName || plan.driverName || '',
|
||
driverPhone: item.driverPhone || plan.driverPhone || '',
|
||
vehicleNo:
|
||
item.vehicleNo ||
|
||
item.vehicleNumber ||
|
||
item.flightNo ||
|
||
item.shipNo ||
|
||
item.trainNo ||
|
||
plan.vehicleNo ||
|
||
plan.vehicleNumber ||
|
||
plan.flightNo ||
|
||
plan.shipNo ||
|
||
plan.trainNo ||
|
||
'',
|
||
trailerVehicleNo: item.trailerVehicleNo || '',
|
||
escortName: item.escortName || '',
|
||
escortPhone: item.escortPhone || '',
|
||
mileage: item.mileage || '',
|
||
cargoType:
|
||
itemGoods.cargoType ||
|
||
itemGoods.goodsType ||
|
||
item.cargoType ||
|
||
item.goodsType ||
|
||
plan.cargoType ||
|
||
'',
|
||
cargoName:
|
||
itemGoods.cargoName ||
|
||
itemGoods.goodsName ||
|
||
item.cargoName ||
|
||
item.goodsName ||
|
||
plan.cargoName ||
|
||
'',
|
||
cargoInfo,
|
||
quantity:
|
||
itemGoods.quantity ||
|
||
itemGoods.cargoQuantity ||
|
||
itemGoods.goodsQuantity ||
|
||
item.quantity ||
|
||
item.cargoQuantity ||
|
||
item.goodsQuantity ||
|
||
'',
|
||
quantityUnit:
|
||
itemGoods.quantityUnit ||
|
||
itemGoods.goodsQuantityUnit ||
|
||
itemGoods.unit ||
|
||
item.quantityUnit ||
|
||
item.goodsQuantityUnit ||
|
||
item.unit ||
|
||
TRANSPORT_PLAN_QUANTITY_UNIT,
|
||
specification:
|
||
item.specification || item.spec || itemGoods.specification || itemGoods.spec || '',
|
||
model: item.model || itemGoods.model || '',
|
||
packageType:
|
||
item.packageType || item.package || itemGoods.packageType || itemGoods.package || '',
|
||
estimatedStartTime: item.estimatedStartTime || plan.planStartDate || '',
|
||
estimatedEndTime: item.estimatedEndTime || plan.planEndDate || '',
|
||
departureAddress: item.departureAddress || plan.departureAddress || '',
|
||
departureName: item.departureName || plan.departureName || '',
|
||
departureContact: item.departureContact || plan.departureContact || '',
|
||
departurePhone: item.departurePhone || plan.departurePhone || '',
|
||
arrivalAddress: item.arrivalAddress || plan.arrivalAddress || '',
|
||
arrivalName: item.arrivalName || plan.arrivalName || '',
|
||
arrivalContact: item.arrivalContact || plan.arrivalContact || '',
|
||
arrivalPhone: item.arrivalPhone || plan.arrivalPhone || '',
|
||
...feeFields,
|
||
dispatchUserName:
|
||
item.dispatchUserName ||
|
||
plan.dispatchUserName ||
|
||
waybillFallback.createUserName ||
|
||
item.createUserName ||
|
||
'',
|
||
dispatchTime:
|
||
item.dispatchTime ||
|
||
plan.dispatchTime ||
|
||
waybillFallback.createTime ||
|
||
item.createTime ||
|
||
'',
|
||
goodsJson: item.goodsJson || '',
|
||
attachmentsJson: item.attachmentsJson || '',
|
||
remark: item.remark || item.taskRemark || plan.remark || '',
|
||
};
|
||
},
|
||
getDispatchFeeFields(plan = {}, item = {}) {
|
||
const freightInfo = this.parseJsonObject(item.freightJson || plan.freightJson);
|
||
const itemGoods = this.dispatchGoodsRows(item.goodsJson)[0] || {};
|
||
const unitPrice =
|
||
item.unitPrice ??
|
||
itemGoods.unitPrice ??
|
||
plan.unitPrice ??
|
||
freightInfo.unitPrice ??
|
||
freightInfo.freightItems?.[0]?.unitPrice ??
|
||
'';
|
||
const otherFeeTotal =
|
||
item.otherFeeTotal ??
|
||
plan.otherFeeTotal ??
|
||
freightInfo.otherFeeTotal ??
|
||
freightInfo.otherFreightAmount ??
|
||
'';
|
||
const quantity = Number(
|
||
item.quantity ||
|
||
item.cargoQuantity ||
|
||
item.goodsQuantity ||
|
||
itemGoods.quantity ||
|
||
itemGoods.cargoQuantity ||
|
||
itemGoods.goodsQuantity ||
|
||
0
|
||
);
|
||
const calculatedFreight = unitPrice !== '' && quantity ? Number(unitPrice) * quantity : '';
|
||
const explicitFreight = [
|
||
item.freight,
|
||
item.freightAmount,
|
||
item.transportFee,
|
||
freightInfo.freightAmount,
|
||
freightInfo.transportFee,
|
||
plan.freight,
|
||
plan.freightAmount,
|
||
].find(value => value !== undefined && value !== null && String(value).trim() !== '');
|
||
const freight = explicitFreight ?? calculatedFreight;
|
||
const freightTotal =
|
||
freight !== '' || otherFeeTotal !== ''
|
||
? Number(freight || 0) + Number(otherFeeTotal || 0)
|
||
: item.freightTotal ??
|
||
item.totalFreight ??
|
||
plan.freightTotal ??
|
||
plan.totalFreight ??
|
||
freightInfo.totalFreightAmount ??
|
||
freightInfo.freightTotal ??
|
||
'';
|
||
return { unitPrice, freight, freightAmount: freight, otherFeeTotal, freightTotal };
|
||
},
|
||
getDispatchWaybillFallback(plan = {}, item = {}, index = 0) {
|
||
const sources = [
|
||
plan.waybillList,
|
||
plan.waybillRows,
|
||
plan.waybills,
|
||
plan.transportPlanWaybills,
|
||
plan.transportPlanWaybillList,
|
||
plan.dispatchRecords,
|
||
plan.dispatchRecordList,
|
||
plan.dispatchList,
|
||
];
|
||
const rows = sources.reduce((result, source) => {
|
||
const parsed = this.parseTransportPlanWaybillSource(source);
|
||
return result.length ? result : parsed;
|
||
}, []);
|
||
if (!rows.length) return {};
|
||
const itemWaybillId = item.waybillId || item.waybillID || item.waybillNo;
|
||
const matched = itemWaybillId
|
||
? rows.find(
|
||
row =>
|
||
String(row.id || row.waybillId || row.waybillID || row.waybillNo || '') ===
|
||
String(itemWaybillId)
|
||
)
|
||
: null;
|
||
const fallback = matched || rows[index] || rows[0] || {};
|
||
return {
|
||
createUserName:
|
||
fallback.createUserName ||
|
||
fallback.creatorName ||
|
||
fallback.createdUserName ||
|
||
fallback.createdByName ||
|
||
'',
|
||
createTime: fallback.createTime || fallback.createdAt || '',
|
||
};
|
||
},
|
||
formatDispatchCargoInfo(plan = {}, item = {}) {
|
||
const goodsRows = this.dispatchGoodsRows(plan.goodsJson);
|
||
const itemGoodsRows = this.dispatchGoodsRows(item.goodsJson);
|
||
const sourceRows = goodsRows.length
|
||
? goodsRows
|
||
: itemGoodsRows.length
|
||
? itemGoodsRows
|
||
: [item];
|
||
const groups = sourceRows.reduce((result, goods = {}) => {
|
||
const cargoType =
|
||
goods.secondCargoTypeName ||
|
||
goods.secondCargoType ||
|
||
goods.cargoType ||
|
||
goods.goodsType ||
|
||
goods.type ||
|
||
plan.cargoType ||
|
||
'货物';
|
||
const unit = this.getDispatchQuantityUnit(goods);
|
||
if (!result[unit]) {
|
||
result[unit] = {
|
||
cargoType,
|
||
count: 0,
|
||
quantity: 0,
|
||
unit,
|
||
};
|
||
}
|
||
result[unit].count += 1;
|
||
result[unit].quantity += this.parseDispatchQuantity(
|
||
goods.quantity || goods.cargoQuantity || goods.goodsQuantity
|
||
);
|
||
return result;
|
||
}, {});
|
||
|
||
const cargoInfo = Object.values(groups)
|
||
.map(
|
||
group =>
|
||
`${group.cargoType}等${group.count}种货物 | ${this.formatDispatchQuantity(
|
||
group.quantity
|
||
)}${group.unit ? ` ${group.unit}` : ''}`
|
||
)
|
||
.join('; ');
|
||
return cargoInfo || plan.goodsInfo || '';
|
||
},
|
||
dispatchGoodsRows(value) {
|
||
const rows = this.parseJsonArray(value);
|
||
if (rows.length) return rows;
|
||
const item = this.parseJsonObject(value);
|
||
return Object.keys(item).length ? [item] : [];
|
||
},
|
||
getDispatchConfiguredGoodsOptions() {
|
||
const sourceRows = this.dispatchPlanGoodsRows.length
|
||
? this.dispatchPlanGoodsRows
|
||
: this.dispatchRow.cargoName || this.dispatchRow.cargoType
|
||
? [this.dispatchRow]
|
||
: [];
|
||
return sourceRows
|
||
.map((item, index) => {
|
||
const cargoTypePath = this.normalizeBillingCargoTypePath(item.cargoTypePath);
|
||
const cargoTypeNode =
|
||
(cargoTypePath.length && this.findBillingCargoTypeByPath(cargoTypePath)) ||
|
||
this.findBillingCargoTypeByCodeOrName({
|
||
cargoTypeCode:
|
||
item.secondCargoTypeCode || item.cargoTypeCode || item.goodsTypeCode || '',
|
||
cargoType:
|
||
item.secondCargoTypeName || item.cargoType || item.goodsType || item.goodsTypeName,
|
||
});
|
||
const path = cargoTypePath.length ? cargoTypePath : cargoTypeNode?.path || [];
|
||
const labels = path.length ? this.getBillingCargoTypePathLabels(path) : [];
|
||
const cargoTypeLabel =
|
||
item.cargoType ||
|
||
item.secondCargoTypeName ||
|
||
item.goodsType ||
|
||
item.goodsTypeName ||
|
||
labels.at(-1) ||
|
||
'';
|
||
const cargoTypeCode =
|
||
item.secondCargoTypeCode ||
|
||
item.cargoTypeCode ||
|
||
item.goodsTypeCode ||
|
||
cargoTypeNode?.cargoCode ||
|
||
cargoTypeNode?.code ||
|
||
'';
|
||
const cargoName = item.cargoName || item.goodsName || item.name || '';
|
||
return {
|
||
...item,
|
||
_dispatchGoodsIndex: index,
|
||
cargoTypePath: path,
|
||
cargoTypeLabel,
|
||
cargoTypeCode,
|
||
cargoTypeValue: cargoTypeCode || (path.length ? path.join('/') : cargoTypeLabel),
|
||
cargoName,
|
||
};
|
||
})
|
||
.filter(item => item.cargoTypeLabel || item.cargoName);
|
||
},
|
||
getDispatchConfiguredCargoTypeOptions() {
|
||
const options = new Map();
|
||
this.getDispatchConfiguredGoodsOptions().forEach(item => {
|
||
if (!item.cargoTypeLabel || options.has(item.cargoTypeValue)) return;
|
||
options.set(item.cargoTypeValue, {
|
||
label: item.cargoTypeLabel,
|
||
value: item.cargoTypeLabel,
|
||
cargoTypeCode: item.cargoTypeCode,
|
||
cargoTypePath: item.cargoTypePath,
|
||
});
|
||
});
|
||
return Array.from(options.values());
|
||
},
|
||
getDispatchConfiguredCargoNameOptions(row = {}) {
|
||
const goodsOptions = this.getDispatchConfiguredGoodsOptions();
|
||
const rowType = String(row.cargoType || '').trim();
|
||
const rowTypePath = this.normalizeBillingCargoTypePath(row.cargoTypePath);
|
||
const rowTypeValue = rowType || row.cargoTypeCode || rowTypePath.join('/');
|
||
const filtered = rowTypeValue
|
||
? goodsOptions.filter(
|
||
item =>
|
||
item.cargoTypeLabel === rowType ||
|
||
item.cargoTypeValue === rowTypeValue ||
|
||
item.cargoTypeCode === row.cargoTypeCode ||
|
||
item.cargoTypePath.join('/') === rowTypePath.join('/')
|
||
)
|
||
: goodsOptions;
|
||
const source = filtered.length ? filtered : goodsOptions;
|
||
const options = new Map();
|
||
source.forEach(item => {
|
||
if (!item.cargoName || options.has(item.cargoName)) return;
|
||
options.set(item.cargoName, {
|
||
label: item.cargoName,
|
||
value: item.cargoName,
|
||
cargoTypeLabel: item.cargoTypeLabel,
|
||
cargoTypeCode: item.cargoTypeCode,
|
||
cargoTypePath: item.cargoTypePath,
|
||
specification: item.specification || item.spec || '',
|
||
model: item.model || item.modelName || '',
|
||
});
|
||
});
|
||
return Array.from(options.values());
|
||
},
|
||
fetchDispatchConfiguredCargoTypeSuggestions(queryString, callback) {
|
||
const keyword = String(queryString || '').trim().toLowerCase();
|
||
const options = this.getDispatchConfiguredCargoTypeOptions().map(item => ({
|
||
...item,
|
||
value: item.label,
|
||
}));
|
||
callback(
|
||
keyword
|
||
? options.filter(item => String(item.value).toLowerCase().includes(keyword))
|
||
: options
|
||
);
|
||
},
|
||
fetchDispatchConfiguredCargoNameSuggestions(queryString, callback, row = {}) {
|
||
const keyword = String(queryString || '').trim().toLowerCase();
|
||
const options = this.getDispatchConfiguredCargoNameOptions(row).map(item => ({
|
||
...item,
|
||
value: item.label,
|
||
}));
|
||
callback(
|
||
keyword
|
||
? options.filter(item => String(item.value).toLowerCase().includes(keyword))
|
||
: options
|
||
);
|
||
},
|
||
handleDispatchConfiguredCargoTypeSelect(row, item = {}) {
|
||
this.handleDispatchConfiguredCargoTypeChange(row, item.value || item.label || '');
|
||
},
|
||
handleDispatchConfiguredCargoNameSelect(row, item = {}) {
|
||
this.handleDispatchConfiguredCargoNameChange(row, item.value || item.label || '');
|
||
},
|
||
handleDispatchConfiguredCargoTypeChange(row, value) {
|
||
const nextValue = String(value ?? row.cargoType ?? '').trim();
|
||
const option = this.getDispatchConfiguredCargoTypeOptions().find(
|
||
item => String(item.value) === nextValue || item.label === nextValue
|
||
);
|
||
if (option) {
|
||
row.cargoType = option.label || '';
|
||
row.cargoTypeCode = option.cargoTypeCode || '';
|
||
row.cargoTypePath = option.cargoTypePath || [];
|
||
} else {
|
||
row.cargoType = nextValue;
|
||
row.cargoTypeCode = '';
|
||
row.cargoTypePath = [];
|
||
}
|
||
row.cargoName = '';
|
||
row.specification = '';
|
||
row.model = '';
|
||
},
|
||
handleDispatchConfiguredCargoNameChange(row, value) {
|
||
const nextValue = String(value ?? row.cargoName ?? '').trim();
|
||
const option = this.getDispatchConfiguredCargoNameOptions(row).find(
|
||
item => String(item.value) === nextValue || item.label === nextValue
|
||
);
|
||
if (!option) {
|
||
row.cargoName = nextValue;
|
||
return;
|
||
}
|
||
const previousType = String(row.cargoType || '').trim();
|
||
const isKnownType = this.getDispatchConfiguredCargoTypeOptions().some(
|
||
item => item.label === previousType || String(item.value) === previousType
|
||
);
|
||
row.cargoName = option.value || '';
|
||
if (!previousType || isKnownType) {
|
||
row.cargoType = option.cargoTypeLabel || previousType;
|
||
row.cargoTypeCode = option.cargoTypeCode || row.cargoTypeCode || '';
|
||
row.cargoTypePath = option.cargoTypePath || row.cargoTypePath || [];
|
||
}
|
||
row.specification = option.specification || row.specification || '';
|
||
row.model = option.model || row.model || '';
|
||
},
|
||
openDispatchItemDialog(index = -1, row = defaultDispatchRow()) {
|
||
const defaultSummaryItem =
|
||
this.dispatchSummaryItems.find(item => item.remaining > 0) || this.dispatchSummaryItems[0];
|
||
const defaultGoods =
|
||
this.dispatchPlanGoodsRows.find(
|
||
item => this.getDispatchQuantityUnit(item) === defaultSummaryItem?.unit
|
||
) ||
|
||
this.dispatchPlanGoodsRows[0] ||
|
||
{};
|
||
const defaultUnit = defaultSummaryItem?.unit || this.getDispatchQuantityUnit(defaultGoods);
|
||
const baseRow = {
|
||
...defaultDispatchRow(),
|
||
transportType: this.dispatchRow.transportType || '',
|
||
carrierType: this.dispatchRow.carrierType || '承运商',
|
||
carrierName: this.dispatchRow.carrierName || '',
|
||
driverName: this.dispatchRow.driverName || '',
|
||
driverPhone: this.dispatchRow.driverPhone || '',
|
||
vehicleNo: this.dispatchRow.vehicleNo || '',
|
||
cargoName: defaultGoods.cargoName || this.dispatchRow.cargoName || '',
|
||
cargoType: defaultGoods.cargoType || this.dispatchRow.cargoType || '',
|
||
quantity: '',
|
||
quantityUnit: defaultUnit,
|
||
specification: defaultGoods.specification || '',
|
||
model: defaultGoods.model || '',
|
||
packageType: defaultGoods.packageType || '',
|
||
departureName: this.dispatchRow.departureName || '',
|
||
departureAddress: this.dispatchRow.departureAddress || '',
|
||
departureContact: this.dispatchRow.departureContact || '',
|
||
departurePhone: this.dispatchRow.departurePhone || '',
|
||
arrivalName: this.dispatchRow.arrivalName || '',
|
||
arrivalAddress: this.dispatchRow.arrivalAddress || '',
|
||
arrivalContact: this.dispatchRow.arrivalContact || '',
|
||
arrivalPhone: this.dispatchRow.arrivalPhone || '',
|
||
};
|
||
this.dispatchItemIndex = index;
|
||
this.dispatchItemForm = {
|
||
...baseRow,
|
||
...(index >= 0 ? row : {}),
|
||
};
|
||
this.dispatchItemForm.carrierType = this.dispatchItemForm.carrierType || '承运商';
|
||
this.loadDispatchCarrierOptions(this.dispatchRow).then(() => {
|
||
this.applyDispatchSelfCarrierDefaults();
|
||
});
|
||
const goodsRows = this.parseJsonArray(this.dispatchItemForm.goodsJson);
|
||
this.dispatchItemCargoRows = (goodsRows.length ? goodsRows : [this.dispatchItemForm]).map(
|
||
item => this.normalizeTransportCargoRow(item)
|
||
);
|
||
const hasMultiplePlanGoods = this.dispatchPlanGoodsRows.length > 1;
|
||
const shouldAutoFillFullGoods =
|
||
hasMultiplePlanGoods &&
|
||
(index < 0 || !this.hasDispatchVehicleIdentifier(this.dispatchItemForm));
|
||
if (shouldAutoFillFullGoods) {
|
||
const unscheduledGoodsRows = this.getDispatchUnscheduledGoodsRows();
|
||
this.dispatchItemForm.taskEntryMode = 'full';
|
||
this.dispatchItemForm.quantity = '';
|
||
this.dispatchItemCargoRows = unscheduledGoodsRows.length
|
||
? unscheduledGoodsRows
|
||
: this.dispatchPlanGoodsRows.map(goods =>
|
||
this.normalizeTransportCargoRow({ ...goods, quantity: '' })
|
||
);
|
||
}
|
||
if (!this.dispatchItemCargoRows.length) {
|
||
this.dispatchItemCargoRows = [this.normalizeTransportCargoRow()];
|
||
}
|
||
if (this.hasMultipleConfiguredGoods(this.dispatchItemForm, this.dispatchItemCargoRows)) {
|
||
this.dispatchItemForm.taskEntryMode = 'full';
|
||
}
|
||
const freightInfo = this.parseJsonObject(this.dispatchItemForm.freightJson);
|
||
this.dispatchItemForm.freightCurrency = this.dispatchContractCurrency || 'RMB';
|
||
this.loadCurrencyOptions();
|
||
if (this.dispatchItemForm.taskEntryMode === 'full' && freightInfo.freightItems?.length) {
|
||
this.dispatchItemCargoRows = this.dispatchItemCargoRows.map((cargo, itemIndex) => ({
|
||
...cargo,
|
||
unitPrice: cargo.unitPrice || freightInfo.freightItems[itemIndex]?.unitPrice || '',
|
||
priceUnit: cargo.priceUnit || freightInfo.freightItems[itemIndex]?.priceUnit || '元/吨',
|
||
freightAmount:
|
||
cargo.freightAmount || freightInfo.freightItems[itemIndex]?.freightAmount || '',
|
||
}));
|
||
}
|
||
this.dispatchItemAttachmentRows = this.parseJsonArray(
|
||
index >= 0 ? row.attachmentsJson : this.dispatchRow.attachmentsJson
|
||
);
|
||
this.selectedDispatchItemAttachmentRows = [];
|
||
this.dispatchItemBox = true;
|
||
},
|
||
handleDispatchCarrierTypeChange(value) {
|
||
const nextValue = value || '';
|
||
this.dispatchItemForm.carrierName = '';
|
||
this.dispatchItemForm.carrierId = '';
|
||
this.dispatchItemForm.carrierContractId = '';
|
||
if (nextValue === '承运商') {
|
||
this.dispatchItemForm.trailerVehicleNo = '';
|
||
this.dispatchItemForm.escortName = '';
|
||
this.dispatchItemForm.escortPhone = '';
|
||
}
|
||
this.dispatchItemForm.carrierType = nextValue;
|
||
this.loadDispatchCarrierOptions(this.dispatchRow);
|
||
},
|
||
isDispatchCarrierRequired(carrierType) {
|
||
return String(carrierType || '').trim() === '承运商';
|
||
},
|
||
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;
|
||
},
|
||
loadDispatchCarrierOptions(plan = this.dispatchRow) {
|
||
const projectId = plan?.projectId;
|
||
const requestId = ++this.dispatchCarrierRequestId;
|
||
const carrierTypeAtRequest = this.dispatchItemForm.carrierType || '承运商';
|
||
const project = this.projectOptions.find(item => String(item.id) === String(projectId));
|
||
this.dispatchCarrierOptions = this.getProjectCarrierOptions({ ...project, ...plan });
|
||
this.dispatchCarrierContractOptions = [];
|
||
if (!projectId) return Promise.resolve(this.dispatchCarrierOptions);
|
||
this.dispatchCarrierLoading = true;
|
||
const detailPromise = getProjectDetail(projectId)
|
||
.then(res => {
|
||
if (requestId !== this.dispatchCarrierRequestId) return this.dispatchCarrierOptions;
|
||
const detail = res?.data?.data || {};
|
||
this.dispatchCarrierOptions = this.getProjectCarrierOptions({
|
||
...project,
|
||
...plan,
|
||
...detail,
|
||
});
|
||
return this.dispatchCarrierOptions;
|
||
})
|
||
.catch(() => this.dispatchCarrierOptions);
|
||
const contractPromise = getContractList(1, 9999, {
|
||
projectId,
|
||
projectName: plan?.projectName || project?.projectName,
|
||
contractCategory: '承运商合同',
|
||
})
|
||
.then(res => {
|
||
if (requestId !== this.dispatchCarrierRequestId) return [];
|
||
this.dispatchCarrierContractOptions = this.getWaybillCarrierContractOptions(
|
||
extractRecords(res)
|
||
);
|
||
return this.dispatchCarrierContractOptions;
|
||
})
|
||
.catch(() => {
|
||
this.dispatchCarrierContractOptions = [];
|
||
return [];
|
||
});
|
||
return Promise.all([detailPromise, contractPromise])
|
||
.then(([, carrierContracts]) => {
|
||
if (carrierTypeAtRequest !== '自运') {
|
||
this.dispatchCarrierOptions = (carrierContracts || []).map(item => ({
|
||
...item,
|
||
value: item.carrierContractId || item.id,
|
||
}));
|
||
const current = this.dispatchCarrierOptions.find(
|
||
item =>
|
||
String(item.carrierContractId) === String(this.dispatchItemForm.carrierContractId)
|
||
);
|
||
if (current) {
|
||
this.dispatchItemForm.carrierName = current.carrierName;
|
||
} else if (!this.dispatchItemForm.carrierContractId) {
|
||
const matches = this.dispatchCarrierOptions.filter(
|
||
item => String(item.carrierName) === String(this.dispatchItemForm.carrierName || '')
|
||
);
|
||
if (matches.length === 1) {
|
||
this.dispatchItemForm.carrierContractId = matches[0].carrierContractId;
|
||
}
|
||
}
|
||
return this.dispatchCarrierOptions;
|
||
}
|
||
this.applyDispatchSelfCarrierDefaults(carrierContracts);
|
||
return this.dispatchCarrierOptions;
|
||
})
|
||
.finally(() => {
|
||
if (requestId === this.dispatchCarrierRequestId) {
|
||
this.dispatchCarrierLoading = false;
|
||
}
|
||
});
|
||
},
|
||
getWaybillCarrierContractOptions(contracts = []) {
|
||
const counts = contracts.reduce((result, item) => {
|
||
const name = String(
|
||
item.partyB ||
|
||
item.partyBName ||
|
||
item.contractPartyB ||
|
||
item.customerContractPartyB ||
|
||
''
|
||
).trim();
|
||
if (name) result[name] = (result[name] || 0) + 1;
|
||
return result;
|
||
}, {});
|
||
return contracts
|
||
.map(contract => {
|
||
const carrierName = String(
|
||
contract.partyB ||
|
||
contract.partyBName ||
|
||
contract.contractPartyB ||
|
||
contract.customerContractPartyB ||
|
||
''
|
||
).trim();
|
||
if (!carrierName || !contract.id) return null;
|
||
const identifier = contract.contractName || contract.contractNo || contract.id;
|
||
return {
|
||
id: contract.id,
|
||
value: contract.id,
|
||
carrierContractId: contract.id,
|
||
carrierId:
|
||
contract.partyBId ||
|
||
contract.partyBUserId ||
|
||
contract.contractPartyBId ||
|
||
contract.customerContractPartyBId ||
|
||
'',
|
||
carrierName,
|
||
fullName: carrierName,
|
||
label: counts[carrierName] > 1 ? `${carrierName}(${identifier})` : carrierName,
|
||
};
|
||
})
|
||
.filter(Boolean);
|
||
},
|
||
applyDispatchSelfCarrierDefaults(carrierContracts = this.dispatchCarrierContractOptions) {
|
||
if (this.dispatchItemForm.carrierType !== '自运') return;
|
||
const options = carrierContracts || [];
|
||
if (options.length) {
|
||
this.dispatchCarrierOptions = options.map(item => ({ ...item, value: item.carrierName }));
|
||
const current = this.dispatchCarrierOptions.find(
|
||
item => String(item.carrierName) === String(this.dispatchItemForm.carrierName || '')
|
||
);
|
||
if (!current) {
|
||
this.dispatchItemForm.carrierName = this.dispatchCarrierOptions[0].carrierName;
|
||
this.dispatchItemForm.carrierId = '';
|
||
}
|
||
return;
|
||
}
|
||
const customerContract = this.contractOptions.find(
|
||
item => String(item.id) === String(this.dispatchRow?.contractId)
|
||
);
|
||
const carrierName = String(customerContract?.partyB || '').trim();
|
||
if (!carrierName) return;
|
||
if (!this.dispatchCarrierOptions.some(item => String(item.value) === carrierName)) {
|
||
this.dispatchCarrierOptions.push({
|
||
id: `customer-contract-${carrierName}`,
|
||
value: carrierName,
|
||
carrierName,
|
||
fullName: carrierName,
|
||
label: carrierName,
|
||
});
|
||
}
|
||
this.dispatchItemForm.carrierName = carrierName;
|
||
this.dispatchItemForm.carrierId = '';
|
||
},
|
||
handleDispatchCarrierChange(value) {
|
||
const carrier = this.dispatchCarrierOptions.find(item => {
|
||
const optionValue = this.dispatchIsCarrierMode ? item.carrierContractId : item.value;
|
||
return String(optionValue || '') === String(value || '');
|
||
});
|
||
if (this.dispatchItemForm.carrierType !== '自运') {
|
||
this.dispatchItemForm.carrierContractId = carrier?.carrierContractId || '';
|
||
this.dispatchItemForm.carrierId = carrier?.carrierId || '';
|
||
this.dispatchItemForm.carrierName = carrier?.carrierName || '';
|
||
return;
|
||
}
|
||
this.dispatchItemForm.carrierContractId = '';
|
||
this.dispatchItemForm.carrierId =
|
||
this.dispatchItemForm.carrierType === '自运' ? '' : carrier?.id || '';
|
||
this.dispatchItemForm.carrierName = carrier?.carrierName || value || '';
|
||
},
|
||
handleDispatchCargoQuantityInput(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];
|
||
},
|
||
handleDispatchCargoNumberInput(row, prop, value) {
|
||
this.handleFreightNumberInput(row, prop, value);
|
||
},
|
||
handleDispatchNumberInput(prop, value) {
|
||
const text = String(value || '').replace(/[^\d.]/g, '');
|
||
const parts = text.split('.');
|
||
this.dispatchItemForm[prop] =
|
||
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0];
|
||
},
|
||
addDispatchCargoRow(index = -1) {
|
||
const nextRow = this.normalizeTransportCargoRow({ priceUnit: '元/吨' });
|
||
if (index > -1) this.dispatchItemCargoRows.splice(index + 1, 0, nextRow);
|
||
else this.dispatchItemCargoRows.push(nextRow);
|
||
},
|
||
removeDispatchCargoRow(index) {
|
||
this.dispatchItemCargoRows.splice(index, 1);
|
||
if (!this.dispatchItemCargoRows.length) {
|
||
this.dispatchItemCargoRows.push(this.normalizeTransportCargoRow());
|
||
}
|
||
},
|
||
dispatchCargoFreightAmount(cargo = {}) {
|
||
if (cargo.freightAmount !== undefined && cargo.freightAmount !== null) {
|
||
const enteredAmount = String(cargo.freightAmount).trim();
|
||
if (enteredAmount !== '' && Number.isFinite(Number(enteredAmount))) {
|
||
const amount = Number(enteredAmount);
|
||
return Number.isInteger(amount) ? String(amount) : String(Number(amount.toFixed(2)));
|
||
}
|
||
}
|
||
const quantity = Number(cargo.quantity || 0);
|
||
const unitPrice = Number(cargo.unitPrice || 0);
|
||
if (!quantity || !unitPrice) return '';
|
||
const amount = quantity * unitPrice;
|
||
return Number.isInteger(amount) ? String(amount) : String(Number(amount.toFixed(2)));
|
||
},
|
||
formatDispatchAmount(value) {
|
||
const amount = Number(value || 0);
|
||
if (!Number.isFinite(amount)) return '';
|
||
return Number(amount.toFixed(2)).toString();
|
||
},
|
||
dispatchFreightGroupQuantity(group = {}) {
|
||
const quantity = (group.rows || []).reduce(
|
||
(sum, cargo) => sum + Number(cargo.quantity || 0),
|
||
0
|
||
);
|
||
return this.formatDispatchQuantity(quantity);
|
||
},
|
||
dispatchFreightGroupUnitPrice(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];
|
||
},
|
||
dispatchFreightGroupPriceUnit(group = {}) {
|
||
return group.rows?.[0]?.priceUnit || (group.quantityUnit ? `元/${group.quantityUnit}` : '');
|
||
},
|
||
dispatchFreightGroupAmount(group = {}) {
|
||
const amount = (group.rows || []).reduce(
|
||
(sum, cargo) => sum + Number(this.dispatchCargoFreightAmount(cargo) || 0),
|
||
0
|
||
);
|
||
return Number.isInteger(amount) ? String(amount) : String(Number(amount.toFixed(2)));
|
||
},
|
||
handleDispatchFreightGroupNumberInput(group, value) {
|
||
this.handleFreightNumberInput(group.rows?.[0] || {}, 'unitPrice', value);
|
||
const unitPrice = group.rows?.[0]?.unitPrice || '';
|
||
(group.rows || []).forEach(cargo => {
|
||
cargo.unitPrice = unitPrice;
|
||
cargo.freightAmount = '';
|
||
});
|
||
},
|
||
handleDispatchFreightGroupPriceUnitChange(group, value) {
|
||
(group.rows || []).forEach(cargo => {
|
||
cargo.priceUnit = value || '';
|
||
});
|
||
},
|
||
handleDispatchFreightGroupAmountInput(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.formatDispatchAmount(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.formatDispatchAmount(amount);
|
||
});
|
||
},
|
||
dispatchItemRemainingQuantity(row = {}) {
|
||
const unit = this.getDispatchQuantityUnit(row);
|
||
const hasCargoIdentity = Boolean(
|
||
String(row.cargoName || row.goodsName || row.name || '').trim() ||
|
||
String(row.cargoType || row.secondCargoTypeName || row.goodsType || row.type || '').trim()
|
||
);
|
||
const planGoodsRows = this.dispatchPlanGoodsRows;
|
||
const cargoKey = this.getDispatchCargoIdentity(row);
|
||
|
||
// 完整录入时按货物身份计算,不能把同单位的其他货物数量合并到当前行。
|
||
if (planGoodsRows.length && hasCargoIdentity) {
|
||
const matchedPlanRows = planGoodsRows.filter(
|
||
goods => this.getDispatchCargoIdentity(goods) === cargoKey
|
||
);
|
||
if (!matchedPlanRows.length) return 0;
|
||
const total = matchedPlanRows.reduce(
|
||
(sum, goods) =>
|
||
sum +
|
||
this.parseDispatchQuantity(
|
||
goods.quantity || goods.cargoQuantity || goods.goodsQuantity
|
||
),
|
||
0
|
||
);
|
||
const assigned = this.dispatchRows.reduce((sum, dispatchRow, index) => {
|
||
if (index === this.dispatchItemIndex || !this.hasDispatchVehicleIdentifier(dispatchRow)) {
|
||
return sum;
|
||
}
|
||
return (
|
||
sum +
|
||
this.getDispatchGoodsSourceRows(dispatchRow).reduce((goodsSum, goods) => {
|
||
if (this.getDispatchCargoIdentity(goods) !== cargoKey) return goodsSum;
|
||
return (
|
||
goodsSum +
|
||
this.parseDispatchQuantity(
|
||
goods.quantity || goods.cargoQuantity || goods.goodsQuantity
|
||
)
|
||
);
|
||
}, 0)
|
||
);
|
||
}, 0);
|
||
return Math.max(total - assigned, 0);
|
||
}
|
||
|
||
// 兼容没有货物明细的旧数据,以及尚未选择货物的空白行。
|
||
const remaining = this.getDispatchRemainingQuantity(unit);
|
||
const editingRow = this.dispatchRows[this.dispatchItemIndex];
|
||
if (!editingRow || !this.hasDispatchVehicleIdentifier(editingRow)) return remaining;
|
||
const editingGoodsRows = this.dispatchGoodsRows(editingRow.goodsJson);
|
||
const goodsRows = editingGoodsRows.length ? editingGoodsRows : [editingRow];
|
||
return (
|
||
remaining +
|
||
goodsRows.reduce((total, goods) => {
|
||
if (this.getDispatchQuantityUnit(goods) !== unit) return total;
|
||
return (
|
||
total +
|
||
this.parseDispatchQuantity(goods.quantity || goods.cargoQuantity || goods.goodsQuantity)
|
||
);
|
||
}, 0)
|
||
);
|
||
},
|
||
pruneDispatchPendingRows(rows = []) {
|
||
const planQuantities = new Map();
|
||
this.dispatchPlanGoodsRows.forEach(goods => {
|
||
const key = this.getDispatchCargoIdentity(goods);
|
||
const quantity = this.parseDispatchQuantity(
|
||
goods.quantity || goods.cargoQuantity || goods.goodsQuantity
|
||
);
|
||
planQuantities.set(key, (planQuantities.get(key) || 0) + quantity);
|
||
});
|
||
const assignedQuantities = new Map();
|
||
rows.forEach(row => {
|
||
if (!this.hasDispatchVehicleIdentifier(row)) return;
|
||
this.getDispatchGoodsSourceRows(row).forEach(goods => {
|
||
const key = this.getDispatchCargoIdentity(goods);
|
||
assignedQuantities.set(
|
||
key,
|
||
(assignedQuantities.get(key) || 0) +
|
||
this.parseDispatchQuantity(
|
||
goods.quantity || goods.cargoQuantity || goods.goodsQuantity
|
||
)
|
||
);
|
||
});
|
||
});
|
||
const pendingRemaining = new Map(
|
||
Array.from(planQuantities.entries()).map(([key, total]) => [
|
||
key,
|
||
Math.max(total - (assignedQuantities.get(key) || 0), 0),
|
||
])
|
||
);
|
||
return rows.reduce((result, row) => {
|
||
if (this.hasDispatchVehicleIdentifier(row)) {
|
||
result.push(row);
|
||
return result;
|
||
}
|
||
const pendingGoodsRows = this.getDispatchGoodsSourceRows(row)
|
||
.map(goods => {
|
||
const quantity = this.parseDispatchQuantity(
|
||
goods.quantity || goods.cargoQuantity || goods.goodsQuantity
|
||
);
|
||
if (quantity <= 0) return goods;
|
||
const key = this.getDispatchCargoIdentity(goods);
|
||
const available = pendingRemaining.has(key) ? pendingRemaining.get(key) : quantity;
|
||
const remainingQuantity = Math.min(quantity, Math.max(available, 0));
|
||
if (!remainingQuantity) return null;
|
||
if (pendingRemaining.has(key)) {
|
||
pendingRemaining.set(key, Math.max(available - remainingQuantity, 0));
|
||
}
|
||
return {
|
||
...goods,
|
||
quantity: this.formatDispatchQuantity(remainingQuantity),
|
||
};
|
||
})
|
||
.filter(Boolean);
|
||
if (!pendingGoodsRows.length) return result;
|
||
const nextRow = {
|
||
...row,
|
||
goodsJson: JSON.stringify(pendingGoodsRows),
|
||
cargoType: pendingGoodsRows[0].cargoType || row.cargoType || '',
|
||
cargoName: pendingGoodsRows[0].cargoName || row.cargoName || '',
|
||
quantity: pendingGoodsRows[0].quantity || row.quantity || '',
|
||
quantityUnit: pendingGoodsRows[0].quantityUnit || row.quantityUnit || '吨',
|
||
};
|
||
nextRow.cargoInfo = this.formatDispatchCargoInfo({}, nextRow);
|
||
result.push(nextRow);
|
||
return result;
|
||
}, []);
|
||
},
|
||
validateDispatchItemData(row = {}, goodsRows = [], rowIndex = -1) {
|
||
const detailPrefix = rowIndex >= 0 ? `第${rowIndex + 1}条调度明细` : '';
|
||
const warn = message => {
|
||
this.$message.warning(detailPrefix ? `${detailPrefix}${message}` : message);
|
||
return false;
|
||
};
|
||
const requiredMessage = label =>
|
||
warn(detailPrefix ? `的${label}不能为空` : `请选择或输入${label}`);
|
||
const requiredFields = [
|
||
['transportType', '运输方式'],
|
||
['departureAddress', '发货地址'],
|
||
['arrivalAddress', '收货地址'],
|
||
['carrierType', '承运类型'],
|
||
[
|
||
'vehicleNo',
|
||
['water', 'rail', 'air'].includes(this.resolveTransportMode(row.transportType))
|
||
? '船/航/班列号'
|
||
: '车牌号',
|
||
],
|
||
];
|
||
if (this.isDispatchCarrierRequired(row.carrierType)) {
|
||
requiredFields.push(['carrierName', '承运商']);
|
||
}
|
||
if (row.carrierType === '承运商') {
|
||
requiredFields.push(['carrierContractId', '承运商合同']);
|
||
}
|
||
const fullEntry = row.taskEntryMode === 'full';
|
||
const roadTransport = this.resolveTransportMode(row.transportType) === 'road';
|
||
if (fullEntry) {
|
||
if (roadTransport && row.carrierType === '自运') {
|
||
requiredFields.push(['driverName', '司机']);
|
||
requiredFields.push(['driverPhone', '手机号']);
|
||
}
|
||
} else if (row.carrierType && row.carrierType !== '承运商') {
|
||
requiredFields.push(['driverName', '司机']);
|
||
requiredFields.push(['driverPhone', '司机手机号']);
|
||
}
|
||
const emptyField = requiredFields.find(([prop]) => this.isBillingFieldEmpty(row[prop]));
|
||
if (emptyField) return requiredMessage(emptyField[1]);
|
||
|
||
if (!goodsRows.length) {
|
||
return warn(detailPrefix ? '的货物信息不能为空' : '请至少添加一条货物信息');
|
||
}
|
||
for (const [goodsIndex, goods] of goodsRows.entries()) {
|
||
const goodsPrefix = fullEntry ? `第${goodsIndex + 1}行` : '';
|
||
const emptyGoodsField = [
|
||
['cargoType', '货物类型'],
|
||
['cargoName', '货物名称'],
|
||
['quantity', '本次数量'],
|
||
['quantityUnit', '数量单位'],
|
||
].find(([prop]) => this.isBillingFieldEmpty(goods[prop]));
|
||
if (emptyGoodsField) {
|
||
return warn(
|
||
detailPrefix
|
||
? `的${goodsPrefix}${emptyGoodsField[1]}不能为空`
|
||
: `请选择或输入${goodsPrefix}${emptyGoodsField[1]}`
|
||
);
|
||
}
|
||
const quantity = Number(String(goods.quantity).replace(/,/g, ''));
|
||
if (!Number.isFinite(quantity) || quantity <= 0) {
|
||
return warn(
|
||
detailPrefix ? `的${goodsPrefix}本次数量必须大于0` : `${goodsPrefix}本次数量必须大于0`
|
||
);
|
||
}
|
||
if (!this.isBillingFieldEmpty(goods.unitPrice) && Number(goods.unitPrice) < 0) {
|
||
return warn(
|
||
detailPrefix ? `的${goodsPrefix}单价不能小于0` : `${goodsPrefix}单价不能小于0`
|
||
);
|
||
}
|
||
if (!this.isBillingFieldEmpty(goods.freightAmount) && Number(goods.freightAmount) < 0) {
|
||
return warn(
|
||
detailPrefix ? `的${goodsPrefix}运费不能小于0` : `${goodsPrefix}运费不能小于0`
|
||
);
|
||
}
|
||
}
|
||
|
||
const invalidPhoneField = [
|
||
[row.departurePhone, '发货联系方式'],
|
||
[row.arrivalPhone, '收货联系方式'],
|
||
[row.driverPhone, roadTransport ? '手机号' : '联系电话'],
|
||
[row.escortPhone, '押运人手机号'],
|
||
].find(([value]) => String(value || '').trim() && !isMobile(value));
|
||
if (invalidPhoneField) {
|
||
return warn(
|
||
detailPrefix ? `的${invalidPhoneField[1]}格式不正确` : `${invalidPhoneField[1]}格式不正确`
|
||
);
|
||
}
|
||
const lengthFields = roadTransport
|
||
? [
|
||
['driverName', '司机', 20],
|
||
['driverPhone', '手机号', 20],
|
||
['vehicleNo', '车牌号', 30],
|
||
['trailerVehicleNo', '挂车车牌号', 30],
|
||
['escortName', '押运人', 20],
|
||
['escortPhone', '押运人手机号', 20],
|
||
]
|
||
: [
|
||
['vehicleNo', '船/航/班列号', 30],
|
||
['captainName', '船长', 20],
|
||
['cabinNo', '舱位', 30],
|
||
['containerNo', '箱号', 30],
|
||
];
|
||
const invalidLengthField = lengthFields.find(
|
||
([prop, , max]) => String(row[prop] || '').length > max
|
||
);
|
||
if (invalidLengthField) {
|
||
return warn(
|
||
detailPrefix
|
||
? `的${invalidLengthField[1]}不能超过${invalidLengthField[2]}个字符`
|
||
: `${invalidLengthField[1]}不能超过${invalidLengthField[2]}个字符`
|
||
);
|
||
}
|
||
if (row.mileage && (!/^\d{1,10}$/.test(String(row.mileage)) || Number(row.mileage) <= 0)) {
|
||
return warn(
|
||
detailPrefix ? '的里程必须为不超过10位的正整数' : '里程必须为不超过10位的正整数'
|
||
);
|
||
}
|
||
if (!this.isBillingFieldEmpty(row.otherFeeTotal) && Number(row.otherFeeTotal) < 0) {
|
||
return warn(detailPrefix ? '的其他费用合计不能小于0' : '其他费用合计不能小于0');
|
||
}
|
||
if (
|
||
row.estimatedStartTime &&
|
||
row.estimatedEndTime &&
|
||
row.estimatedEndTime < row.estimatedStartTime
|
||
) {
|
||
return warn(
|
||
detailPrefix ? '的预计完成日期不能早于预计发货日期' : '预计完成日期不能早于预计发货日期'
|
||
);
|
||
}
|
||
if (String(row.remark || row.taskRemark || '').length > 200) {
|
||
return warn(detailPrefix ? '的备注不能超过200个字符' : '备注不能超过200个字符');
|
||
}
|
||
return true;
|
||
},
|
||
validateDispatchItemForm() {
|
||
const goodsRows =
|
||
this.dispatchItemForm.taskEntryMode === 'full'
|
||
? this.dispatchItemCargoRows
|
||
: [this.dispatchItemForm];
|
||
if (!this.validateDispatchItemData(this.dispatchItemForm, goodsRows)) return false;
|
||
const quantityGroups = new Map();
|
||
goodsRows.forEach(goods => {
|
||
const unit = this.getDispatchQuantityUnit(goods);
|
||
const hasCargoIdentity = Boolean(
|
||
String(goods.cargoName || goods.goodsName || goods.name || '').trim() ||
|
||
String(
|
||
goods.cargoType || goods.secondCargoTypeName || goods.goodsType || goods.type || ''
|
||
).trim()
|
||
);
|
||
const key = hasCargoIdentity
|
||
? `cargo:${this.getDispatchCargoIdentity(goods)}`
|
||
: `unit:${unit}`;
|
||
const group = quantityGroups.get(key) || { row: goods, quantity: 0 };
|
||
group.quantity += this.parseDispatchQuantity(goods.quantity);
|
||
quantityGroups.set(key, group);
|
||
});
|
||
for (const { row: goods, quantity } of quantityGroups.values()) {
|
||
if (this.isDispatchPlanQuantityUnlimited(goods)) continue;
|
||
const unit = this.getDispatchQuantityUnit(goods);
|
||
const remaining = this.dispatchItemRemainingQuantity(goods);
|
||
if (quantity - remaining > 1e-8) {
|
||
this.$message.warning(
|
||
`本次调度${unit}合计不能超过剩余数量${this.formatDispatchQuantity(remaining)}${unit}`
|
||
);
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
},
|
||
saveDispatchItem() {
|
||
if (!this.validateDispatchItemForm()) return;
|
||
const goodsRows =
|
||
this.dispatchItemForm.taskEntryMode === 'full'
|
||
? this.dispatchItemCargoRows.map(row => this.normalizeTransportCargoRow(row))
|
||
: [
|
||
this.normalizeTransportCargoRow({
|
||
...this.dispatchItemForm,
|
||
remark: this.dispatchItemForm.remark || '',
|
||
}),
|
||
];
|
||
const firstGoods = goodsRows[0] || {};
|
||
const nextRow = {
|
||
...this.dispatchItemForm,
|
||
goodsJson: JSON.stringify(goodsRows),
|
||
cargoType: firstGoods.cargoType || this.dispatchItemForm.cargoType || '',
|
||
cargoTypeCode: firstGoods.cargoTypeCode || this.dispatchItemForm.cargoTypeCode || '',
|
||
cargoTypePath: firstGoods.cargoTypePath || this.dispatchItemForm.cargoTypePath || [],
|
||
cargoName: firstGoods.cargoName || this.dispatchItemForm.cargoName || '',
|
||
quantity: firstGoods.quantity || this.dispatchItemForm.quantity || '',
|
||
quantityUnit: firstGoods.quantityUnit || this.dispatchItemForm.quantityUnit || '',
|
||
unitPrice: firstGoods.unitPrice || '',
|
||
priceUnit: firstGoods.priceUnit || '',
|
||
freightTotal: this.dispatchItemFreightTotal,
|
||
freightJson: JSON.stringify({
|
||
currency: this.dispatchContractCurrency || this.dispatchItemForm.freightCurrency || 'RMB',
|
||
totalFreightAmount: this.dispatchItemFreightSubtotal,
|
||
otherFreightAmount: this.dispatchItemForm.otherFeeTotal || '',
|
||
freightItems: goodsRows.map((cargo, index) => ({
|
||
cargoIndex: index,
|
||
cargoName: cargo.cargoName || '',
|
||
cargoType: cargo.cargoType || '',
|
||
unitPrice: cargo.unitPrice || '',
|
||
priceUnit: cargo.priceUnit || '',
|
||
quantity: cargo.quantity || '',
|
||
quantityUnit: cargo.quantityUnit || '',
|
||
freightAmount: this.dispatchCargoFreightAmount(cargo),
|
||
})),
|
||
}),
|
||
attachmentsJson: JSON.stringify(this.dispatchItemAttachmentRows),
|
||
};
|
||
nextRow.cargoInfo = this.formatDispatchCargoInfo({}, nextRow);
|
||
nextRow.freight = this.dispatchItemFreightSubtotal;
|
||
nextRow.otherFeeTotal = this.dispatchItemForm.otherFeeTotal || '';
|
||
const quantityUnit = this.getDispatchQuantityUnit(nextRow);
|
||
const otherDispatchedQuantity = this.dispatchRows.reduce((total, row, index) => {
|
||
if (
|
||
index === this.dispatchItemIndex ||
|
||
this.getDispatchQuantityUnit(row) !== quantityUnit ||
|
||
!this.hasDispatchVehicleIdentifier(row)
|
||
) {
|
||
return total;
|
||
}
|
||
return total + this.parseDispatchQuantity(row.quantity);
|
||
}, 0);
|
||
const currentQuantity = this.parseDispatchQuantity(nextRow.quantity);
|
||
const totalQuantity = this.getDispatchSummaryItem(quantityUnit)?.total || 0;
|
||
const persistedDispatchedQuantity = this.dispatchRows.length
|
||
? 0
|
||
: this.parseJsonArray(this.dispatchRow.dispatchRows).reduce((total, row) => {
|
||
if (
|
||
this.getDispatchQuantityUnit(row) !== quantityUnit ||
|
||
!this.hasDispatchVehicleIdentifier(row)
|
||
) {
|
||
return total;
|
||
}
|
||
return total + this.parseDispatchQuantity(row.quantity);
|
||
}, 0);
|
||
const dispatchedQuantity =
|
||
persistedDispatchedQuantity + otherDispatchedQuantity + currentQuantity;
|
||
if (totalQuantity > 0 && dispatchedQuantity - totalQuantity > 1e-8) {
|
||
this.$message.warning(
|
||
`已调度${quantityUnit}合计不能超过总数量${this.formatDispatchQuantity(
|
||
totalQuantity
|
||
)}${quantityUnit}`
|
||
);
|
||
return;
|
||
}
|
||
if (this.dispatchItemIndex >= 0) {
|
||
this.dispatchRows.splice(this.dispatchItemIndex, 1, nextRow);
|
||
} else {
|
||
this.dispatchRows.push(nextRow);
|
||
}
|
||
this.dispatchRows = this.pruneDispatchPendingRows(this.dispatchRows);
|
||
this.dispatchItemBox = false;
|
||
this.dispatchItemIndex = -1;
|
||
this.dispatchItemForm = defaultDispatchRow();
|
||
this.dispatchItemCargoRows = [];
|
||
this.dispatchItemAttachmentRows = [];
|
||
this.selectedDispatchItemAttachmentRows = [];
|
||
},
|
||
resetDispatchItemDialog() {
|
||
this.dispatchItemIndex = -1;
|
||
this.dispatchItemForm = defaultDispatchRow();
|
||
this.dispatchItemCargoRows = [];
|
||
this.dispatchItemAttachmentRows = [];
|
||
this.selectedDispatchItemAttachmentRows = [];
|
||
},
|
||
removeDispatchRow(index) {
|
||
this.$confirm('确定删除该调度明细?', '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
}).then(() => {
|
||
this.dispatchRows.splice(index, 1);
|
||
});
|
||
},
|
||
handleDispatchAddRow() {
|
||
if (!this.dispatchHasAddableQuantity) {
|
||
this.$message.warning('待调度列表货物总量已达到计划总量,不能新增调度明细');
|
||
return;
|
||
}
|
||
this.openDispatchItemDialog();
|
||
},
|
||
handleDispatchEditRow(row, index) {
|
||
this.openDispatchItemDialog(index, row);
|
||
},
|
||
buildDispatchPayload(mode) {
|
||
const rows = this.dispatchRows.map(
|
||
({ _key, cargoInfo, dispatchUserName, dispatchTime, ...rest }) => rest
|
||
);
|
||
return {
|
||
id: this.dispatchRow.id,
|
||
mode,
|
||
waybills: rows.map(row => ({
|
||
...row,
|
||
taskEntryMode: row.taskEntryMode || 'simple',
|
||
taskRemark: row.remark || '',
|
||
estimatedStartTime: this.formatDispatchDate(row.estimatedStartTime),
|
||
estimatedEndTime: this.formatDispatchDate(row.estimatedEndTime),
|
||
goodsJson:
|
||
row.goodsJson ||
|
||
JSON.stringify([
|
||
{
|
||
cargoName: row.cargoName,
|
||
cargoType: row.cargoType,
|
||
quantity: row.quantity,
|
||
quantityUnit: row.quantityUnit,
|
||
specification: row.specification,
|
||
model: row.model,
|
||
packageType: row.packageType,
|
||
},
|
||
]),
|
||
})),
|
||
};
|
||
},
|
||
formatDispatchDate(value) {
|
||
if (!value) return '';
|
||
const date = this.$dayjs(value);
|
||
return date.isValid() ? date.format('YYYY-MM-DD') : value;
|
||
},
|
||
validateDispatchRows(mode) {
|
||
if (!this.dispatchRows.length) {
|
||
this.$message.warning('请先添加调度明细');
|
||
return false;
|
||
}
|
||
for (const [index, row] of this.dispatchRows.entries()) {
|
||
const goodsRows = row.taskEntryMode === 'full' ? this.parseJsonArray(row.goodsJson) : [row];
|
||
if (mode === 'draft') {
|
||
if (row.taskEntryMode === 'full' && !goodsRows.length) {
|
||
this.$message.warning(`第${index + 1}条调度明细的货物信息不能为空`);
|
||
return false;
|
||
}
|
||
const invalidPhoneField = [
|
||
[row.departurePhone, '发货联系方式'],
|
||
[row.arrivalPhone, '收货联系方式'],
|
||
[row.driverPhone, '手机号'],
|
||
[row.escortPhone, '押运人手机号'],
|
||
].find(([value]) => String(value || '').trim() && !isMobile(value));
|
||
if (invalidPhoneField) {
|
||
this.$message.warning(`第${index + 1}条调度明细的${invalidPhoneField[1]}格式不正确`);
|
||
return false;
|
||
}
|
||
continue;
|
||
}
|
||
if (!this.validateDispatchItemData(row, goodsRows, index)) return false;
|
||
}
|
||
return true;
|
||
},
|
||
handleDispatchSubmit(mode) {
|
||
if (!this.dispatchRow?.id || typeof this.api.dispatch !== 'function') {
|
||
this.$message.warning('运输计划调度接口不可用');
|
||
return;
|
||
}
|
||
if (!this.validateDispatchRows(mode)) {
|
||
return;
|
||
}
|
||
if (mode === 'submit') {
|
||
this.$confirm(
|
||
`请确认调度信息,将生成 <span style="color: #409eff; font-size: 20px; padding: 0 4px;">${this.dispatchRows.length}</span> 条运单,是否继续?`,
|
||
'提示',
|
||
{
|
||
confirmButtonText: '确认',
|
||
cancelButtonText: '取消',
|
||
dangerouslyUseHTMLString: true,
|
||
type: 'warning',
|
||
}
|
||
)
|
||
.then(() => this.submitDispatch(mode))
|
||
.catch(() => {});
|
||
return;
|
||
}
|
||
this.submitDispatch(mode);
|
||
},
|
||
submitDispatch(mode) {
|
||
this.dispatchSaving = mode;
|
||
const shouldCompletePlan = mode === 'submit' && !this.dispatchHasRemainingQuantity;
|
||
this.api
|
||
.dispatch(this.buildDispatchPayload(mode))
|
||
.then(() => {
|
||
if (shouldCompletePlan && typeof this.api.complete === 'function') {
|
||
return this.api.complete(this.dispatchRow.id);
|
||
}
|
||
return undefined;
|
||
})
|
||
.then(() => {
|
||
const message =
|
||
mode === 'draft'
|
||
? '生成草稿成功'
|
||
: shouldCompletePlan
|
||
? '确认生成成功,运输计划已完成'
|
||
: '确认生成成功';
|
||
this.$message.success(message);
|
||
this.dispatchBox = false;
|
||
this.onLoad(this.page, this.query);
|
||
})
|
||
.finally(() => {
|
||
this.dispatchSaving = '';
|
||
});
|
||
},
|
||
dispatchAttachmentLabel(item) {
|
||
return (
|
||
item?.originalName ||
|
||
item?.name ||
|
||
item?.fileName ||
|
||
item?.attachmentName ||
|
||
item?.title ||
|
||
'附件'
|
||
);
|
||
},
|
||
dispatchAttachmentUrl(item) {
|
||
return item?.url || item?.fileUrl || item?.downloadUrl || '';
|
||
},
|
||
formatDispatchRouteContact(contact = '', phone = '') {
|
||
return [contact, phone].filter(Boolean).join(' - ');
|
||
},
|
||
parseDispatchRoutePoint(row, type) {
|
||
const prefix = type === 'start' ? 'departure' : 'arrival';
|
||
const name = row[`${prefix}Name`] || row[`${prefix}Address`] || '';
|
||
const address = row[`${prefix}Address`] || row[`${prefix}DetailAddress`] || '';
|
||
const contact = this.formatDispatchRouteContact(
|
||
row[`${prefix}Contact`],
|
||
row[`${prefix}Phone`]
|
||
);
|
||
return { name, address, contact };
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
// 对齐 customer-archive 的 .archive-form 风格:label 固定宽折行 + 控件 250px
|
||
.transport-plan-page__dispatch-item-form,
|
||
.transport-plan-page__transport-plan-import-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-autocomplete),
|
||
:deep(.el-form-item__content > .el-input-number),
|
||
:deep(.el-form-item__content > .el-date-editor) {
|
||
width: 250px;
|
||
max-width: 100%;
|
||
}
|
||
}
|
||
|
||
.transport-plan-page {
|
||
&__field {
|
||
width: 100%;
|
||
}
|
||
|
||
&__contract-name {
|
||
text-align: left;
|
||
}
|
||
|
||
.dialog-section-title {
|
||
display: flex;
|
||
align-items: center;
|
||
width: 100%;
|
||
gap: 0;
|
||
color: #303133;
|
||
font-size: 14px;
|
||
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 {
|
||
box-sizing: border-box;
|
||
min-width: 100%;
|
||
max-width: 100%;
|
||
}
|
||
|
||
&__shipping-title,
|
||
&__goods-title {
|
||
justify-content: flex-start !important;
|
||
padding-left: 0;
|
||
text-align: left;
|
||
}
|
||
|
||
&__shipping-route-action {
|
||
margin-left: auto;
|
||
}
|
||
|
||
&__goods-title &__section-actions {
|
||
margin-left: auto;
|
||
}
|
||
|
||
&__section-actions {
|
||
display: flex;
|
||
flex: 0 0 auto;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
&__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;
|
||
}
|
||
|
||
.transport-plan-page__map-suffix {
|
||
color: #409eff;
|
||
font-size: 18px;
|
||
}
|
||
|
||
:deep(.el-input .el-input__icon) {
|
||
color: #409eff;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.transport-plan-page__address-pick-btn {
|
||
:deep(.el-icon) {
|
||
font-size: 22px;
|
||
}
|
||
}
|
||
}
|
||
|
||
&__dispatch-shipping-form {
|
||
:deep(.el-form-item) {
|
||
margin-bottom: 14px;
|
||
}
|
||
}
|
||
|
||
&__address-choose-link {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
height: 100%;
|
||
padding: 0 4px;
|
||
font-size: 13px;
|
||
line-height: 1;
|
||
cursor: pointer;
|
||
}
|
||
|
||
&__dispatch-shipping-form &__route-address-row {
|
||
grid-template-columns: 260px minmax(280px, 360px) 58px minmax(160px, 220px) 72px minmax(
|
||
160px,
|
||
220px
|
||
);
|
||
}
|
||
|
||
&__dispatch-mode-switch {
|
||
display: flex;
|
||
justify-content: flex-start;
|
||
width: 100%;
|
||
margin-top: 16px;
|
||
}
|
||
|
||
&__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;
|
||
box-sizing: border-box;
|
||
width: 100%;
|
||
min-width: 0;
|
||
max-width: 100%;
|
||
overflow: hidden;
|
||
}
|
||
|
||
&__cargo-import {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 56px;
|
||
padding: 0 !important;
|
||
|
||
: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-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(.transport-plan-page__full-form-item) {
|
||
width: 100%;
|
||
max-width: 100%;
|
||
}
|
||
|
||
:deep(.transport-plan-page__full-form-item > .el-form-item) {
|
||
width: 100%;
|
||
}
|
||
|
||
:deep(.transport-plan-page__full-form-item > .el-form-item > .el-form-item__label) {
|
||
display: none;
|
||
width: 0 !important;
|
||
padding: 0 !important;
|
||
}
|
||
|
||
:deep(.transport-plan-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(.transport-plan-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;
|
||
}
|
||
|
||
&__create-actions {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
&__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;
|
||
}
|
||
|
||
&__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;
|
||
}
|
||
|
||
&__dispatch-dialog {
|
||
:deep(.el-dialog__header) {
|
||
display: block;
|
||
height: 0;
|
||
padding: 0;
|
||
}
|
||
|
||
:deep(.el-dialog__headerbtn) {
|
||
z-index: 2;
|
||
top: 12px;
|
||
right: 12px;
|
||
}
|
||
|
||
:deep(.el-dialog__body) {
|
||
padding: 16px;
|
||
background: #f5f6fa;
|
||
display: flex;
|
||
height: calc(100vh - 140px);
|
||
max-height: calc(100vh - 140px);
|
||
box-sizing: border-box;
|
||
overflow: hidden;
|
||
}
|
||
}
|
||
|
||
&__dispatch-shell {
|
||
display: flex;
|
||
flex: 1;
|
||
flex-direction: column;
|
||
min-height: 0;
|
||
}
|
||
|
||
&__dispatch-top {
|
||
padding: 0;
|
||
}
|
||
|
||
&__dispatch-heading {
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
&__dispatch-heading-title {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
&__dispatch-heading-name {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
}
|
||
|
||
&__dispatch-heading-no {
|
||
font-size: 14px;
|
||
color: #606266;
|
||
}
|
||
|
||
&__dispatch-summary {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
|
||
gap: 16px;
|
||
align-items: stretch;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
&__dispatch-summary-card,
|
||
&__dispatch-route-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
|
||
:deep(.section-card__body) {
|
||
flex: 1;
|
||
}
|
||
}
|
||
|
||
&__dispatch-summary-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 12px 16px;
|
||
}
|
||
|
||
&__dispatch-summary-item {
|
||
min-width: 0;
|
||
|
||
&.is-wide {
|
||
grid-column: span 2;
|
||
}
|
||
}
|
||
|
||
&__dispatch-summary-label {
|
||
margin-bottom: 4px;
|
||
font-size: 12px;
|
||
color: #909399;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
&__dispatch-summary-value {
|
||
min-height: 20px;
|
||
font-size: 14px;
|
||
line-height: 1.45;
|
||
color: #303133;
|
||
word-break: break-word;
|
||
}
|
||
|
||
&__dispatch-summary-item--attachments {
|
||
grid-column: 1 / -1;
|
||
}
|
||
|
||
&__dispatch-attachment-link {
|
||
display: inline-block;
|
||
margin-right: 16px;
|
||
margin-bottom: 4px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
&__dispatch-route {
|
||
position: relative;
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) 56px minmax(0, 1fr);
|
||
gap: 40px;
|
||
align-items: center;
|
||
//height: 100%;
|
||
margin: 20px 0;
|
||
}
|
||
|
||
&__dispatch-route-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
max-width: 90%;
|
||
padding: 16px;
|
||
min-height: 80px;
|
||
border: 1px dashed #e3e4ec;
|
||
border-radius: 12px;
|
||
}
|
||
|
||
&__dispatch-route-badge {
|
||
display: inline-flex;
|
||
flex: 0 0 auto;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 50%;
|
||
color: #fff;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||
|
||
&.is-start {
|
||
background: #2a92ff;
|
||
}
|
||
|
||
&.is-end {
|
||
background: #ffb11a;
|
||
}
|
||
}
|
||
|
||
&__dispatch-route-body {
|
||
min-width: 0;
|
||
text-align: left;
|
||
}
|
||
|
||
&__dispatch-route-title {
|
||
font-size: 18px;
|
||
line-height: 1.35;
|
||
color: #303133;
|
||
font-weight: 600;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
&__dispatch-route-address {
|
||
margin-top: 4px;
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
color: #606266;
|
||
}
|
||
|
||
&__dispatch-route-contact {
|
||
margin-top: 2px;
|
||
font-size: 13px;
|
||
color: #909399;
|
||
}
|
||
|
||
&__dispatch-route-vehicle {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 44px;
|
||
height: 44px;
|
||
margin: 0 auto;
|
||
border-radius: 50%;
|
||
background: #fff;
|
||
border: 2px solid var(--el-color-primary);
|
||
color: var(--el-color-primary);
|
||
font-size: 22px;
|
||
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.18);
|
||
|
||
&::before,
|
||
&::after {
|
||
content: '';
|
||
position: absolute;
|
||
top: 50%;
|
||
width: 20px;
|
||
height: 0;
|
||
border-top: 2px dashed var(--el-color-primary-light-5);
|
||
transform: translateY(-50%);
|
||
}
|
||
|
||
&::before {
|
||
right: 100%;
|
||
margin-right: 6px;
|
||
}
|
||
|
||
&::after {
|
||
left: 100%;
|
||
margin-left: 6px;
|
||
}
|
||
}
|
||
|
||
&__dispatch-list-summary {
|
||
margin-left: 8px;
|
||
font-size: 14px;
|
||
color: #3c3c3c;
|
||
}
|
||
|
||
&__dispatch-table {
|
||
flex: 1;
|
||
min-height: 0;
|
||
font-size: 13px;
|
||
|
||
:deep(.el-table__header .cell) {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
:deep(.cell) {
|
||
white-space: normal;
|
||
line-height: 1.35;
|
||
}
|
||
|
||
:deep(.transport-plan-page__dispatch-driver) {
|
||
display: inline-block;
|
||
max-width: 100%;
|
||
white-space: nowrap;
|
||
}
|
||
}
|
||
|
||
&__dispatch-list-card {
|
||
display: flex;
|
||
flex: 1;
|
||
flex-direction: column;
|
||
min-height: 0;
|
||
|
||
:deep(.section-card__body) {
|
||
display: flex;
|
||
flex: 1;
|
||
flex-direction: column;
|
||
min-height: 0;
|
||
}
|
||
}
|
||
|
||
&__dispatch-actions {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
white-space: nowrap;
|
||
|
||
.el-button {
|
||
margin-left: 0;
|
||
}
|
||
}
|
||
|
||
&__dispatch-footer {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 10px;
|
||
padding-right: 0;
|
||
padding-top: 16px;
|
||
margin-top: 16px;
|
||
border-top: 1px solid #e9edf5;
|
||
}
|
||
|
||
&__dispatch-item-dialog {
|
||
width: calc(100% - 32px) !important;
|
||
max-width: 1400px;
|
||
|
||
:deep(.el-dialog__body) {
|
||
min-width: 0;
|
||
padding-top: 20px;
|
||
overflow-x: hidden;
|
||
}
|
||
}
|
||
|
||
&__dispatch-item-form {
|
||
box-sizing: border-box;
|
||
width: 100%;
|
||
min-width: 0;
|
||
max-width: 100%;
|
||
padding: 16px;
|
||
border: 1px solid #ebeef5;
|
||
background: #fff;
|
||
|
||
> .dialog-section-title {
|
||
margin-top: 16px;
|
||
|
||
&:first-child {
|
||
margin-top: 0;
|
||
}
|
||
}
|
||
|
||
> .transport-plan-page__dispatch-shipping-form,
|
||
> .transport-plan-page__cargo-wrap,
|
||
> .transport-plan-page__dispatch-item-grid,
|
||
> .transport-plan-page__attachment {
|
||
background: #fff;
|
||
}
|
||
|
||
> .transport-plan-page__attachment {
|
||
padding: 0 0 4px;
|
||
}
|
||
}
|
||
|
||
&__dispatch-unit-select {
|
||
width: 104px;
|
||
|
||
:deep(.el-select__wrapper),
|
||
:deep(.el-select__wrapper:hover),
|
||
:deep(.el-select__wrapper.is-focused) {
|
||
width: 104px;
|
||
min-height: 24px;
|
||
padding: 0 8px;
|
||
border: 0;
|
||
box-shadow: none !important;
|
||
}
|
||
}
|
||
|
||
&__dispatch-quantity-hint {
|
||
display: block;
|
||
width: 100%;
|
||
margin-top: 4px;
|
||
color: #909399;
|
||
font-size: 12px;
|
||
line-height: 18px;
|
||
}
|
||
|
||
&__dispatch-item-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 16px 24px;
|
||
|
||
:deep(.el-form-item) {
|
||
margin-bottom: 0;
|
||
min-width: 0;
|
||
}
|
||
|
||
&--compact {
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
}
|
||
|
||
&--full {
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
}
|
||
|
||
&--carrier {
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
}
|
||
}
|
||
|
||
&__dispatch-item-span-2 {
|
||
grid-column: span 2;
|
||
}
|
||
|
||
&__dispatch-item-span-3 {
|
||
grid-column: span 3;
|
||
}
|
||
|
||
&__dispatch-item-span-4 {
|
||
grid-column: 1 / -1;
|
||
}
|
||
|
||
&__dispatch-item-carrier-type {
|
||
grid-column: 1 / -1;
|
||
width: calc((100% - 48px) / 3);
|
||
}
|
||
|
||
&__required-column {
|
||
&::before {
|
||
margin-right: 4px;
|
||
color: #f56c6c;
|
||
content: '*';
|
||
}
|
||
}
|
||
|
||
&__attachment {
|
||
box-sizing: border-box;
|
||
width: 100%;
|
||
min-width: 0;
|
||
max-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;
|
||
box-sizing: border-box;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
width: 100%;
|
||
min-width: 0;
|
||
max-width: 100%;
|
||
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;
|
||
}
|
||
}
|
||
|
||
&__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-dialog.is-transport-plan-detail {
|
||
:deep(.el-dialog) {
|
||
max-width: none;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
:deep(.el-dialog__header) {
|
||
display: none;
|
||
}
|
||
|
||
:deep(.el-dialog__body) {
|
||
padding: 8px;
|
||
background: #f5f6f8;
|
||
}
|
||
}
|
||
|
||
&__transport-plan-detail-head,
|
||
&__transport-plan-waybill-card {
|
||
background: #fff;
|
||
border-radius: 6px;
|
||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||
}
|
||
|
||
&__transport-plan-detail-head {
|
||
padding: 20px 40px 24px;
|
||
}
|
||
|
||
&__transport-plan-detail-title-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
margin-bottom: 18px;
|
||
|
||
h2 {
|
||
margin: 0;
|
||
color: #303133;
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
|
||
span {
|
||
margin: 0 8px;
|
||
color: #303133;
|
||
}
|
||
}
|
||
|
||
:deep(.el-tag) {
|
||
height: 36px;
|
||
padding: 0 14px;
|
||
border: 0;
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
line-height: 36px;
|
||
}
|
||
|
||
:deep(.is-dispatching) {
|
||
color: #67c23a;
|
||
background: #e1f3d8;
|
||
}
|
||
}
|
||
|
||
&__transport-plan-detail-overview {
|
||
display: grid;
|
||
grid-template-columns: minmax(540px, 1fr) minmax(560px, 1.2fr);
|
||
gap: 22px;
|
||
}
|
||
|
||
&__transport-plan-detail-fields {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
gap: 16px 32px;
|
||
align-content: start;
|
||
}
|
||
|
||
&__transport-plan-detail-field {
|
||
display: flex;
|
||
min-width: 0;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
|
||
span {
|
||
color: #606266;
|
||
font-size: 14px;
|
||
}
|
||
|
||
strong {
|
||
overflow: hidden;
|
||
color: #409eff;
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
strong.is-normal {
|
||
color: #303133;
|
||
font-weight: 400;
|
||
white-space: normal;
|
||
word-break: break-word;
|
||
}
|
||
|
||
&.is-attachments {
|
||
grid-column: 1 / -1;
|
||
}
|
||
}
|
||
|
||
&__transport-plan-detail-attachments {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 14px 32px;
|
||
|
||
.el-link,
|
||
span {
|
||
font-size: 16px;
|
||
}
|
||
}
|
||
|
||
&__transport-plan-detail-route {
|
||
display: flex;
|
||
min-width: 0;
|
||
align-items: flex-start;
|
||
padding: 12px 0 0;
|
||
}
|
||
|
||
&__transport-plan-detail-route-point {
|
||
display: flex;
|
||
min-width: 0;
|
||
align-items: flex-start;
|
||
gap: 12px;
|
||
|
||
b {
|
||
display: inline-flex;
|
||
flex: 0 0 auto;
|
||
width: 36px;
|
||
height: 36px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #fff;
|
||
border-radius: 8px;
|
||
background: #409eff;
|
||
font-size: 14px;
|
||
|
||
&.is-end {
|
||
background: #e6a23c;
|
||
}
|
||
}
|
||
|
||
div {
|
||
min-width: 0;
|
||
text-align: center;
|
||
}
|
||
|
||
strong {
|
||
display: block;
|
||
color: #303133;
|
||
font-size: 18px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
p {
|
||
max-width: 280px;
|
||
margin: 10px 0 0;
|
||
color: #303133;
|
||
font-size: 14px;
|
||
line-height: 1.45;
|
||
word-break: break-word;
|
||
}
|
||
}
|
||
|
||
&__transport-plan-detail-route-line {
|
||
flex: 1;
|
||
min-width: 56px;
|
||
height: 22px;
|
||
margin: 0 14px;
|
||
border-bottom: 4px solid #e4e7ed;
|
||
}
|
||
|
||
&__transport-plan-waybill-card {
|
||
//margin-top: 8px;
|
||
padding: 16px 14px 12px;
|
||
}
|
||
|
||
&__transport-plan-waybill-heading {
|
||
display: flex;
|
||
align-items: baseline;
|
||
flex-wrap: wrap;
|
||
gap: 0;
|
||
margin: 0 0 14px;
|
||
padding-left: 16px;
|
||
|
||
h3 {
|
||
margin: 0 30px 0 0;
|
||
color: #303133;
|
||
font-size: 18px;
|
||
}
|
||
|
||
span {
|
||
color: #303133;
|
||
font-size: 16px;
|
||
}
|
||
|
||
em {
|
||
color: #409eff;
|
||
font-style: normal;
|
||
}
|
||
}
|
||
|
||
&__transport-plan-waybill-table {
|
||
width: 100%;
|
||
|
||
:deep(.el-table__header th) {
|
||
color: #303133;
|
||
background: #f7f7f7;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
:deep(.el-table__cell) {
|
||
height: 52px;
|
||
color: #303133;
|
||
border-color: #eff1f7;
|
||
font-size: 14px;
|
||
}
|
||
|
||
:deep(.el-table__body tr:nth-child(even) > td) {
|
||
background: #fafafa;
|
||
}
|
||
|
||
:deep(.el-table__fixed-right) {
|
||
background: #fff;
|
||
}
|
||
}
|
||
|
||
&__transport-plan-waybill-pagination {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
padding-top: 12px;
|
||
}
|
||
|
||
&__transport-plan-waybill-status {
|
||
&.is-processing {
|
||
color: #67c23a;
|
||
}
|
||
|
||
&.is-completed {
|
||
color: #409eff;
|
||
}
|
||
|
||
&.is-pending,
|
||
&.is-draft,
|
||
&.is-cancelled {
|
||
color: #606266;
|
||
}
|
||
}
|
||
|
||
&__transport-plan-import-form {
|
||
padding: 12px 24px 4px;
|
||
|
||
.el-select,
|
||
.el-input {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
&__import-file-item {
|
||
margin-top: 28px;
|
||
|
||
:deep(.el-form-item__content) {
|
||
gap: 16px;
|
||
}
|
||
}
|
||
|
||
&__import-file-tip {
|
||
color: #606266;
|
||
}
|
||
|
||
&__import-preview {
|
||
margin: 8px 24px 0;
|
||
padding-top: 16px;
|
||
border-top: 1px solid #eff1f7;
|
||
}
|
||
|
||
&__import-preview-head {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 24px;
|
||
margin-bottom: 12px;
|
||
|
||
.dialog-section-title {
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
|
||
&__import-preview-tabs {
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
&__import-preview-table {
|
||
overflow-x: auto;
|
||
|
||
:deep(.el-table) {
|
||
min-width: 1550px;
|
||
}
|
||
|
||
:deep(.el-table__cell) {
|
||
border-color: #eff1f7;
|
||
}
|
||
|
||
:deep(.el-table__row:nth-child(even) td) {
|
||
background: #fafafa;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 1280px) {
|
||
&__route-address-row {
|
||
grid-template-columns: 240px minmax(240px, 1fr) 40px 58px minmax(140px, 1fr);
|
||
}
|
||
|
||
&__dispatch-shipping-form &__route-address-row {
|
||
grid-template-columns: 240px minmax(240px, 300px) 58px minmax(140px, 180px) 72px minmax(
|
||
140px,
|
||
180px
|
||
);
|
||
}
|
||
|
||
&__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;
|
||
}
|
||
|
||
&__dispatch-shipping-form &__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;
|
||
}
|
||
|
||
/* 独立表单页:四个业务分区统一使用普通白色圆角模块。 */
|
||
:global(.transport-plan-form-page-dialog .transport-plan-page__full-form-item) {
|
||
width: 100%;
|
||
max-width: 100%;
|
||
}
|
||
|
||
:global(.transport-plan-form-page-dialog .transport-plan-page__full-form-item > .el-form-item) {
|
||
width: 100%;
|
||
}
|
||
|
||
:global(
|
||
.transport-plan-form-page-dialog
|
||
.transport-plan-page__full-form-item
|
||
> .el-form-item
|
||
> .el-form-item__label
|
||
) {
|
||
display: none;
|
||
width: 0 !important;
|
||
padding: 0 !important;
|
||
}
|
||
|
||
:global(
|
||
.transport-plan-form-page-dialog
|
||
.transport-plan-page__full-form-item
|
||
> .el-form-item
|
||
> .el-form-item__content
|
||
) {
|
||
flex: 1 1 100%;
|
||
width: 100%;
|
||
max-width: 100%;
|
||
margin-left: 0 !important;
|
||
}
|
||
|
||
:global(
|
||
.transport-plan-form-page-dialog
|
||
.transport-plan-page__full-form-item
|
||
> .el-form-item
|
||
> .el-form-item__content
|
||
> div
|
||
) {
|
||
width: 100%;
|
||
min-width: 0;
|
||
}
|
||
|
||
:global(.transport-plan-form-page-dialog) {
|
||
width: 100% !important;
|
||
min-height: 100%;
|
||
box-sizing: border-box;
|
||
padding-bottom: 96px;
|
||
background: #f5f6fa;
|
||
margin: 0 !important;
|
||
border-radius: 0;
|
||
box-shadow: none;
|
||
}
|
||
|
||
:global(.transport-plan-form-page-dialog .avue-form) {
|
||
padding: 0;
|
||
background: transparent;
|
||
box-shadow: none;
|
||
}
|
||
|
||
:global(.transport-plan-form-page-dialog .avue-form > .el-form > .el-row) {
|
||
margin: 0 !important;
|
||
}
|
||
|
||
:global(.transport-plan-form-page-dialog .avue-group) {
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
margin: 0 0 12px;
|
||
overflow: hidden;
|
||
border-radius: 6px;
|
||
background: #fff;
|
||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
||
}
|
||
|
||
:global(.transport-plan-form-page-dialog .avue-group:last-child) {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
:global(
|
||
.transport-plan-form-page-dialog--group-only
|
||
.avue-form
|
||
> .el-form
|
||
> .el-row
|
||
> .avue-group:first-child
|
||
) {
|
||
display: none;
|
||
}
|
||
|
||
:global(.transport-plan-form-page-dialog .avue-group .avue-form__group) {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
flex-wrap: wrap;
|
||
box-sizing: border-box;
|
||
margin: 0;
|
||
padding: 0 16px 4px;
|
||
background: transparent;
|
||
box-shadow: none;
|
||
}
|
||
|
||
:global(.transport-plan-form-page-dialog .avue-group .avue-group__header) {
|
||
height: auto;
|
||
min-height: 20px;
|
||
box-sizing: border-box;
|
||
padding: 0 16px !important;
|
||
border: 0;
|
||
line-height: 20px;
|
||
}
|
||
|
||
:global(.transport-plan-form-page-dialog .avue-group .avue-form__group--flex) {
|
||
display: flex;
|
||
}
|
||
|
||
:global(.transport-plan-form-page-dialog .avue-group .avue-form__row) {
|
||
padding: 0 !important;
|
||
}
|
||
|
||
:global(.transport-plan-form-page-dialog .avue-group .el-form-item) {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
:global(.transport-plan-form-page-dialog .avue-group .dialog-section-title) {
|
||
min-height: 20px;
|
||
margin: 8px 0 16px;
|
||
gap: 0;
|
||
color: #303133;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
text-align: left;
|
||
}
|
||
|
||
:global(.transport-plan-form-page-dialog .avue-group .dialog-section-title::before) {
|
||
flex: none;
|
||
width: 4px;
|
||
height: 16px;
|
||
margin-right: 8px;
|
||
background: #409eff;
|
||
content: '';
|
||
}
|
||
|
||
:global(.transport-plan-form-page-dialog .avue-group .transport-plan-page__shipping-title),
|
||
:global(.transport-plan-form-page-dialog .avue-group .transport-plan-page__goods-title) {
|
||
justify-content: flex-start !important;
|
||
padding-left: 0;
|
||
text-align: left;
|
||
}
|
||
|
||
/* 卡片视觉由外层 avue-group 统一承担,内层表单不再叠加白底和阴影。 */
|
||
:global(.transport-plan-form-page-dialog .avue-group .avue-form__group:has(.dialog-section-title)) {
|
||
padding: 0 16px 4px;
|
||
background: transparent;
|
||
border-radius: 0;
|
||
box-shadow: none;
|
||
}
|
||
|
||
:global(.transport-plan-form-page-dialog .el-dialog__body) {
|
||
max-height: none;
|
||
padding: 20px 24px 96px;
|
||
background: #f5f6fa;
|
||
}
|
||
|
||
:global(.el-overlay:has(.transport-plan-form-page-dialog)) {
|
||
position: fixed !important;
|
||
inset: 92px 0 0 230px !important;
|
||
z-index: 100 !important;
|
||
overflow: auto;
|
||
background: transparent;
|
||
}
|
||
|
||
:global(.el-overlay:has(.transport-plan-form-page-dialog) .el-overlay-dialog) {
|
||
min-height: 100%;
|
||
overflow: visible;
|
||
}
|
||
|
||
:global(.transport-plan-form-page-dialog .el-dialog__header) {
|
||
padding: 18px 24px;
|
||
margin-right: 0;
|
||
background: #fff;
|
||
border-bottom: 1px solid #eff1f7;
|
||
}
|
||
|
||
:global(.transport-plan-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(
|
||
.transport-plan-form-page-dialog
|
||
.avue-form__menu
|
||
> .transport-plan-page__standalone-menu-actions
|
||
) {
|
||
display: flex;
|
||
order: 2;
|
||
gap: 8px;
|
||
}
|
||
|
||
:global(.transport-plan-form-page-dialog .avue-form__menu > .el-button) {
|
||
order: 3;
|
||
}
|
||
|
||
:global(.avue--collapse .el-overlay:has(.transport-plan-form-page-dialog)) {
|
||
left: 60px !important;
|
||
}
|
||
|
||
:global(.avue--collapse .transport-plan-form-page-dialog .avue-form__menu) {
|
||
left: 60px;
|
||
}
|
||
|
||
:global(.avue-layout--horizontal .el-overlay:has(.transport-plan-form-page-dialog)) {
|
||
top: 92px !important;
|
||
left: 0 !important;
|
||
}
|
||
|
||
:global(.avue-layout--horizontal .transport-plan-form-page-dialog .avue-form__menu) {
|
||
left: 0;
|
||
}
|
||
|
||
/* 独立表单页 footer 的 Avue 提交/清空按钮自带图标(el-icon-check / el-icon-delete),
|
||
Avue 源码用 `|| 'el-icon-xxx'` 兜底,无法通过 option 配置去除,统一用 CSS 隐藏只保留文字 */
|
||
:global(.transport-plan-form-page-dialog .avue-form__menu .el-icon) {
|
||
display: none;
|
||
}
|
||
|
||
.el-input__icon {
|
||
color: #1e90ff !important;
|
||
}
|
||
</style>
|