Files
tms-erp-web/src/views/business/components/business-crud-page.vue
T
b2894lxlx c6217fcff7 Merge remote-tracking branch 'websoft/master'
# Conflicts:
#	src/views/business/process-config.vue
2026-08-14 14:19:48 +08:00

14749 lines
529 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<basic-container class="business-crud-page">
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
v-model:page="page"
v-model="form"
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"
@search-icon-change="handleSearchIconChange"
@on-load="onLoad"
>
<template #menu-left>
<el-button
type="primary"
:icon="actionButtonLikeSearch ? undefined : 'el-icon-plus'"
v-if="canCreate"
@click="$refs.crud.rowAdd()"
>{{ config.createText || '新增' }}
</el-button>
<el-button
type="primary"
icon="el-icon-upload"
plain
v-if="
(config.importUrl || config.enableTransportPlanImport) &&
hasPermission(`${config.permission}_import`) &&
!readonlyScope
"
@click="handleImport"
>{{ config.importText || '批量导入' }}
</el-button>
<el-button
type="primary"
icon="el-icon-download"
plain
v-if="config.templateUrl && hasPermission(`${config.permission}_template`)"
@click="handleTemplate"
>下载模板
</el-button>
<el-button
type="primary"
icon="el-icon-download"
plain
v-if="config.exportUrl && hasPermission(`${config.permission}_export`)"
@click="handleExport"
>批量导出
</el-button>
<el-button
type="success"
icon="el-icon-check"
plain
v-if="canBatchComplete"
@click="handleBatchComplete"
>批量完成
</el-button>
<el-button
type="primary"
:plain="!actionButtonLikeSearch"
v-if="canRoadLoading"
@click="handleRoadLoading"
>{{ config.roadLoadingText || '公路配载' }}
</el-button>
<el-button
type="danger"
icon="el-icon-delete"
plain
v-if="
config.batchDelete !== false &&
hasPermission(`${config.permission}_delete`) &&
!readonlyScope
"
@click="handleDelete"
>批量删除
</el-button>
</template>
<template #status="{ row }">
<el-tag :type="statusTagType(row.status)">
{{ displayStatus(row, 'status') }}
</el-tag>
</template>
<template #businessStatus="{ row }">
<el-tag :type="statusTagType(row.businessStatus)">
{{ displayStatus(row, 'businessStatus') }}
</el-tag>
</template>
<template #approvalStatus="{ row }">
<el-tag :type="statusTagType(row.approvalStatus)">
{{ displayStatus(row, 'approvalStatus') }}
</el-tag>
</template>
<template #goodsInfo="{ row }">
<span>{{ formatTransportPlanGoodsInfo(row) || '-' }}</span>
</template>
<template #departureAddress="{ row }">
<el-tooltip
v-if="isTransportPlanPage || isWaybillDetailLayout"
:content="row.departureAddress || '-'"
placement="top"
>
<span>{{
isTransportPlanPage
? formatTransportPlanProvinceCityDistrict(row.departureAddress)
: formatTransportPlanProvinceCityDistrict(row.departureAddress)
}}</span>
</el-tooltip>
<span v-else>{{ row.departureAddress || '-' }}</span>
</template>
<template #arrivalAddress="{ row }">
<el-tooltip
v-if="isTransportPlanPage || isWaybillDetailLayout"
:content="row.arrivalAddress || '-'"
placement="top"
>
<span>{{
isTransportPlanPage
? formatTransportPlanProvinceCityDistrict(row.arrivalAddress)
: formatTransportPlanProvinceCityDistrict(row.arrivalAddress)
}}</span>
</el-tooltip>
<span v-else>{{ row.arrivalAddress || '-' }}</span>
</template>
<template #basicInfoTitle-form>
<div class="dialog-section-title">基本信息</div>
</template>
<template #templateInfoTitle-form>
<div class="dialog-section-title">模板信息</div>
</template>
<template #contractCategory-label>合同类型</template>
<template v-if="config.enableProjectSelect" #projectName-label>项目</template>
<template v-if="config.enableContractExpiryTags" #search-menu>
<div v-show="searchExpanded" class="business-crud-page__contract-expiry-search">
<span class="business-crud-page__contract-expiry-label">签约类型:</span>
<el-select v-model="contractSignType" clearable placeholder="请选择">
<el-option
v-for="item in contractSignTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<div class="business-crud-page__contract-expiry-tags">
<el-check-tag
v-for="item in contractExpiryTagOptions"
:key="item.value"
:checked="contractExpiryScope === item.value"
@change="handleContractExpiryScopeChange(item.value)"
>
{{ item.label }}{{ contractExpiryStats[item.statKey] || 0 }}
</el-check-tag>
</div>
</div>
</template>
<template #projectName-form>
<el-select
v-model="selectedProjectId"
class="business-crud-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="business-crud-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="business-crud-page__field"
placeholder="请输入合同名称"
clearable
maxlength="100"
show-word-limit
:disabled="dialogReadonly"
/>
</template>
<template #planName-form>
<el-select
v-if="shippingPlanSelectEnabled"
v-model="form.planId"
class="business-crud-page__field"
placeholder="请选择计划名称"
filterable
clearable
:loading="shippingPlanLoading"
:disabled="dialogReadonly || isTemplateCreate"
@visible-change="visible => visible && loadShippingPlanOptions()"
@change="handleShippingPlanChange"
>
<el-option
v-for="item in shippingPlanOptions"
:key="item.id || item.planName"
:label="item.planName"
:value="item.id"
/>
</el-select>
<el-input
v-else
v-model="form.planName"
class="business-crud-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 business-crud-page__shipping-title"
>
<span>收发货信息</span>
<el-link
v-if="!dialogReadonly"
type="primary"
:disabled="transportAddressSelectDisabled"
@click="openTransportRouteDialog"
>
选择线路
</el-link>
</div>
</template>
<template #departureAddress-form>
<div class="business-crud-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="business-crud-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="business-crud-page__map-suffix"
@click.stop="openTransportMapDialog('departure')"
>
<Location />
</el-icon>
</template>
</el-input>
<el-tooltip content="选择常用地址" placement="top">
<el-button
type="primary"
:icon="List"
link
:disabled="transportAddressSelectDisabled"
@click="openTransportAddressPicker('departure')"
/>
</el-tooltip>
<span class="business-crud-page__route-label">联系人</span>
<el-input
v-model="form.departureContact"
placeholder="请输入"
:disabled="dialogReadonly"
/>
<span class="business-crud-page__route-label">联系方式</span>
<el-input v-model="form.departurePhone" placeholder="请输入" :disabled="dialogReadonly" />
</div>
</template>
<template #arrivalAddress-form>
<div class="business-crud-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="business-crud-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="business-crud-page__map-suffix"
@click.stop="openTransportMapDialog('arrival')"
>
<Location />
</el-icon>
</template>
</el-input>
<el-tooltip content="选择常用地址" placement="top">
<el-button
type="primary"
:icon="List"
link
:disabled="transportAddressSelectDisabled"
@click="openTransportAddressPicker('arrival')"
/>
</el-tooltip>
<span class="business-crud-page__route-label">联系人</span>
<el-input v-model="form.arrivalContact" placeholder="请输入" :disabled="dialogReadonly" />
<span class="business-crud-page__route-label">联系方式</span>
<el-input v-model="form.arrivalPhone" placeholder="请输入" :disabled="dialogReadonly" />
</div>
</template>
<template #taskInfoTitle-form>
<div class="dialog-section-title dialog-section-title--action">
<span>任务信息</span>
<el-segmented
v-model="form.taskEntryMode"
:options="taskEntryModeOptions"
:disabled="dialogReadonly"
@change="handleTaskEntryModeChange"
/>
</div>
</template>
<template #taskInfoForm-form>
<div class="business-crud-page__task">
<div v-if="isTaskFullMode" class="business-crud-page__task-full">
<div class="business-crud-page__subsection">
<div
class="dialog-section-title dialog-section-title--action business-crud-page__goods-title"
>
<span>货物信息</span>
<div v-if="!dialogReadonly" class="business-crud-page__section-actions">
<el-link type="primary" @click="cargoImportBox = true">导入货物</el-link>
<el-link type="primary" @click="openCommonCargoDialog">常用货物</el-link>
</div>
</div>
<div class="business-crud-page__cargo-wrap">
<el-table
:data="transportCargoRows"
border
class="business-crud-page__cargo-table business-crud-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="business-crud-page__required-column">货物类型</span>
</template>
<template #default="{ row }">
<el-cascader
v-model="row.cargoTypePath"
:options="transportCargoTypeOptions"
:props="transportCargoTypeCascaderProps"
placeholder="从货物类型获取"
clearable
filterable
:disabled="dialogReadonly"
:filter-method="filterBillingCargoType"
@visible-change="visible => visible && ensureBillingCargoTypeOptions()"
@change="value => handleTransportCargoTypeChange(row, value)"
/>
</template>
</el-table-column>
<el-table-column min-width="240" align="center">
<template #header>
<span class="business-crud-page__required-column">货物名称</span>
</template>
<template #default="{ row }">
<el-autocomplete
v-model="row.cargoName"
placeholder="请输入"
clearable
:debounce="300"
:fetch-suggestions="
(queryString, callback) =>
fetchTaskCargoSuggestions(queryString, callback, row.cargoTypePath)
"
:disabled="dialogReadonly"
:loading="isTaskCargoOptionsLoading(row.cargoTypePath)"
@select="item => handleTaskCargoRowSelect(row, item)"
>
<template #default="{ item }">
<div class="business-crud-page__cargo-autocomplete-item">
<span>{{ formatBillingCargoTypeLabel(item) }}</span>
<small v-if="item.cargoCode">{{ item.cargoCode }}</small>
</div>
</template>
</el-autocomplete>
</template>
</el-table-column>
<el-table-column label="包装" min-width="130" align="center">
<template #default="{ row }">
<el-select v-model="row.packageType" clearable :disabled="dialogReadonly">
<el-option
v-for="item in packageOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</el-table-column>
<el-table-column min-width="130" align="center">
<template #header>
<span class="business-crud-page__required-column">数量</span>
</template>
<template #default="{ row }">
<el-input
v-model="row.quantity"
placeholder="请输入"
:disabled="dialogReadonly"
@input="value => handleTransportCargoQuantityInput(row, value)"
/>
</template>
</el-table-column>
<el-table-column min-width="140" align="center">
<template #header>
<span class="business-crud-page__required-column">数量单位</span>
</template>
<template #default="{ row }">
<el-select
v-model="row.quantityUnit"
clearable
:disabled="dialogReadonly"
@visible-change="visible => visible && loadTaskQuantityUnitOptions()"
>
<el-option
v-for="item in taskQuantityUnitOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="品牌" min-width="130" align="center">
<template #default="{ row }">
<el-input
v-model="row.brand"
placeholder="请输入"
:disabled="dialogReadonly"
/>
</template>
</el-table-column>
<el-table-column label="规格" min-width="130" align="center">
<template #default="{ row }">
<el-input
v-model="row.specification"
placeholder="请输入"
:disabled="dialogReadonly"
/>
</template>
</el-table-column>
<el-table-column label="型号" min-width="130" align="center">
<template #default="{ row }">
<el-input
v-model="row.model"
placeholder="请输入"
:disabled="dialogReadonly"
/>
</template>
</el-table-column>
<el-table-column label="物料编码" min-width="150" align="center">
<template #default="{ row }">
<el-input
v-model="row.materialCode"
placeholder="请输入"
:disabled="dialogReadonly"
/>
</template>
</el-table-column>
<el-table-column label="设备编码" min-width="150" align="center">
<template #default="{ row }">
<el-input
v-model="row.deviceCode"
placeholder="请输入"
:disabled="dialogReadonly"
/>
</template>
</el-table-column>
<el-table-column label="备注" min-width="180" align="center">
<template #default="{ row }">
<el-input
v-model="row.remark"
placeholder="请输入"
:disabled="dialogReadonly"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="140" align="center" fixed="right">
<template #default="{ $index }">
<div class="business-crud-page__cargo-actions">
<el-link
v-if="!dialogReadonly && $index === transportCargoRows.length - 1"
type="primary"
@click="addTransportCargoRow($index)"
>
新增
</el-link>
<el-link
v-if="!dialogReadonly"
type="danger"
@click="removeTransportCargoRow($index)"
>
删除
</el-link>
</div>
</template>
</el-table-column>
</el-table>
</div>
</div>
<el-form
:model="form"
label-position="right"
label-width="auto"
class="business-crud-page__task-form business-crud-page__task-form--full"
>
<div class="business-crud-page__task-row">
<el-form-item
label="承运类型"
required
class="business-crud-page__task-carrier-type business-crud-page__task-span-4"
>
<el-radio-group
v-model="form.carrierType"
:disabled="dialogReadonly"
@change="handleTaskCarrierTypeChange"
>
<el-radio-button
v-for="item in taskCarrierTypes"
:key="item"
:label="item"
:value="item"
/>
</el-radio-group>
</el-form-item>
</div>
<div
v-if="hasTransportType && isCarrierMode"
class="business-crud-page__subsection business-crud-page__carrier-section"
>
<div class="business-crud-page__subsection-head">
<span>承运信息</span>
</div>
<div class="business-crud-page__task-row">
<el-form-item label="承运商" required class="business-crud-page__task-span-1">
<el-select
v-model="form.carrierName"
placeholder="请输入"
filterable
clearable
:suffix-icon="Search"
:loading="taskCarrierLoading"
:disabled="dialogReadonly"
@visible-change="visible => visible && loadTaskCarrierOptions()"
@change="handleTaskCarrierChange"
>
<el-option
v-for="item in taskCarrierOptions"
:key="
item.id ||
item.customerName ||
item.carrierName ||
item.fullName ||
item.name ||
item.customerCode
"
:label="item.customerName || item.carrierName || item.fullName || item.name"
:value="item.customerName || item.carrierName || item.fullName || item.name"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="!isNonRoadTransport"
label="司机"
class="business-crud-page__task-span-1"
>
<el-autocomplete
v-model="form.driverName"
placeholder="请输入"
clearable
:debounce="300"
:fetch-suggestions="fetchTaskDriverSuggestions"
:loading="taskDriverLoading"
:disabled="dialogReadonly"
@select="item => handleTaskDriverSelect('form', item)"
/>
</el-form-item>
<el-form-item
v-if="!isNonRoadTransport"
label="手机号"
class="business-crud-page__task-span-1"
>
<el-input
v-model="form.driverPhone"
placeholder="请输入"
clearable
maxlength="20"
:disabled="dialogReadonly"
/>
</el-form-item>
<el-form-item
:label="transportVehicleNoLabel"
required
class="business-crud-page__task-span-1"
>
<el-input
v-model="form.vehicleNo"
:placeholder="`请输入${transportVehicleNoLabel}`"
clearable
maxlength="30"
:disabled="dialogReadonly"
/>
</el-form-item>
<el-form-item
v-if="isNonRoadTransport"
label="船长"
class="business-crud-page__task-span-1"
>
<el-input
v-model="form.captainName"
placeholder="请输入"
clearable
maxlength="20"
:disabled="dialogReadonly"
/>
</el-form-item>
<el-form-item
v-if="isNonRoadTransport"
label="联系电话"
class="business-crud-page__task-span-1"
>
<el-input
v-model="form.driverPhone"
placeholder="请输入"
clearable
maxlength="20"
:disabled="dialogReadonly"
/>
</el-form-item>
<el-form-item
v-if="isNonRoadTransport"
label="箱号"
class="business-crud-page__task-span-1"
>
<el-input
v-model="form.containerNo"
placeholder="请输入"
clearable
maxlength="30"
:disabled="dialogReadonly"
/>
</el-form-item>
</div>
<div class="business-crud-page__task-row">
<el-form-item
v-if="isNonRoadTransport"
label="舱位"
class="business-crud-page__task-span-1"
>
<el-input
v-model="form.cabinNo"
placeholder="请输入"
clearable
maxlength="30"
:disabled="dialogReadonly"
/>
</el-form-item>
<el-form-item
v-if="showRoadEscortFields"
label="挂车车牌号"
class="business-crud-page__task-span-1"
>
<el-input
v-model="form.trailerVehicleNo"
placeholder="请输入"
clearable
maxlength="30"
:disabled="dialogReadonly"
/>
</el-form-item>
<el-form-item
v-if="showRoadEscortFields"
label="押运人"
class="business-crud-page__task-span-1"
>
<el-input
v-model="form.escortName"
placeholder="请输入"
clearable
maxlength="20"
:disabled="dialogReadonly"
/>
</el-form-item>
<el-form-item
v-if="showRoadEscortFields"
label="押运人手机号"
class="business-crud-page__task-span-1"
>
<el-input
v-model="form.escortPhone"
placeholder="请输入"
clearable
maxlength="20"
:disabled="dialogReadonly"
/>
</el-form-item>
<el-form-item
v-if="!isNonRoadTransport"
label="里程(km)"
class="business-crud-page__task-span-1"
>
<el-input
v-model="form.mileage"
placeholder="请输入"
clearable
maxlength="10"
inputmode="numeric"
:disabled="dialogReadonly"
@input="handleTaskMileageInput"
/>
</el-form-item>
</div>
<div v-if="!isNonRoadTransport" class="business-crud-page__task-row">
<el-form-item
:label="isRoadTransport ? '计划发货日期' : '预计发货日期'"
class="business-crud-page__task-span-1"
>
<el-date-picker
v-model="form.estimatedStartTime"
type="date"
placeholder="默认填充当天"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
:disabled="dialogReadonly"
/>
</el-form-item>
<el-form-item
:label="isRoadTransport ? '计划完成日期' : '预计完成日期'"
class="business-crud-page__task-span-1"
>
<el-date-picker
v-model="form.estimatedEndTime"
type="date"
placeholder="请输入"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
:disabled="dialogReadonly"
/>
</el-form-item>
</div>
<div class="business-crud-page__task-row">
<el-form-item
label="备注"
class="business-crud-page__task-note business-crud-page__task-span-4"
>
<el-input
v-model="form.taskRemark"
placeholder="请输入,200字以内"
clearable
maxlength="200"
show-word-limit
:disabled="dialogReadonly"
/>
</el-form-item>
</div>
</div>
<template v-else-if="hasTransportType">
<div v-if="isRoadTransport" class="business-crud-page__task-row">
<el-form-item
label="司机"
:required="isRoadTransport"
class="business-crud-page__task-span-1"
>
<el-autocomplete
v-model="form.driverName"
placeholder="请输入"
clearable
:debounce="300"
:fetch-suggestions="fetchTaskDriverSuggestions"
:loading="taskDriverLoading"
:disabled="dialogReadonly"
@select="item => handleTaskDriverSelect('form', item)"
/>
</el-form-item>
<el-form-item label="手机号" required class="business-crud-page__task-span-1">
<el-input
v-model="form.driverPhone"
placeholder="请输入"
clearable
maxlength="20"
:disabled="dialogReadonly"
/>
</el-form-item>
<el-form-item label="车牌号" required class="business-crud-page__task-span-1">
<el-input
v-model="form.vehicleNo"
placeholder="请输入"
clearable
maxlength="30"
:disabled="dialogReadonly"
/>
</el-form-item>
<el-form-item label="挂车车牌号" class="business-crud-page__task-span-1">
<el-input
v-model="form.trailerVehicleNo"
placeholder="请输入"
clearable
maxlength="30"
:disabled="dialogReadonly"
/>
</el-form-item>
</div>
<div v-if="isRoadTransport" class="business-crud-page__task-row">
<el-form-item label="押运人" class="business-crud-page__task-span-1">
<el-input
v-model="form.escortName"
placeholder="请输入"
clearable
maxlength="20"
:disabled="dialogReadonly"
/>
</el-form-item>
<el-form-item label="押运人手机号" class="business-crud-page__task-span-1">
<el-input
v-model="form.escortPhone"
placeholder="请输入"
clearable
maxlength="20"
:disabled="dialogReadonly"
/>
</el-form-item>
<el-form-item label="里程(km)" class="business-crud-page__task-span-1">
<el-input
v-model="form.mileage"
placeholder="请输入"
clearable
maxlength="10"
inputmode="numeric"
:disabled="dialogReadonly"
@input="handleTaskMileageInput"
/>
</el-form-item>
</div>
<div v-if="isRoadTransport" class="business-crud-page__task-row">
<el-form-item
:label="isRoadTransport ? '计划发货日期' : '预计发货日期'"
class="business-crud-page__task-span-1"
>
<el-date-picker
v-model="form.estimatedStartTime"
type="date"
placeholder="默认填充当天"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
:disabled="dialogReadonly"
/>
</el-form-item>
<el-form-item
:label="isRoadTransport ? '计划完成日期' : '预计完成日期'"
class="business-crud-page__task-span-1"
>
<el-date-picker
v-model="form.estimatedEndTime"
type="date"
placeholder="请输入"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
:disabled="dialogReadonly"
/>
</el-form-item>
</div>
<div v-if="isRoadTransport" class="business-crud-page__task-row">
<el-form-item
label="备注"
class="business-crud-page__task-note business-crud-page__task-span-4"
>
<el-input
v-model="form.taskRemark"
placeholder="请输入,200字以内"
clearable
maxlength="200"
show-word-limit
:disabled="dialogReadonly"
/>
</el-form-item>
</div>
<template v-if="isNonRoadTransport">
<div class="business-crud-page__task-row">
<el-form-item :label="transportVehicleNoLabel" required class="business-crud-page__task-span-1">
<el-input v-model="form.vehicleNo" :placeholder="`请输入${transportVehicleNoLabel}`" clearable maxlength="30" :disabled="dialogReadonly" />
</el-form-item>
<el-form-item label="船长" class="business-crud-page__task-span-1">
<el-input v-model="form.captainName" placeholder="请输入" clearable maxlength="20" :disabled="dialogReadonly" />
</el-form-item>
<el-form-item label="联系电话" class="business-crud-page__task-span-1">
<el-input v-model="form.driverPhone" placeholder="请输入" clearable maxlength="20" :disabled="dialogReadonly" />
</el-form-item>
<el-form-item label="箱号" class="business-crud-page__task-span-1">
<el-input v-model="form.containerNo" placeholder="请输入" clearable maxlength="30" :disabled="dialogReadonly" />
</el-form-item>
</div>
<div class="business-crud-page__task-row">
<el-form-item label="舱位" class="business-crud-page__task-span-1">
<el-input v-model="form.cabinNo" placeholder="请输入" clearable maxlength="30" :disabled="dialogReadonly" />
</el-form-item>
</div>
<div class="business-crud-page__task-row">
<el-form-item label="备注" class="business-crud-page__task-note business-crud-page__task-span-4">
<el-input v-model="form.taskRemark" placeholder="请输入,200字以内" clearable maxlength="200" show-word-limit :disabled="dialogReadonly" />
</el-form-item>
</div>
</template>
</template>
</el-form>
<div class="business-crud-page__subsection">
<div class="business-crud-page__subsection-head">
<span>运费信息</span>
</div>
<el-form
:model="form"
label-position="right"
label-width="auto"
class="business-crud-page__freight-form business-crud-page__freight-form--road"
>
<template v-for="(cargo, index) in transportCargoRows" :key="index">
<el-form-item :label="`单价${index + 1}`">
<el-input
v-model="cargo.unitPrice"
placeholder="请输入"
clearable
:disabled="dialogReadonly"
@input="value => handleTaskFullFreightNumberInput(cargo, 'unitPrice', value)"
>
<template #append>
<el-select
v-model="cargo.priceUnit"
class="business-crud-page__append-select business-crud-page__append-select--wide"
placeholder="单位"
:disabled="dialogReadonly"
@visible-change="visible => visible && loadTaskFeeUnitOptions()"
>
<el-option
v-for="item in taskFeeUnitOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</el-input>
</el-form-item>
<el-form-item :label="`数量合计${index + 1}`">
<el-input :model-value="taskFullFreightQuantity(index)" disabled>
<template #append>
<el-select
:model-value="cargo.quantityUnit"
class="business-crud-page__append-select business-crud-page__append-select--wide"
placeholder="单位"
disabled
>
<el-option
:label="cargo.quantityUnit || '单位'"
:value="cargo.quantityUnit || ''"
/>
</el-select>
</template>
</el-input>
</el-form-item>
<el-form-item :label="`运费${index + 1}`">
<el-input
:model-value="taskFullFreightAmount(cargo)"
disabled
placeholder="自动计算"
>
<template #suffix>{{ taskFreightCurrencyLabel }}</template>
</el-input>
</el-form-item>
</template>
<el-form-item label="运费合计">
<el-input :model-value="taskFullFreightTotal" disabled>
<template #suffix>{{ taskFreightCurrencyLabel }}</template>
</el-input>
</el-form-item>
<el-form-item label="币种">
<el-select
v-model="taskFreightCurrency"
placeholder="请选择"
clearable
:disabled="dialogReadonly"
@visible-change="visible => visible && loadShippingTemplateCurrencyOptions()"
>
<el-option
v-for="item in shippingTemplateCurrencyOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="其他运费合计">
<el-input
v-model="form.otherFeeTotal"
placeholder="请输入"
clearable
:disabled="dialogReadonly"
@input="value => handleTaskNumberInput('otherFeeTotal', value)"
>
<template #suffix>{{ taskFreightCurrencyLabel }}</template>
</el-input>
</el-form-item>
</el-form>
</div>
</div>
<el-form
v-else
:model="form"
label-position="right"
label-width="auto"
class="business-crud-page__task-form"
>
<el-form-item
label="承运类型"
required
class="business-crud-page__task-carrier-type business-crud-page__task-span-4"
>
<el-radio-group
v-model="form.carrierType"
:disabled="dialogReadonly"
@change="handleTaskCarrierTypeChange"
>
<el-radio-button
v-for="item in taskCarrierTypes"
:key="item"
:label="item"
:value="item"
/>
</el-radio-group>
</el-form-item>
<template v-if="hasTransportType">
<el-form-item v-if="isCarrierMode" label="承运商" required>
<el-select
v-model="form.carrierName"
placeholder="请选择承运商"
filterable
clearable
:loading="taskCarrierLoading"
:disabled="dialogReadonly || form.carrierType !== '承运商'"
@visible-change="visible => visible && loadTaskCarrierOptions()"
@change="handleTaskCarrierChange"
>
<el-option
v-for="item in taskCarrierOptions"
:key="
item.id ||
item.customerName ||
item.carrierName ||
item.fullName ||
item.name ||
item.customerCode
"
:label="item.customerName || item.carrierName || item.fullName || item.name"
:value="item.customerName || item.carrierName || item.fullName || item.name"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="isRoadTransport"
label="司机"
:required="form.carrierType === '自运'"
>
<el-autocomplete
v-model="form.driverName"
placeholder="请输入司机"
clearable
:debounce="300"
:fetch-suggestions="fetchTaskDriverSuggestions"
:loading="taskDriverLoading"
:disabled="dialogReadonly"
@select="item => handleTaskDriverSelect('form', item)"
/>
</el-form-item>
<el-form-item
v-if="isRoadTransport"
label="手机号"
:required="form.carrierType === '自运'"
>
<el-input
v-model="form.driverPhone"
placeholder="请输入手机号"
clearable
maxlength="20"
:disabled="dialogReadonly"
/>
</el-form-item>
<el-form-item :label="transportVehicleNoLabel" required>
<el-input
v-model="form.vehicleNo"
:placeholder="`请输入${transportVehicleNoLabel}`"
clearable
maxlength="30"
:disabled="dialogReadonly"
/>
</el-form-item>
<el-form-item v-if="isNonRoadTransport" label="船长">
<el-input
v-model="form.captainName"
placeholder="请输入船长"
clearable
maxlength="20"
:disabled="dialogReadonly"
/>
</el-form-item>
<el-form-item v-if="isNonRoadTransport && isCarrierMode" label="联系电话">
<el-input
v-model="form.driverPhone"
placeholder="请输入联系电话"
clearable
maxlength="20"
:disabled="dialogReadonly"
/>
</el-form-item>
<el-form-item v-if="isNonRoadTransport" label="箱号">
<el-input
v-model="form.containerNo"
placeholder="请输入箱号"
clearable
maxlength="30"
:disabled="dialogReadonly"
/>
</el-form-item>
<el-form-item v-if="isNonRoadTransport" label="舱位">
<el-input
v-model="form.cabinNo"
placeholder="请输入舱位"
clearable
maxlength="30"
:disabled="dialogReadonly"
/>
</el-form-item>
<el-form-item v-if="showRoadEscortFields" label="挂车车牌号">
<el-input
v-model="form.trailerVehicleNo"
placeholder="请输入挂车车牌号"
clearable
maxlength="30"
:disabled="dialogReadonly"
/>
</el-form-item>
<el-form-item v-if="showRoadEscortFields" label="押运人">
<el-input
v-model="form.escortName"
placeholder="请输入押运人"
clearable
maxlength="20"
:disabled="dialogReadonly"
/>
</el-form-item>
<el-form-item v-if="showRoadEscortFields" label="押运人手机号">
<el-input
v-model="form.escortPhone"
placeholder="请输入押运人手机号"
clearable
maxlength="20"
:disabled="dialogReadonly"
/>
</el-form-item>
<el-form-item label="货物类型" required>
<el-cascader
v-model="form.cargoTypePath"
:options="transportCargoTypeOptions"
:props="transportCargoTypeCascaderProps"
placeholder="请选择到二级"
clearable
filterable
:disabled="dialogReadonly"
:filter-method="filterBillingCargoType"
@visible-change="visible => visible && ensureBillingCargoTypeOptions()"
@change="handleTaskCargoTypeChange"
/>
</el-form-item>
<el-form-item label="货物名称">
<el-autocomplete
v-model="form.cargoName"
placeholder="请输入货物名称"
clearable
:debounce="300"
:fetch-suggestions="
(queryString, callback) =>
fetchTaskCargoSuggestions(queryString, callback, form.cargoTypePath)
"
:disabled="dialogReadonly"
:loading="taskCargoLoading"
@select="handleTaskCargoSelect"
>
<template #default="{ item }">
<div class="business-crud-page__cargo-autocomplete-item">
<span>{{ formatBillingCargoTypeLabel(item) }}</span>
<small v-if="item.cargoCode">{{ item.cargoCode }}</small>
</div>
</template>
</el-autocomplete>
</el-form-item>
<el-form-item label="规格">
<el-input
v-model="form.specification"
placeholder="选择货物自动回填"
clearable
:disabled="dialogReadonly"
/>
</el-form-item>
<el-form-item label="型号">
<el-input
v-model="form.model"
placeholder="选择货物自动回填"
clearable
:disabled="dialogReadonly"
/>
</el-form-item>
<el-form-item label="数量">
<el-input
v-model="form.quantity"
placeholder="请输入数量"
clearable
:disabled="dialogReadonly"
@input="value => handleTaskNumberInput('quantity', value)"
>
<template #append>
<el-select
v-model="form.quantityUnit"
class="business-crud-page__append-select business-crud-page__append-select--wide"
placeholder="单位"
:disabled="dialogReadonly"
@visible-change="visible => visible && loadTaskQuantityUnitOptions()"
>
<el-option
v-for="item in taskQuantityUnitOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</el-input>
</el-form-item>
<el-form-item label="里程(km)">
<el-input
v-model="form.mileage"
placeholder="请输入里程"
clearable
maxlength="10"
inputmode="numeric"
:disabled="dialogReadonly"
@input="handleTaskMileageInput"
/>
</el-form-item>
<el-form-item :label="isRoadTransport ? '计划发货日期' : '预计发货日期'">
<el-date-picker
v-model="form.estimatedStartTime"
type="date"
placeholder="请选择预计发货日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
:disabled="dialogReadonly"
/>
</el-form-item>
<el-form-item :label="isRoadTransport ? '计划完成日期' : '预计完成日期'">
<el-date-picker
v-model="form.estimatedEndTime"
type="date"
placeholder="请选择预计完成日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
:disabled="dialogReadonly"
/>
</el-form-item>
<el-form-item label="单价">
<el-input
v-model="form.unitPrice"
placeholder="请输入单价"
clearable
:disabled="dialogReadonly"
@input="value => handleTaskNumberInput('unitPrice', value)"
>
<template #append>
<el-select
v-model="form.priceUnit"
class="business-crud-page__append-select business-crud-page__append-select--wide"
placeholder="单位"
:disabled="dialogReadonly"
@visible-change="visible => visible && loadTaskFeeUnitOptions()"
>
<el-option
v-for="item in taskFeeUnitOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</el-input>
</el-form-item>
<el-form-item label="其他费用合计">
<el-input
v-model="form.otherFeeTotal"
placeholder="请输入其他费用合计"
clearable
:disabled="dialogReadonly"
@input="value => handleTaskNumberInput('otherFeeTotal', value)"
/>
</el-form-item>
<el-form-item label="备注" class="business-crud-page__task-remark">
<el-input
v-model="form.taskRemark"
type="textarea"
:rows="2"
maxlength="200"
show-word-limit
placeholder="请输入备注,200字以内"
:disabled="dialogReadonly"
/>
</el-form-item>
</template>
</el-form>
</div>
</template>
<template #goodsInfoTitle-form>
<div
v-if="config.enableTransportPlanForm"
class="dialog-section-title dialog-section-title--action business-crud-page__goods-title"
>
<span>货物信息</span>
<div v-if="!dialogReadonly" class="business-crud-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="business-crud-page__cargo-wrap">
<el-table
:data="transportCargoRows"
border
class="business-crud-page__cargo-table"
empty-text="暂无货物信息"
>
<el-table-column type="index" label="序号" width="70" align="center" fixed="left" />
<el-table-column label="货物名称" min-width="150" 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="business-crud-page__cargo-autocomplete-item">
<span>{{ formatBillingCargoTypeLabel(item) }}</span>
<small v-if="item.cargoCode">{{ item.cargoCode }}</small>
</div>
</template>
</el-autocomplete>
</template>
</el-table-column>
<el-table-column min-width="220" align="center">
<template #header>
<span class="business-crud-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="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="business-crud-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 v-if="!shippingTemplateFreightVisible" class="business-crud-page__cargo-total">
运输数量合计:{{ transportCargoQuantityTotal }}
</div>
<section
v-if="shippingTemplateFreightVisible"
class="business-crud-page__freight-section"
>
<div class="dialog-section-title">
运费信息{{ transportMode === 'road' ? '(公路)' : '' }}
</div>
<template v-if="transportMode === 'road'">
<el-form
:model="shippingTemplateFreight"
label-position="right"
label-width="auto"
class="business-crud-page__freight-form business-crud-page__freight-form--road"
>
<template v-for="(row, index) in shippingTemplateFreight.freightItems" :key="index">
<el-form-item :label="`单价${index + 1}`">
<el-input
v-model="row.unitPrice"
placeholder="请输入"
:disabled="dialogReadonly"
@input="
value => handleShippingTemplateFreightNumberInput(row, 'unitPrice', value)
"
>
<template #append>
<el-select
v-model="row.priceUnit"
class="business-crud-page__append-select"
placeholder="单位"
:disabled="dialogReadonly"
@visible-change="visible => visible && loadTaskFeeUnitOptions()"
>
<el-option
v-for="item in taskFeeUnitOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</el-input>
</el-form-item>
<el-form-item :label="`数量合计${index + 1}`">
<el-input :model-value="shippingTemplateRoadFreightQuantity(index)" disabled>
<template #append>
<el-select
:model-value="transportCargoRows[index]?.quantityUnit"
class="business-crud-page__append-select"
placeholder="单位"
disabled
>
<el-option
:label="transportCargoRows[index]?.quantityUnit || '单位'"
:value="transportCargoRows[index]?.quantityUnit || ''"
/>
</el-select>
</template>
</el-input>
</el-form-item>
<el-form-item :label="`运费${index + 1}`">
<el-input :model-value="shippingTemplateRoadFreightAmount(row, index)" disabled>
<template #suffix>{{ shippingTemplateCurrencyLabel }}</template>
</el-input>
</el-form-item>
</template>
<el-form-item label="运费合计">
<el-input :model-value="shippingTemplateRoadFreightTotal" disabled>
<template #suffix>{{ shippingTemplateCurrencyLabel }}</template>
</el-input>
</el-form-item>
<el-form-item label="其他运费合计">
<el-input
v-model="shippingTemplateFreight.otherFreightAmount"
placeholder="请输入"
:disabled="dialogReadonly"
@input="
value =>
handleShippingTemplateFreightNumberInput(
shippingTemplateFreight,
'otherFreightAmount',
value
)
"
>
<template #suffix>{{ shippingTemplateCurrencyLabel }}</template>
</el-input>
</el-form-item>
<el-form-item label="币种">
<el-select
v-model="shippingTemplateFreight.currency"
placeholder="请选择"
clearable
:disabled="dialogReadonly"
@visible-change="visible => visible && loadShippingTemplateCurrencyOptions()"
>
<el-option
v-for="item in shippingTemplateCurrencyOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-form>
</template>
<el-form
v-else
:model="shippingTemplateFreight"
label-position="right"
label-width="auto"
class="business-crud-page__freight-form business-crud-page__freight-form--non-road"
>
<el-form-item label="运费">
<el-input
v-model="shippingTemplateFreight.freightAmount"
placeholder="请输入"
:disabled="dialogReadonly"
@input="
value =>
handleShippingTemplateFreightNumberInput(
shippingTemplateFreight,
'freightAmount',
value
)
"
>
<template #suffix>{{ shippingTemplateCurrencyLabel }}</template>
</el-input>
</el-form-item>
<el-form-item label="数量">
<el-input :model-value="transportCargoQuantityTotal" disabled />
</el-form-item>
<el-form-item label="其他运费合计">
<el-input
v-model="shippingTemplateFreight.otherFreightAmount"
placeholder="请输入"
:disabled="dialogReadonly"
@input="
value =>
handleShippingTemplateFreightNumberInput(
shippingTemplateFreight,
'otherFreightAmount',
value
)
"
>
<template #suffix>{{ shippingTemplateCurrencyLabel }}</template>
</el-input>
</el-form-item>
<el-form-item label="币种">
<el-select
v-model="shippingTemplateFreight.currency"
placeholder="请选择"
clearable
:disabled="dialogReadonly"
@visible-change="visible => visible && loadShippingTemplateCurrencyOptions()"
>
<el-option
v-for="item in shippingTemplateCurrencyOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-form>
</section>
</div>
</template>
<template #organizationName-form>
<el-cascader
v-model="selectedOrganizationId"
class="business-crud-page__field"
:placeholder="config.organizationPlaceholder || '请选择所属组织'"
:options="organizationOptions"
:props="organizationCascaderProps"
clearable
filterable
:show-all-levels="false"
:disabled="dialogReadonly"
@visible-change="visible => visible && loadOrganizationOptions()"
@change="handleOrganizationChange"
/>
</template>
<template #applyLimit-form>
<el-input
v-model="form.applyLimit"
class="business-crud-page__field"
placeholder="请输入申请临时额度"
:disabled="dialogReadonly"
@input="value => handleAmountInput('applyLimit', value)"
/>
</template>
<template #contractPeriod-form>
<div class="business-crud-page__date-range">
<el-date-picker
v-model="form.startDate"
class="business-crud-page__date"
type="date"
placeholder="YYYY-MM-DD"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
:disabled="dialogReadonly"
@change="handleContractDateChange"
/>
<span class="business-crud-page__date-separator">至</span>
<el-date-picker
v-model="form.endDate"
class="business-crud-page__date"
type="date"
placeholder="YYYY-MM-DD"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
:disabled="dialogReadonly"
@change="handleContractDateChange"
/>
</div>
</template>
<template #copyCount-form>
<div class="business-crud-page__inline-number">
<el-input
v-model="form.copyCount"
placeholder="请输入"
:disabled="dialogReadonly"
@input="value => handleIntegerInput('copyCount', value)"
/>
<span>份</span>
</div>
</template>
<template #paymentDays-form>
<div class="business-crud-page__inline-number">
<el-input
v-model="form.paymentDays"
placeholder="请输入"
:disabled="dialogReadonly"
@input="value => handleIntegerInput('paymentDays', value)"
/>
<span>天</span>
</div>
</template>
<template #contractFileTitle-form>
<div class="dialog-section-title">合同文件</div>
</template>
<template #contractFileJson-form>
<div class="business-crud-page__attachment">
<div class="business-crud-page__attachment-head">
<vehicle-attachment-upload
v-model="contractFileRows"
:readonly="dialogReadonly"
:file-types="attachmentFileTypes"
:max-size="500"
:show-tip="false"
:show-file-list="false"
button-text="上传附件"
@change="handleContractFileChange"
/>
<el-button
type="primary"
:disabled="!contractFileRows.length"
@click="handleContractFileBatchDownload"
>
批量下载
</el-button>
</div>
<el-table
:data="contractFileRows"
border
class="business-crud-page__attachment-table"
@selection-change="handleContractFileSelectionChange"
>
<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="240" align="center" show-overflow-tooltip>
<template #default="{ row }">{{ row.originalName || row.name }}</template>
</el-table-column>
<el-table-column label="文件大小" width="120" 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="170"
align="center"
sortable
/>
<el-table-column label="操作" width="120" align="center" fixed="right">
<template #default="{ row, $index }">
<el-link v-if="!dialogReadonly" type="danger" @click="removeContractFile($index)">
删除
</el-link>
<el-link v-else type="primary" @click="downloadAttachment(row)"> 下载 </el-link>
</template>
</el-table-column>
</el-table>
</div>
</template>
<template #billingInfoTitle-form>
<div class="dialog-section-title">计费信息</div>
</template>
<template #billingPlanJson-form>
<div class="business-crud-page__billing">
<div class="business-crud-page__billing-head">
<div class="business-crud-page__billing-switch">
<span>自动生成费用明细</span>
<el-radio-group v-model="form.billingEnabled" :disabled="dialogReadonly">
<el-radio :label="1">开启</el-radio>
<el-radio :label="0">关闭</el-radio>
</el-radio-group>
</div>
<el-button v-if="!dialogReadonly" type="primary" @click="openBillingPlan()">
添加
</el-button>
</div>
<el-table :data="billingPlanRows" border class="business-crud-page__billing-table">
<el-table-column type="index" label="序号" width="80" align="center" />
<el-table-column prop="planName" label="方案名称" min-width="220" align="center" />
<el-table-column label="默认方案" width="140" align="center">
<template #default="{ row }">{{ row.defaultPlan ? '是' : '' }}</template>
</el-table-column>
<el-table-column
prop="remark"
label="备注"
min-width="260"
align="center"
show-overflow-tooltip
/>
<el-table-column label="操作" width="180" align="center" fixed="right">
<template #default="{ row, $index }">
<el-link type="primary" @click="openBillingPlan(row, $index)"> 编辑 </el-link>
<el-link v-if="!dialogReadonly" type="danger" @click="removeBillingPlan($index)">
删除
</el-link>
</template>
</el-table-column>
</el-table>
</div>
</template>
<template #settlementRuleTitle-form>
<div class="dialog-section-title">结算生成规则</div>
</template>
<template #settlementRuleJson-form>
<div class="business-crud-page__settlement-rule">
<div class="business-crud-page__settlement-switch">
<span>自动生成结算单</span>
<el-radio-group v-model="settlementRuleForm.autoGenerate" :disabled="dialogReadonly">
<el-radio :label="1">开启</el-radio>
<el-radio :label="0">关闭</el-radio>
</el-radio-group>
</div>
<el-form
v-if="settlementRuleEnabled"
:model="settlementRuleForm"
label-position="right"
label-width="auto"
class="business-crud-page__module-form business-crud-page__module-form--three"
>
<el-form-item label="账单起始日期" required :error="settlementRuleErrors.billStartDate">
<el-date-picker
v-model="settlementRuleForm.billStartDate"
type="date"
placeholder="请选择账单起始日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
:disabled="dialogReadonly"
@change="clearSettlementRuleError('billStartDate')"
/>
</el-form-item>
<el-form-item label="结算类型" required :error="settlementRuleErrors.settlementType">
<el-select
v-model="settlementRuleForm.settlementType"
placeholder="请选择结算类型"
:disabled="dialogReadonly"
@change="handleSettlementTypeChange"
>
<el-option
v-for="item in settlementTypeOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="showSettlementBillCycleType"
label="账单周期类型"
required
:error="settlementRuleErrors.billCycleType"
>
<el-select
v-model="settlementRuleForm.billCycleType"
placeholder="请选择账单周期类型"
:disabled="dialogReadonly"
@change="handleSettlementBillCycleTypeChange"
>
<el-option
v-for="item in billCycleTypeOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="showSettlementBillCutoffDay"
label="账单截单日"
required
:error="settlementRuleErrors.billCutoffDay"
>
<el-select
v-model="settlementRuleForm.billCutoffDay"
placeholder="请选择账单截单日"
:disabled="dialogReadonly"
@change="clearSettlementRuleError('billCutoffDay')"
>
<el-option
v-for="item in billCutoffDayOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="showSettlementCycleDays"
label="周期天数"
required
:error="settlementRuleErrors.cycleDays"
>
<el-select
v-model="settlementRuleForm.cycleDays"
placeholder="请选择周期天数"
:disabled="dialogReadonly"
@change="clearSettlementRuleError('cycleDays')"
>
<el-option
v-for="item in cycleDayOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-form>
</div>
</template>
<template #reconciliationTitle-form>
<div class="dialog-section-title">对账配置</div>
</template>
<template #reconciliationJson-form>
<el-form
:model="reconciliationForm"
label-position="right"
label-width="auto"
class="business-crud-page__module-form business-crud-page__module-form--two"
>
<el-form-item label="是否跳过对账" required>
<el-select
v-model="reconciliationForm.skipReconciliation"
placeholder="请选择是否跳过对账"
:disabled="dialogReadonly"
>
<el-option label="是" :value="1" />
<el-option label="否" :value="0" />
</el-select>
</el-form-item>
<el-form-item label="对账模式" required>
<el-select
v-model="reconciliationForm.reconciliationMode"
placeholder="请选择对账模式"
:disabled="dialogReadonly"
>
<el-option
v-for="item in reconciliationModeOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
</el-form>
</template>
<template #attachmentTitle-form>
<div class="dialog-section-title">{{ config.attachmentTitle || '其它附件' }}</div>
</template>
<template #attachmentsJson-form>
<div class="business-crud-page__attachment">
<div class="business-crud-page__attachment-head">
<vehicle-attachment-upload
v-model="attachmentRows"
:readonly="dialogReadonly"
:file-types="attachmentFileTypes"
:max-size="500"
:show-tip="false"
:show-file-list="false"
button-text="上传附件"
@change="handleAttachmentChange"
/>
<el-button
type="primary"
:disabled="!attachmentRows.length"
@click="handleBatchDownload"
>
批量下载
</el-button>
</div>
<el-table
:data="attachmentRows"
border
class="business-crud-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="center" show-overflow-tooltip>
<template #default="{ row }">{{ row.originalName || row.name }}</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>
</template>
<template #changeRecordTitle-form>
<div class="dialog-section-title">变更记录</div>
</template>
<template #changeRecordJson-form>
<el-table :data="changeRecordRows" border class="business-crud-page__change-table">
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column
prop="changeDate"
label="变更日期"
min-width="150"
align="center"
sortable
/>
<el-table-column prop="handlerUserName" label="经办人" min-width="140" align="center" />
<el-table-column prop="changeType" label="变更类型" min-width="160" align="center" />
<el-table-column
prop="changeReason"
label="变更原因"
min-width="240"
align="center"
show-overflow-tooltip
/>
<el-table-column prop="statusName" label="状态" min-width="140" align="center" />
<el-table-column label="操作" width="120" align="center" fixed="right">
<template #default="{ row }">
<el-link type="primary" @click="handleChangeRecordFlow(row)"> 流程 </el-link>
</template>
</el-table-column>
</el-table>
</template>
<template #menu-form-before>
<div
v-if="showFooterFreightSummary && !dialogReadonly"
class="business-crud-page__shipping-template-footer-summary"
>
<span>
运费合计:<strong>¥{{ formatFooterFreightTotal }}</strong>
</span>
<el-link
v-if="hasProjectProcessConfig"
type="primary"
@click="openShippingTemplateProcessConfig"
>
查看过程配置
</el-link>
</div>
<el-button
v-if="config.enableDraftSave && !dialogReadonly"
type="primary"
plain
:loading="draftSaveLoading"
@click="handleDraftSave"
>
{{ config.draftSaveText || '保存' }}
</el-button>
</template>
<template #menu-form>
<div v-if="showTransportPlanCreateActions" class="business-crud-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>
<div v-if="showContractCreateActions" class="business-crud-page__contract-actions">
<el-button
type="primary"
plain
:loading="contractCreateActionLoading === 'draft'"
:disabled="Boolean(contractCreateActionLoading)"
@click="handleContractCreateAction('draft')"
>
保存
</el-button>
<el-button
type="primary"
:loading="contractCreateActionLoading === 'temporary'"
:disabled="Boolean(contractCreateActionLoading)"
@click="handleContractCreateAction('temporary')"
>
提交临时合同
</el-button>
<el-button
type="primary"
:loading="contractCreateActionLoading === 'formal'"
:disabled="Boolean(contractCreateActionLoading)"
@click="handleContractCreateAction('formal')"
>
提交正式合同
</el-button>
</div>
</template>
<template #contractStage="{ row }">
<el-tag :type="stageTagType(row.contractStage)">
{{ displayStatus(row, 'contractStage') }}
</el-tag>
</template>
<template #menu="{ row, index }">
<template v-if="config.customMenuActions">
<el-link
v-for="action in menuActions(row)"
:key="action.key"
:type="action.type || 'primary'"
@click="handleMenuAction(action, row, index)"
>
{{ action.label }}
</el-link>
</template>
<template v-else>
<el-link type="primary" v-if="showDetailButton(row)" @click="openDetail(row)"
>详情</el-link
>
<el-link
type="primary"
v-if="hasPermission(`${config.permission}_view`) && !showDetailButton(row)"
@click="$refs.crud.rowView(row, index)"
>
查看
</el-link>
<el-link type="primary" v-if="canEdit(row)" @click="$refs.crud.rowEdit(row, index)">
编辑
</el-link>
<el-link type="primary" v-if="canCopy(row)" @click="handleCopy(row)"> 复制 </el-link>
<el-link type="primary" v-if="canEnable(row)" @click="handleAction('enable', row)">
启用
</el-link>
<el-link type="primary" v-if="canDisable(row)" @click="handleAction('disable', row)">
停用
</el-link>
<el-link type="primary" v-if="canReassign(row)" @click="handleAction('reassign', row)">
重新派单
</el-link>
<el-link type="primary" v-if="canCancel(row)" @click="handleAction('cancel', row)">
取消
</el-link>
<el-link type="primary" v-if="canComplete(row)" @click="handleAction('complete', row)">
{{ completeActionLabel }}
</el-link>
<el-link
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>
</template>
</template>
</avue-crud>
<empty-pagination
: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="[
'business-crud-page__detail-dialog',
{ 'is-transport-plan-detail': isTransportPlanDetailLayout },
]"
>
<div v-loading="detailLoading" class="business-crud-page__detail-content">
<template v-if="isTransportPlanDetailLayout">
<section class="business-crud-page__transport-plan-detail-head">
<div class="business-crud-page__transport-plan-detail-title-row">
<h2>
{{ detailRow.planName || detailRow.planNo || '运输计划' }}
<span>|</span>
{{ detailRow.planNo || '-' }}
</h2>
<el-tag class="is-dispatching" effect="light">
{{ displayStatus(detailRow, 'businessStatus') }}
</el-tag>
<el-tag type="primary" effect="light">
{{
detailRow.transportTypeName ||
formatTransportPlanDetailTransportType(detailRow.transportType)
}}
</el-tag>
</div>
<div class="business-crud-page__transport-plan-detail-overview">
<div class="business-crud-page__transport-plan-detail-fields">
<div class="business-crud-page__transport-plan-detail-field">
<span>客户</span>
<strong>{{ detailRow.customerName || '-' }}</strong>
</div>
<div class="business-crud-page__transport-plan-detail-field">
<span>项目</span>
<strong>{{ detailRow.projectName || '-' }}</strong>
</div>
<div class="business-crud-page__transport-plan-detail-field">
<span>合同编号</span>
<strong>{{ detailRow.contractNo || detailRow.contractName || '-' }}</strong>
</div>
<div class="business-crud-page__transport-plan-detail-field">
<span>货物信息</span>
<strong class="is-normal">{{ transportPlanGoodsText }}</strong>
</div>
<div class="business-crud-page__transport-plan-detail-field">
<span>计划执行时间</span>
<strong class="is-normal">{{ transportPlanDateRange }}</strong>
</div>
<div class="business-crud-page__transport-plan-detail-field is-attachments">
<span>附件</span>
<div class="business-crud-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="business-crud-page__transport-plan-detail-route">
<div class="business-crud-page__transport-plan-detail-route-point">
<b class="is-start">起</b>
<div>
<strong>{{ detailRow.departureName || detailRow.departureAddress || '-' }}</strong>
<p>{{ formatTransportPlanProvinceCityDistrict(detailRow.departureAddress) }}</p>
<p>{{ [detailRow.departureContact, detailRow.departurePhone].filter(Boolean).join(' - ') || '-' }}</p>
</div>
</div>
<span class="business-crud-page__transport-plan-detail-route-line" />
<div class="business-crud-page__transport-plan-detail-route-point">
<b class="is-end">终</b>
<div>
<strong>{{ detailRow.arrivalName || detailRow.arrivalAddress || '-' }}</strong>
<p>{{ formatTransportPlanProvinceCityDistrict(detailRow.arrivalAddress) }}</p>
<p>{{ [detailRow.arrivalContact, detailRow.arrivalPhone].filter(Boolean).join(' - ') || '-' }}</p>
</div>
</div>
</div>
</div>
</section>
<section class="business-crud-page__transport-plan-waybill-card">
<div class="business-crud-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="business-crud-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="192" />
<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="120">
<template #default="{ row }">
<span
:class="['business-crud-page__transport-plan-waybill-status', row.statusClass]"
>
{{ row.statusText || '-' }}
</span>
</template>
</el-table-column>
<el-table-column label="操作" width="128" fixed="right">
<template #default="{ row }">
<el-link
type="primary"
@click="openTransportPlanWaybillDetail(row)"
>
详情
</el-link>
</template>
</el-table-column>
</el-table>
<div class="business-crud-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-if="isWaybillDetailLayout">
<section-card class="business-crud-page__waybill-detail-summary">
<div class="business-crud-page__waybill-heading">
<strong>运单详情</strong>
<span>{{ detailRow.waybillNo || '-' }}</span>
<el-tag type="success">{{ displayStatus(detailRow, 'businessStatus') }}</el-tag>
<el-tag type="primary">{{ waybillTransportMode(detailRow) || '-' }}</el-tag>
<el-link
v-if="detailRow.id"
class="business-crud-page__waybill-route-change-link"
type="primary"
@click="openWaybillRouteChangeDialog"
>
变更运输路线
</el-link>
</div>
<div class="business-crud-page__waybill-summary-grid">
<div
v-for="item in waybillDetailSummaryFields"
:key="item[0]"
class="business-crud-page__waybill-summary-item"
:class="{ 'is-plan': item[0] === 'planName' }"
>
<span class="business-crud-page__waybill-label">{{ item[1] }}</span>
<span :class="['business-crud-page__waybill-value', { 'is-link': item[2] }]">
{{ formatDetailValue(detailRow, item[0]) }}
</span>
</div>
<div class="business-crud-page__waybill-route">
<div class="business-crud-page__waybill-route-head">
<b>起</b
><strong>{{
detailRow.departureName || detailRow.departureAddress || '-'
}}</strong
><b class="end">终</b
><strong>{{ detailRow.arrivalName || detailRow.arrivalAddress || '-' }}</strong>
</div>
<div class="business-crud-page__waybill-route-text">
<el-tooltip :content="detailRow.departureAddress || '-'" placement="top">
<span>{{ formatTransportPlanProvinceCityDistrict(detailRow.departureAddress) }}</span>
</el-tooltip>
<span> → </span>
<el-tooltip :content="detailRow.arrivalAddress || '-'" placement="top">
<span>{{ formatTransportPlanProvinceCityDistrict(detailRow.arrivalAddress) }}</span>
</el-tooltip>
</div>
</div>
<div class="business-crud-page__waybill-attachments">
<span class="business-crud-page__waybill-label">附件</span>
<div>
<el-link
v-for="file in attachmentRows"
:key="file.url || file.name || file.originalName"
type="primary"
@click="previewAttachment(file)"
>
{{ file.originalName || file.name || '-' }}
</el-link>
<span v-if="!attachmentRows.length">-</span>
</div>
</div>
</div>
</section-card>
<section-card title="货物信息">
<el-table
:data="
transportCargoRows.length ? transportCargoRows : parseJsonArray(detailRow.goodsJson)
"
border
height="220"
>
<el-table-column type="index" label="序号" width="70" />
<el-table-column prop="cargoName" label="货物名称" min-width="150" />
<el-table-column prop="cargoType" label="货物类型" min-width="130" />
<el-table-column prop="packageType" label="包装" min-width="100" />
<el-table-column prop="quantity" label="数量" min-width="100" />
<el-table-column prop="quantityUnit" label="数量单位" min-width="110" />
<el-table-column prop="materialCode" label="物料编码" min-width="130" />
<el-table-column prop="deviceCode" label="设备编码" min-width="130" />
<el-table-column prop="brand" label="品牌" min-width="110" />
<el-table-column label="规格型号" min-width="140"
><template #default="{ row }">{{
[row.specification, row.model].filter(Boolean).join('/') || '-'
}}</template></el-table-column
>
<el-table-column prop="remark" label="备注" min-width="180" />
</el-table>
</section-card>
<section-card title="承运信息">
<div class="business-crud-page__waybill-carrier-grid">
<div class="business-crud-page__waybill-detail-field">
<span>承运类型</span><strong>{{ detailRow.carrierType || '-' }}</strong>
</div>
<div
v-if="waybillIsCarrier(detailRow)"
class="business-crud-page__waybill-detail-field"
>
<span>承运商</span><strong>{{ detailRow.carrierName || '-' }}</strong>
</div>
<div
v-if="waybillIsRoadTransport(detailRow)"
class="business-crud-page__waybill-detail-field"
>
<span>司机</span
><strong
>{{ detailRow.driverName || '-' }} {{ detailRow.driverPhone || '' }}</strong
>
</div>
<div class="business-crud-page__waybill-detail-field">
<span>{{ waybillVehicleNoLabel(detailRow) }}</span>
<strong>{{ detailRow.vehicleNo || '-' }}</strong>
</div>
<div
v-if="waybillIsNonRoadTransport(detailRow)"
class="business-crud-page__waybill-detail-field"
>
<span>船长</span><strong>{{ detailRow.captainName || '-' }}</strong>
</div>
<div
v-if="waybillIsNonRoadTransport(detailRow) && waybillIsCarrier(detailRow)"
class="business-crud-page__waybill-detail-field"
>
<span>联系电话</span><strong>{{ detailRow.driverPhone || '-' }}</strong>
</div>
<div
v-if="waybillIsNonRoadTransport(detailRow)"
class="business-crud-page__waybill-detail-field"
>
<span>箱号</span><strong>{{ detailRow.containerNo || '-' }}</strong>
</div>
<div
v-if="waybillIsNonRoadTransport(detailRow)"
class="business-crud-page__waybill-detail-field"
>
<span>舱位</span><strong>{{ detailRow.cabinNo || '-' }}</strong>
</div>
<div
v-if="waybillIsRoadTransport(detailRow) && !waybillIsCarrier(detailRow)"
class="business-crud-page__waybill-detail-field"
>
<span>挂车车牌号</span><strong>{{ detailRow.trailerVehicleNo || '-' }}</strong>
</div>
<div
v-if="waybillIsRoadTransport(detailRow) && !waybillIsCarrier(detailRow)"
class="business-crud-page__waybill-detail-field"
>
<span>押运人</span><strong>{{ detailRow.escortName || '-' }}</strong>
</div>
<div
v-if="waybillIsRoadTransport(detailRow) && !waybillIsCarrier(detailRow)"
class="business-crud-page__waybill-detail-field"
>
<span>押运人手机号</span><strong>{{ detailRow.escortPhone || '-' }}</strong>
</div>
<div
v-if="waybillIsRoadTransport(detailRow) && !waybillIsCarrier(detailRow)"
class="business-crud-page__waybill-detail-field"
>
<span>里程(km)</span><strong>{{ detailRow.mileage || '-' }}</strong>
</div>
<div
class="business-crud-page__waybill-detail-field business-crud-page__waybill-detail-field--remark"
>
<span>备注</span
><strong>{{ detailRow.taskRemark || detailRow.remark || '-' }}</strong>
</div>
</div>
</section-card>
<section-card title="运费信息">
<div
class="business-crud-page__waybill-carrier-grid business-crud-page__waybill-fee-grid"
>
<div class="business-crud-page__waybill-detail-field">
<span>单价</span>
<strong>
<template v-if="waybillUnitPriceWithCurrency(detailRow).value !== '-'">
<span class="business-crud-page__waybill-fee-value">
{{ waybillUnitPriceWithCurrency(detailRow).value }}
</span>
<span v-if="waybillUnitPriceWithCurrency(detailRow).unit">
{{ ' ' + waybillUnitPriceWithCurrency(detailRow).unit }}
</span>
</template>
<template v-else>-</template>
</strong>
</div>
<div class="business-crud-page__waybill-detail-field">
<span>运费</span>
<strong>
<template v-if="waybillAmountWithCurrency(detailRow, 'freight').value !== '-'">
<span class="business-crud-page__waybill-fee-value">{{ waybillAmountWithCurrency(detailRow, 'freight').value }}</span>元
</template>
<template v-else>-</template>
</strong>
</div>
<div class="business-crud-page__waybill-detail-field">
<span>其他费用合计</span>
<strong>
<template v-if="waybillAmountWithCurrency(detailRow, 'otherFeeTotal').value !== '-'">
<span class="business-crud-page__waybill-fee-value">{{ waybillAmountWithCurrency(detailRow, 'otherFeeTotal').value }}</span>元
</template>
<template v-else>-</template>
</strong>
</div>
<div class="business-crud-page__waybill-detail-field">
<span>运费合计</span>
<strong>
<template v-if="waybillAmountWithCurrency(detailRow, 'freightTotal').value !== '-'">
<span class="business-crud-page__waybill-fee-value">{{ waybillAmountWithCurrency(detailRow, 'freightTotal').value }}</span>元
</template>
<template v-else>-</template>
</strong>
</div>
</div>
</section-card>
<div class="business-crud-page__waybill-detail-bottom">
<section-card title="执行详情">
<el-tabs v-model="waybillProcessDetailTab" type="border-card">
<el-tab-pane label="打卡详情" name="punch">
<el-timeline v-if="waybillDetailProcessNodes.length">
<el-timeline-item
v-for="(node, index) in waybillDetailProcessNodes"
:key="node.key || index"
:timestamp="node.time || ''"
>{{ node.name || node.nodeName || node.label }}</el-timeline-item
>
</el-timeline>
<el-empty v-else description="暂无打卡详情" :image-size="50" />
</el-tab-pane>
<el-tab-pane
v-if="waybillHasRelatedVoucher"
label="批量补录"
name="batchSupplement"
>
<div v-loading="waybillVoucherImagesLoading" class="business-crud-page__voucher-image-grid">
<div
v-for="(image, index) in waybillVoucherImages"
:key="image.id || image.objectKey || index"
class="business-crud-page__voucher-image-item"
:title="image.imageName"
@click="previewWaybillVoucherImage(index)"
>
<el-image :src="image.url" fit="cover" lazy />
</div>
<el-empty
v-if="!waybillVoucherImagesLoading && !waybillVoucherImages.length"
description="暂无凭证图片"
:image-size="50"
/>
</div>
</el-tab-pane>
<!-- <el-tab-pane label="司机上传" name="driverUpload">
<el-empty description="暂无司机上传数据" :image-size="50" />
</el-tab-pane> -->
</el-tabs>
</section-card>
<section-card title="物流轨迹">
<template #extra>
<div class="business-crud-page__waybill-track-actions">
<el-button plain @click="handleWaybillTrackAction('playback')"
>轨迹回放</el-button
>
<el-button plain @click="handleWaybillTrackAction('locate')">实时定位</el-button>
</div>
</template>
<div class="business-crud-page__waybill-map-empty">暂无数据</div>
</section-card>
</div>
</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="business-crud-page__attachment-description"
>
<div
v-for="(row, index) in attachmentRows"
:key="row.id || row.uid || `${attachmentName(row)}-${index}`"
class="business-crud-page__attachment-description-item"
>
<span class="business-crud-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-model="waybillRouteChangeBox"
title="变更运输路线"
append-to-body
destroy-on-close
width="78%"
class="business-crud-page__route-change-dialog"
>
<el-tabs v-model="waybillRouteChangeTab">
<el-tab-pane label="变更路线" name="change">
<section-card title="运单地址">
<el-table :data="waybillRouteChangeRows" border>
<el-table-column type="index" label="序号" width="70" />
<el-table-column prop="waybillNo" label="运单号" min-width="150">
<template #default="{ row }">
<el-link type="primary">{{ row.waybillNo || '-' }}</el-link>
</template>
</el-table-column>
<el-table-column prop="projectName" label="项目名称" min-width="160" />
<el-table-column label="发货地" min-width="310">
<template #default="{ row }">
<div class="business-crud-page__route-change-original">
原:{{ row.originalDepartureAddress || '-' }}
</div>
<el-input
v-model="row.departureAddress"
placeholder="请选择或输入地址"
@input="updateWaybillRouteChangeNodes"
>
<template #suffix>
<el-icon
class="business-crud-page__route-change-location"
@click="openWaybillCommonAddressDialog(row, 'departure')"
>
<Location />
</el-icon>
</template>
</el-input>
</template>
</el-table-column>
<el-table-column label="到货地" min-width="310">
<template #default="{ row }">
<div class="business-crud-page__route-change-original">
原:{{ row.originalArrivalAddress || '-' }}
</div>
<el-input
v-model="row.arrivalAddress"
placeholder="请选择或输入地址"
@input="updateWaybillRouteChangeNodes"
>
<template #suffix>
<el-icon
class="business-crud-page__route-change-location"
@click="openWaybillCommonAddressDialog(row, 'arrival')"
>
<Location />
</el-icon>
</template>
</el-input>
</template>
</el-table-column>
</el-table>
</section-card>
<section-card title="路线编辑">
<template #extra>
<span class="business-crud-page__route-change-hint">拖拽调整顺序</span>
</template>
<div
v-if="waybillRouteChangeNodes.length"
class="business-crud-page__route-change-list"
>
<div
v-for="(node, index) in waybillRouteChangeNodes"
:key="node.key || index"
class="business-crud-page__route-change-item"
draggable="true"
@dragstart="handleWaybillRouteChangeDragStart(index)"
@dragover.prevent
@drop="handleWaybillRouteChangeDrop(index)"
>
<span class="business-crud-page__route-change-type">
{{ waybillRouteChangeTypeText(index) }}
</span>
<span>{{ node.address }}</span>
<span class="business-crud-page__route-change-tags">
<el-tag v-for="tag in node.tags || []" :key="tag" size="small">{{ tag }}</el-tag>
</span>
<el-icon><Rank /></el-icon>
</div>
</div>
<el-empty v-else description="暂无路线" :image-size="60" />
</section-card>
<el-form label-width="auto">
<el-form-item label="变更备注">
<el-input
v-model="waybillRouteChangeRemark"
type="textarea"
:rows="1"
maxlength="200"
show-word-limit
placeholder="请输入"
/>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="变更记录" name="records">
<el-table :data="waybillRouteChangeRecords" border>
<el-table-column prop="changeTime" label="变更时间" min-width="170" />
<el-table-column prop="changeUser" label="变更人" min-width="120" />
<el-table-column prop="content" label="变更内容" min-width="360">
<template #default="{ row }">
<div class="business-crud-page__route-change-record-content">
{{ row.content || '-' }}
</div>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="200" show-overflow-tooltip />
</el-table>
<el-empty
v-if="!waybillRouteChangeRecords.length"
description="暂无变更记录"
:image-size="60"
/>
</el-tab-pane>
</el-tabs>
<template #footer>
<el-button @click="waybillRouteChangeBox = false">关闭</el-button>
<el-button
v-if="waybillRouteChangeTab === 'change'"
type="primary"
:loading="waybillRouteChangeSaving"
@click="confirmWaybillRouteChange"
>
确认变更
</el-button>
</template>
</el-dialog>
<el-dialog
v-model="waybillCommonAddressBox"
title="选择常用地址"
append-to-body
width="900px"
class="business-crud-page__common-address-dialog"
>
<div class="business-crud-page__common-address-toolbar">
<el-input
v-model="waybillCommonAddressQuery.addressName"
clearable
placeholder="请输入地址名称"
@keyup.enter="loadWaybillCommonAddresses"
/>
<el-button type="primary" @click="loadWaybillCommonAddresses">查询</el-button>
</div>
<el-table
v-loading="waybillCommonAddressLoading"
:data="waybillCommonAddressRows"
border
@row-dblclick="selectWaybillCommonAddress"
>
<el-table-column prop="addressName" label="地址名称" min-width="180" />
<el-table-column prop="regionName" label="区域" min-width="180" />
<el-table-column prop="detailAddress" label="详细地址" min-width="260" />
<el-table-column label="操作" width="90">
<template #default="{ row }">
<el-link type="primary" @click="selectWaybillCommonAddress(row)">选择</el-link>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-pagination
v-model:current-page="waybillCommonAddressPage.currentPage"
v-model:page-size="waybillCommonAddressPage.pageSize"
layout="total, prev, pager, next"
:total="waybillCommonAddressPage.total"
@current-change="loadWaybillCommonAddresses"
/>
<el-button @click="waybillCommonAddressBox = false">关闭</el-button>
</template>
</el-dialog>
<el-dialog
v-if="config.enableTransportPlanImport"
v-model="transportPlanImportBox"
:title="config.transportPlanImportTitle || `导入${config.title}`"
append-to-body
width="96%"
class="business-crud-page__transport-plan-import-dialog"
@closed="resetTransportPlanImport"
>
<el-form
ref="transportPlanImportFormRef"
:model="transportPlanImportForm"
:rules="transportPlanImportRules"
label-position="right"
label-width="auto"
class="business-crud-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="business-crud-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="business-crud-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="business-crud-page__import-preview">
<div class="business-crud-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 v-model="transportPlanImportTab" class="business-crud-page__import-preview-tabs">
<el-tab-pane :label="`计划单数(${transportPlanImportRows.length}`" name="all" />
<el-tab-pane :label="`疑似重复(${transportPlanImportDuplicateCount})`" name="duplicate" />
</el-tabs>
<div class="business-crud-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>
<waybill-import-dialog v-if="config.enableWaybillImport" v-model="waybillImportBox" />
<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="business-crud-page__dispatch-dialog"
@closed="resetDispatchDialog"
>
<div v-loading="dispatchLoading" class="business-crud-page__dispatch-shell">
<div class="business-crud-page__dispatch-top">
<div class="business-crud-page__dispatch-heading">
<div class="business-crud-page__dispatch-heading-title">
<span class="business-crud-page__dispatch-heading-name">
{{ dispatchDialogTitle }}
</span>
<span class="business-crud-page__dispatch-heading-no">
{{ dispatchDialogNo || '-' }}
</span>
<el-tag :type="statusTagType(dispatchRow.businessStatus)" effect="light">
{{ dispatchDialogStatusText }}
</el-tag>
<el-tag type="primary" effect="light">
{{ formatDispatchTransportType(dispatchRow.transportType) || '公路整车' }}
</el-tag>
</div>
</div>
<div class="business-crud-page__dispatch-summary">
<section-card title="基本信息" class="business-crud-page__dispatch-summary-card">
<div class="business-crud-page__dispatch-summary-grid">
<div class="business-crud-page__dispatch-summary-item">
<div class="business-crud-page__dispatch-summary-label">客户</div>
<div class="business-crud-page__dispatch-summary-value">
{{ dispatchRow.customerName || '-' }}
</div>
</div>
<div class="business-crud-page__dispatch-summary-item">
<div class="business-crud-page__dispatch-summary-label">项目</div>
<div class="business-crud-page__dispatch-summary-value">
{{ dispatchRow.projectName || '-' }}
</div>
</div>
<div class="business-crud-page__dispatch-summary-item">
<div class="business-crud-page__dispatch-summary-label">合同编号</div>
<div class="business-crud-page__dispatch-summary-value">
{{ dispatchRow.contractName || dispatchRow.contractNo || '-' }}
</div>
</div>
<div class="business-crud-page__dispatch-summary-item">
<div class="business-crud-page__dispatch-summary-label">计划执行时间</div>
<div class="business-crud-page__dispatch-summary-value">
{{
[dispatchRow.planStartDate, dispatchRow.planEndDate]
.filter(Boolean)
.join(' ~ ') || '-'
}}
</div>
</div>
<div class="business-crud-page__dispatch-summary-item is-wide">
<div class="business-crud-page__dispatch-summary-label">货物信息</div>
<div class="business-crud-page__dispatch-summary-value">
{{ dispatchPlanCargoInfo || '-' }}
</div>
</div>
<div
class="business-crud-page__dispatch-summary-item business-crud-page__dispatch-summary-item--attachments"
>
<div class="business-crud-page__dispatch-summary-label">附件</div>
<div class="business-crud-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="business-crud-page__dispatch-attachment-link"
>
{{ dispatchAttachmentLabel(item) }}
</el-link>
<span v-if="!dispatchAttachmentRows.length">-</span>
</div>
</div>
</div>
</section-card>
<section-card title="收发货路线" class="business-crud-page__dispatch-route-card">
<div class="business-crud-page__dispatch-route">
<div class="business-crud-page__dispatch-route-item">
<span class="business-crud-page__dispatch-route-badge is-start">起</span>
<div class="business-crud-page__dispatch-route-body">
<div class="business-crud-page__dispatch-route-title">
{{ parseDispatchRoutePoint(dispatchRow, 'start').name || '-' }}
</div>
<div class="business-crud-page__dispatch-route-address">
{{ parseDispatchRoutePoint(dispatchRow, 'start').address || '-' }}
</div>
<div class="business-crud-page__dispatch-route-contact">
{{ parseDispatchRoutePoint(dispatchRow, 'start').contact || '-' }}
</div>
</div>
</div>
<div class="business-crud-page__dispatch-route-vehicle">
<el-icon><Van /></el-icon>
</div>
<div class="business-crud-page__dispatch-route-item">
<span class="business-crud-page__dispatch-route-badge is-end">终</span>
<div class="business-crud-page__dispatch-route-body">
<div class="business-crud-page__dispatch-route-title">
{{ parseDispatchRoutePoint(dispatchRow, 'end').name || '-' }}
</div>
<div class="business-crud-page__dispatch-route-address">
{{ parseDispatchRoutePoint(dispatchRow, 'end').address || '-' }}
</div>
<div class="business-crud-page__dispatch-route-contact">
{{ parseDispatchRoutePoint(dispatchRow, 'end').contact || '-' }}
</div>
</div>
</div>
</div>
</section-card>
</div>
</div>
<section-card class="business-crud-page__dispatch-list-card">
<template #title>
<span>待调度列表</span>
<span class="business-crud-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="business-crud-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="220"
align="center"
show-overflow-tooltip
>
<template #default="{ row }">
<div>
<div>{{ row.driverName || '-' }}</div>
<div>{{ row.driverPhone || '-' }}</div>
</div>
</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="220" align="center" fixed="right">
<template #default="{ row, $index }">
<div class="business-crud-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="business-crud-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="business-crud-page__dispatch-item-dialog"
@closed="resetDispatchItemDialog"
>
<el-form
:model="dispatchItemForm"
label-position="right"
label-width="auto"
class="business-crud-page__dispatch-item-form"
>
<div class="dialog-section-title">收发货信息</div>
<div class="business-crud-page__dispatch-shipping-form">
<el-form-item label="发货地址" required>
<div class="business-crud-page__route-address-row">
<el-input
v-model="dispatchItemForm.departureName"
:placeholder="dispatchStationNamePlaceholder"
:suffix-icon="dispatchStationMode ? Search : undefined"
/>
<el-input
v-model="dispatchItemForm.departureAddress"
:placeholder="dispatchAddressDetailPlaceholder"
:suffix-icon="dispatchStationMode ? undefined : Location"
/>
<span class="business-crud-page__route-label">联系人</span>
<el-input v-model="dispatchItemForm.departureContact" placeholder="请输入" />
<span class="business-crud-page__route-label">联系方式</span>
<el-input v-model="dispatchItemForm.departurePhone" placeholder="请输入" />
</div>
</el-form-item>
<el-form-item label="收货地址" required>
<div class="business-crud-page__route-address-row">
<el-input
v-model="dispatchItemForm.arrivalName"
:placeholder="dispatchStationNamePlaceholder"
:suffix-icon="dispatchStationMode ? Search : undefined"
/>
<el-input
v-model="dispatchItemForm.arrivalAddress"
:placeholder="dispatchAddressDetailPlaceholder"
:suffix-icon="dispatchStationMode ? undefined : Location"
/>
<span class="business-crud-page__route-label">联系人</span>
<el-input v-model="dispatchItemForm.arrivalContact" placeholder="请输入" />
<span class="business-crud-page__route-label">联系方式</span>
<el-input v-model="dispatchItemForm.arrivalPhone" placeholder="请输入" />
</div>
</el-form-item>
</div>
<div
class="dialog-section-title dialog-section-title--action"
style="display: flex; width: 100%; justify-content: space-between"
>
<span class="business-crud-page__dispatch-task-title">任务信息</span>
<el-segmented
v-model="dispatchItemForm.taskEntryMode"
:options="dispatchTaskEntryModeOptions"
class="business-crud-page__dispatch-entry-mode"
@change="handleDispatchTaskEntryModeChange"
/>
</div>
<template v-if="dispatchItemForm.taskEntryMode === 'full'">
<div class="dialog-section-title dialog-section-title--action business-crud-page__goods-title">
<span>货物信息</span>
<div class="business-crud-page__section-actions">
<el-link type="primary" @click="cargoImportBox = true">导入货物</el-link>
<el-link type="primary" @click="openCommonCargoDialog">常用货物</el-link>
</div>
</div>
<div class="business-crud-page__cargo-wrap">
<el-table
:data="dispatchItemCargoRows"
border
class="business-crud-page__cargo-table business-crud-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="business-crud-page__required-column">货物类型</span></template>
<template #default="{ row }">
<el-select
v-model="row.cargoType"
placeholder="请选择货物类型"
clearable
filterable
@change="value => handleDispatchConfiguredCargoTypeChange(row, value)"
>
<el-option
v-for="item in getDispatchConfiguredCargoTypeOptions()"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</el-table-column>
<el-table-column min-width="240" align="center">
<template #header><span class="business-crud-page__required-column">货物名称</span></template>
<template #default="{ row }">
<el-select
v-model="row.cargoName"
placeholder="请选择货物名称"
clearable
filterable
@change="value => handleDispatchConfiguredCargoNameChange(row, value)"
>
<el-option
v-for="item in getDispatchConfiguredCargoNameOptions(row)"
: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-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 }">
{{ formatDispatchQuantity(dispatchItemRemainingQuantity(row)) }}
</template>
</el-table-column>
<el-table-column min-width="130" align="center">
<template #header><span class="business-crud-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="business-crud-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="business-crud-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="business-crud-page__dispatch-item-grid business-crud-page__dispatch-item-grid--full">
<template v-for="(cargo, index) in dispatchItemCargoRows" :key="cargo._key || index">
<el-form-item :label="`单价${index + 1}`">
<el-input v-model="cargo.unitPrice" placeholder="请输入" @input="value => handleDispatchCargoNumberInput(cargo, 'unitPrice', value)">
<template #append><el-select v-model="cargo.priceUnit" class="business-crud-page__dispatch-unit-select" placeholder="元/吨" @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="`数量合计${index + 1}`"><el-input :model-value="formatDispatchQuantity(cargo.quantity)" disabled><template #append>{{ cargo.quantityUnit || '吨' }}</template></el-input></el-form-item>
<el-form-item :label="`运费${index + 1}`"><el-input :model-value="dispatchCargoFreightAmount(cargo)" disabled><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>
<el-form-item label="币种"><el-select v-model="dispatchItemForm.freightCurrency" placeholder="请选择" clearable @visible-change="visible => visible && loadShippingTemplateCurrencyOptions()"><el-option v-for="item in shippingTemplateCurrencyOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item>
</div>
</template>
<template v-else>
<div
class="business-crud-page__dispatch-item-grid business-crud-page__dispatch-item-grid--compact"
>
<el-form-item
label="承运类型"
required
class="business-crud-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 v-if="dispatchItemForm.carrierType === '承运商'" label="承运商" required>
<el-select
v-model="dispatchItemForm.carrierName"
placeholder="请输入"
filterable
clearable
:suffix-icon="Search"
:loading="taskCarrierLoading"
@visible-change="visible => visible && loadDispatchCarrierOptions()"
@change="handleDispatchCarrierChange"
>
<el-option
v-for="item in taskCarrierOptions"
:key="
item.id || item.customerName || item.carrierName || item.fullName || item.name
"
:label="item.customerName || item.carrierName || item.fullName || item.name"
: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-input v-model="dispatchItemForm.escortName" placeholder="请输入" />
</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-select
v-model="dispatchItemForm.cargoType"
placeholder="请选择货物类型"
clearable
filterable
@change="handleDispatchConfiguredCargoTypeChange(dispatchItemForm, $event)"
>
<el-option
v-for="item in getDispatchConfiguredCargoTypeOptions()"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="货物名称" required>
<el-select
v-model="dispatchItemForm.cargoName"
placeholder="请选择货物名称"
clearable
filterable
@change="handleDispatchConfiguredCargoNameChange(dispatchItemForm, $event)"
>
<el-option
v-for="item in getDispatchConfiguredCargoNameOptions(dispatchItemForm)"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</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="business-crud-page__dispatch-unit-select"
>
<el-option
v-for="item in quantityUnitOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</template>
</el-input>
<div class="business-crud-page__dispatch-quantity-hint">
剩余数量:{{
formatDispatchQuantity(dispatchItemRemainingQuantity(dispatchItemForm))
}}
{{ dispatchItemForm.quantityUnit || '吨' }}
</div>
</el-form-item>
<el-form-item label="里程(公里)" :required="dispatchItemForm.carrierType !== '承运商'">
<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="business-crud-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.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="币种">
<el-select
v-model="dispatchItemForm.freightCurrency"
placeholder="请选择"
clearable
@visible-change="visible => visible && loadShippingTemplateCurrencyOptions()"
>
<el-option
v-for="item in shippingTemplateCurrencyOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="备注" class="business-crud-page__dispatch-item-span-3">
<el-input
v-model="dispatchItemForm.remark"
maxlength="200"
show-word-limit
placeholder="请输入,200字以内"
/>
</el-form-item>
</div>
</template>
<div
v-if="dispatchItemForm.taskEntryMode === 'full'"
class="dialog-section-title business-crud-page__dispatch-carrier-title"
style="display: flex; width: 100%; margin-top: 16px"
>
承运信息
</div>
<div
v-if="dispatchItemForm.taskEntryMode === 'full'"
class="business-crud-page__dispatch-item-grid business-crud-page__dispatch-item-grid--carrier"
>
<el-form-item label="承运类型" required class="business-crud-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 v-if="dispatchItemForm.carrierType === '承运商'" label="承运商" required>
<el-select
v-model="dispatchItemForm.carrierName"
placeholder="请输入"
filterable
clearable
:suffix-icon="Search"
:loading="taskCarrierLoading"
@visible-change="visible => visible && loadDispatchCarrierOptions()"
@change="handleDispatchCarrierChange"
>
<el-option
v-for="item in taskCarrierOptions"
:key="
item.id || item.customerName || item.carrierName || item.fullName || item.name
"
:label="item.customerName || item.carrierName || item.fullName || item.name"
: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-input v-model="dispatchItemForm.escortName" placeholder="请输入" />
</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)"
required
>
<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="business-crud-page__dispatch-item-span-2">
<el-input
v-model="dispatchItemForm.remark"
type="textarea"
:rows="2"
maxlength="200"
show-word-limit
placeholder="请输入,200字以内"
/>
</el-form-item>
</div>
<div class="dialog-section-title">附件</div>
<div class="business-crud-page__attachment">
<div class="business-crud-page__attachment-head">
<vehicle-attachment-upload
v-model="dispatchItemAttachmentRows"
:file-types="attachmentFileTypes"
:max-size="500"
:show-tip="false"
:show-file-list="false"
button-text="上传附件"
@change="handleDispatchItemAttachmentChange"
/>
<el-button
type="primary"
:disabled="!dispatchItemAttachmentRows.length"
@click="handleDispatchItemBatchDownload"
>
批量下载
</el-button>
</div>
<el-table
:data="dispatchItemAttachmentRows"
border
class="business-crud-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>
</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="billingPlanMode === 'edit' ? '编辑计费方案' : '添加计费方案'"
append-to-body
v-model="billingPlanBox"
width="90%"
class="business-crud-page__billing-dialog"
>
<el-form
ref="billingPlanFormRef"
:model="billingPlanForm"
:rules="billingPlanRules"
label-position="right"
label-width="auto"
>
<div class="business-crud-page__billing-form-row">
<el-form-item label="方案名称" prop="planName" required>
<el-input
v-model="billingPlanForm.planName"
maxlength="100"
:disabled="dialogReadonly"
/>
</el-form-item>
<el-form-item label="" class="business-crud-page__billing-default">
<el-checkbox v-model="billingPlanForm.defaultPlan" :disabled="dialogReadonly">
默认方案
</el-checkbox>
</el-form-item>
</div>
<el-form-item label="备注">
<el-input
v-model="billingPlanForm.remark"
class="business-crud-page__billing-plan-remark"
maxlength="100"
show-word-limit
:disabled="dialogReadonly"
/>
</el-form-item>
</el-form>
<div class="business-crud-page__billing-rule-head">
<el-link v-if="!dialogReadonly" type="primary" @click="addBillingRule"> +添加规则 </el-link>
</div>
<div class="business-crud-page__billing-rule-wrap">
<el-table
:data="billingPlanForm.rules"
border
class="business-crud-page__billing-rule-table"
>
<el-table-column type="index" label="序号" width="70" align="center" fixed="left" />
<el-table-column width="180" align="center">
<template #header>
<span class="business-crud-page__required-column">费用类型</span>
</template>
<template #default="{ row }">
<el-select
v-model="row.feeType"
class="business-crud-page__billing-field"
clearable
filterable
:disabled="dialogReadonly"
:loading="billingFeeCategoryLoading"
placeholder="请选择"
@change="value => handleBillingFeeTypeChange(row, value)"
>
<el-option
v-for="item in billingFeeCategoryOptions"
:key="item.id || item.dictKey"
:label="item.dictValue"
:value="item.dictKey"
/>
</el-select>
</template>
</el-table-column>
<el-table-column width="210" align="center">
<template #header>
<span class="business-crud-page__required-column">费用项</span>
</template>
<template #default="{ row }">
<el-select
v-model="row.feeItem"
class="business-crud-page__billing-field"
clearable
filterable
:disabled="dialogReadonly || !row.feeType"
:loading="billingFeeItemLoadingMap[resolveBillingFeeType(row.feeType)]"
:placeholder="row.feeType ? '请选择' : '请先选择费用类型'"
>
<el-option
v-for="item in getBillingFeeItemOptions(row.feeType)"
:key="item.id || item.name || item.englishName"
:label="item.name || item.englishName"
:value="item.name || item.englishName"
/>
</el-select>
</template>
</el-table-column>
<el-table-column width="170" align="center">
<template #header>
<span class="business-crud-page__required-column">计费要素</span>
</template>
<template #default="{ row }">
<el-select
v-model="row.billingElement"
:disabled="dialogReadonly"
placeholder="请选择"
@change="handleBillingElementChange(row)"
>
<el-option
v-for="item in billingElementOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</template>
</el-table-column>
<el-table-column width="180" align="center">
<template #header>
<span class="business-crud-page__required-column">计费类型</span>
</template>
<template #default="{ row }">
<el-select
v-model="row.billingType"
:disabled="dialogReadonly || !row.billingElement"
:placeholder="row.billingElement ? '请选择' : '请先选择计费要素'"
@change="handleBillingTypeChange(row)"
>
<el-option
v-for="item in getBillingTypeOptions(row)"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</template>
</el-table-column>
<el-table-column width="150" align="center">
<template #header>
<span class="business-crud-page__required-column">计费单位</span>
</template>
<template #default="{ row }">
<el-select
v-model="row.billingUnit"
clearable
filterable
:disabled="dialogReadonly"
:loading="billingUnitLoading"
placeholder="请选择"
>
<el-option
v-for="item in billingUnitOptions"
:key="item.id || item.dictKey || item.dictValue"
:label="item.dictValue"
:value="item.dictValue"
/>
</el-select>
</template>
</el-table-column>
<el-table-column width="140" align="center">
<template #header>
<span class="business-crud-page__required-column">单价(元)</span>
</template>
<template #default="{ row }">
<el-input
v-model="row.unitPrice"
:disabled="dialogReadonly"
placeholder="请输入"
@input="value => handleBillingDecimalInput(row, 'unitPrice', value)"
/>
</template>
</el-table-column>
<el-table-column label="计费要素下限" width="260" align="center">
<template #default="{ row }">
<div class="business-crud-page__limit-list">
<el-input
v-for="(item, rangeIndex) in getBillingLimitRanges(row)"
:key="rangeIndex"
v-model="item.lowerLimit"
:disabled="!canEditBillingLimit(row)"
:placeholder="canEditBillingLimit(row) ? '请输入下限' : '无需配置'"
@input="value => handleBillingLimitInput(row, rangeIndex, 'lowerLimit', value)"
/>
</div>
</template>
</el-table-column>
<el-table-column label="计费要素上限" width="260" align="center">
<template #default="{ row }">
<div class="business-crud-page__limit-list">
<div
v-for="(item, rangeIndex) in getBillingLimitRanges(row)"
:key="rangeIndex"
class="business-crud-page__limit-row"
>
<el-input
v-model="item.upperLimit"
:disabled="!canEditBillingLimit(row)"
:placeholder="canEditBillingLimit(row) ? '请输入上限' : '无需配置'"
@input="value => handleBillingLimitInput(row, rangeIndex, 'upperLimit', value)"
/>
<el-link
v-if="canEditBillingLimit(row)"
type="primary"
@click="addBillingLimitRange(row, rangeIndex)"
>
添加
</el-link>
<el-button
v-if="canEditBillingLimit(row) && getBillingLimitRanges(row).length > 1"
type="danger"
text
@click="removeBillingLimitRange(row, rangeIndex)"
>
删除
</el-button>
</div>
</div>
</template>
</el-table-column>
<el-table-column label="保底计费重量" width="240" align="center">
<template #default="{ row }">
<el-input
v-model="row.minimumBillingWeight"
:disabled="!canEditMinimumBillingWeight(row)"
:placeholder="
canEditMinimumBillingWeight(row)
? '实际计量值小于保底数值时按保底计费'
: '仅按重量、按吨·公里可填写'
"
@input="value => handleBillingDecimalInput(row, 'minimumBillingWeight', value)"
/>
</template>
</el-table-column>
<el-table-column label="备注" width="180" align="center">
<template #default="{ row }">
<el-input
v-model="row.remark"
maxlength="100"
show-word-limit
:disabled="dialogReadonly"
placeholder="请输入"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="230" align="center" fixed="right">
<template #default="{ row, $index }">
<el-link type="primary" @click="openBillingMatch(row, $index)">
设置匹配条件
</el-link>
<el-link v-if="!dialogReadonly" type="primary" @click="copyBillingRule(row)">
复制
</el-link>
<el-link v-if="!dialogReadonly" type="danger" @click="removeBillingRule($index)">
删除
</el-link>
</template>
</el-table-column>
</el-table>
</div>
<template #footer>
<div class="business-crud-page__dialog-footer">
<el-button type="primary" :disabled="dialogReadonly" @click="submitBillingPlan">
提交
</el-button>
<el-button @click="billingPlanBox = false">取消</el-button>
</div>
</template>
</el-dialog>
<el-dialog title="设置匹配条件" append-to-body v-model="billingMatchBox" width="720px">
<el-form
:model="billingMatchForm"
label-position="right"
label-width="90px"
class="business-crud-page__match-form"
>
<el-row :gutter="48">
<el-col :span="12">
<el-form-item label="起运地">
<el-cascader
v-model="billingMatchForm.originPath"
:options="billingMatchRegionOptions"
:props="billingMatchRegionCascaderProps"
:placeholder="billingMatchForm.origin || '请选择起运地'"
:loading="billingMatchRegionLoading"
:disabled="dialogReadonly"
clearable
filterable
@visible-change="visible => visible && ensureBillingMatchRegionOptions()"
@change="value => handleBillingMatchRegionChange('origin', value)"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="目的地">
<el-cascader
v-model="billingMatchForm.destinationPath"
:options="billingMatchRegionOptions"
:props="billingMatchRegionCascaderProps"
:placeholder="billingMatchForm.destination || '请选择目的地'"
:loading="billingMatchRegionLoading"
:disabled="dialogReadonly"
clearable
filterable
@visible-change="visible => visible && ensureBillingMatchRegionOptions()"
@change="value => handleBillingMatchRegionChange('destination', value)"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="运输方式">
<el-select
v-model="billingMatchForm.transportMode"
:disabled="dialogReadonly"
clearable
>
<el-option label="公路" value="公路" />
<el-option label="铁路" value="铁路" />
<el-option label="水路" value="水路" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="货物类型">
<el-cascader
v-model="billingMatchForm.cargoTypePath"
:options="billingCargoTypeOptions"
:props="billingCargoTypeCascaderProps"
:placeholder="billingMatchForm.cargoType || '请选择货物类型'"
:loading="billingCargoTypeLoading"
:disabled="dialogReadonly"
clearable
filterable
:filter-method="filterBillingCargoType"
@visible-change="visible => visible && ensureBillingCargoTypeOptions()"
@change="handleBillingMatchCargoTypeChange"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="billingMatchBox = false">取消</el-button>
<el-button type="primary" :disabled="dialogReadonly" @click="saveBillingMatch">
保存
</el-button>
</template>
</el-dialog>
<el-dialog
title="选择线路"
append-to-body
v-model="transportRouteBox"
width="1280px"
@opened="loadTransportRouteList"
>
<div class="business-crud-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="business-crud-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="business-crud-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="business-crud-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="business-crud-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="business-crud-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="business-crud-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="business-crud-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="business-crud-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="business-crud-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 ref="transportAmap" class="business-crud-page__map"></div>
<div class="business-crud-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="business-crud-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="business-crud-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="business-crud-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="business-crud-page__cargo-import-dialog"
@closed="cargoImportRows = []"
>
<div class="business-crud-page__cargo-import">
<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>
<template #footer>
<el-button :disabled="cargoImportLoading" @click="closeCargoImportDialog">关闭</el-button>
<el-button
type="primary"
:loading="cargoImportLoading"
:disabled="!cargoImportRows.length"
@click="confirmCargoImport"
>
确定
</el-button>
</template>
</el-dialog>
<el-dialog
v-model="shippingTemplateProcessConfigBox"
title="过程配置"
append-to-body
width="980px"
class="business-crud-page__process-config-dialog"
>
<div v-loading="shippingTemplateProcessConfigLoading">
<el-empty
v-if="!shippingTemplateProcessConfigRows.length"
description="当前项目暂无过程配置"
:image-size="72"
/>
<section
v-for="row in shippingTemplateProcessConfigRows"
:key="row.id || row.configCode"
class="business-crud-page__process-config-section"
>
<div class="business-crud-page__process-config-meta">
<span>{{ row.configName || '过程配置' }}</span>
<span v-if="row.defaultFinishDays">默认{{ row.defaultFinishDays }}天后完成运输</span>
</div>
<div
v-if="getProcessConfigEnabledNodes(row).length"
class="business-crud-page__process-timeline"
>
<div class="business-crud-page__process-timeline-line"></div>
<div
v-for="node in getProcessConfigEnabledNodes(row)"
:key="node.key"
class="business-crud-page__process-timeline-item"
>
<span class="business-crud-page__process-timeline-dot"></span>
<div class="business-crud-page__process-timeline-name">{{ node.name }}</div>
<div class="business-crud-page__process-timeline-desc">
<div v-for="text in processConfigTimelineDescriptions(node)" :key="text">
{{ text }}
</div>
</div>
</div>
</div>
<el-empty v-else description="未配置现行过程节点" :image-size="56" />
</section>
</div>
<template #footer>
<el-button type="primary" @click="shippingTemplateProcessConfigBox = false">关闭</el-button>
</template>
</el-dialog>
<el-dialog :title="`${config.title}导入`" append-to-body v-model="excelBox" width="555px">
<avue-form :option="excelOptionConfig" v-model="excelForm">
<template #excelTemplate>
<el-button type="primary" @click="handleTemplate">
点击下载<i class="el-icon-download el-icon--right"></i>
</el-button>
</template>
</avue-form>
</el-dialog>
</basic-container>
</template>
<script>
import { exportBlob, importBlob } from '@/api/common';
import { getList as getCommonAddressList } from '@/api/base/common-address';
import { getList as getAirportMasterList } from '@/api/base/airport-master';
import { getList as getCargoTypeList } from '@/api/base/cargo-type';
import { getList as getFeeItemList } from '@/api/base/fee-item';
import { getList as getPortTerminalList } from '@/api/base/port-terminal';
import { getList as getRailwayStationList } from '@/api/base/railway-station';
import { getLazyTree as getRegionLazyTree } from '@/api/base/region';
import { getList as getCommonCargoList } from '@/api/business/common-cargo';
import {
getDetail as getContractDetail,
getList as getContractList,
} from '@/api/business/contract-manage';
import { getList as getProjectList } from '@/api/business/project-apply';
import { getList as getCommonRouteList } from '@/api/business/common-route';
import {
getDetail as getTransportPlanDetail,
getList as getTransportPlanList,
} from '@/api/business/transport-plan';
import {
getDetail as getProcessConfigDetail,
getList as getProcessConfigList,
getVoucherImages as getProcessConfigVoucherImages,
} from '@/api/business/process-config';
import { getList as getCarrierCustomerList } from '@/api/vehicle/customer-archive';
import { getList as getDriverList } from '@/api/transportCapacity/driver';
import { getDictionary } from '@/api/system/dictbiz';
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
import { getDeptTree } from '@/api/system/dept';
import { addressTypeOptions } from '@/option/base/common-address';
import { packageOptions } from '@/option/business/common';
import { formatUpdateUserName } from '@/utils/audit';
import { getToken } from '@/utils/auth';
import { openImportDialog } from '@/utils/import-excel';
import { applyTableMenuWidth } from '@/utils/table-menu';
import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { isMobile } from '@/utils/validate';
import { List, Location, Rank, 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 { mapGetters } from 'vuex';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
import WaybillImportDialog from './waybill-import-dialog.vue';
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
let amapLoader;
const attachmentViewerPlugins = [
imagePlugin(),
pdfPlugin({ workerSrc: pdfWorkerSrc, useFetchData: true }),
officePlugin({ pdf: { workerSrc: pdfWorkerSrc, useFetchData: true } }),
textPlugin(),
fallbackPlugin(),
];
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 defaultBillingRule = () => ({
feeType: '',
feeItem: '',
billingElement: '',
billingType: '',
billingUnit: '',
unitPrice: '',
lowerLimit: '',
upperLimit: '',
limitRanges: [{ lowerLimit: '', upperLimit: '' }],
minimumBillingWeight: '',
remark: '',
matchCondition: {
origin: '',
originCode: '',
originPath: [],
destination: '',
destinationCode: '',
destinationPath: [],
transportMode: '',
cargoType: '',
cargoTypeCode: '',
cargoTypePath: [],
},
});
const defaultBillingPlan = () => ({
planName: '',
defaultPlan: false,
remark: '',
rules: [defaultBillingRule()],
});
const defaultSettlementRule = () => ({
autoGenerate: 1,
billStartDate: '',
settlementType: '月结',
billCycleType: '固定截单日',
billCutoffDay: 25,
cycleDays: '',
});
const defaultReconciliation = () => ({
skipReconciliation: 1,
reconciliationMode: '明细逐行核对',
});
const defaultTransportCargo = () => ({
cargoName: '',
cargoType: '',
cargoTypeCode: '',
cargoTypePath: [],
quantity: '',
quantityUnit: '',
unitPrice: '',
priceUnit: '元/吨',
packageType: '',
brand: '',
specification: '',
model: '',
materialCode: '',
deviceCode: '',
remark: '',
});
const defaultShippingTemplateFreight = () => ({
currency: 'CNY',
otherFreightAmount: '',
freightAmount: '',
freightItems: [],
});
const defaultShippingTemplateFreightItem = () => ({
unitPrice: '',
priceUnit: '',
});
const defaultDispatchRow = () => ({
taskEntryMode: 'simple',
transportType: '',
carrierType: '承运商',
carrierId: '',
carrierName: '',
driverId: '',
driverName: '',
driverPhone: '',
vehicleNo: '',
captainName: '',
cabinNo: '',
containerNo: '',
trailerVehicleNo: '',
escortName: '',
escortPhone: '',
mileage: '',
cargoType: '',
cargoTypeCode: '',
cargoTypePath: [],
cargoName: '',
brand: '',
cargoInfo: '',
quantity: '',
quantityUnit: '吨',
specification: '',
model: '',
packageType: '',
brand: '',
materialCode: '',
unitPrice: '',
priceUnit: '元/吨',
otherFeeTotal: '',
freightCurrency: 'CNY',
freightJson: '',
goodsJson: '',
estimatedStartTime: '',
estimatedEndTime: '',
departureAddress: '',
departureName: '',
departureContact: '',
departurePhone: '',
arrivalAddress: '',
arrivalName: '',
arrivalContact: '',
arrivalPhone: '',
dispatchUserName: '',
dispatchTime: '',
attachmentsJson: '',
remark: '',
});
const taskCarrierTypes = ['承运商', '自运', '网货平台'];
export default {
components: { WaybillImportDialog, Rank, ElImageViewer, OpenFileViewer },
name: 'BusinessCrudPage',
props: {
api: {
type: Object,
required: true,
},
config: {
type: Object,
required: true,
},
crudOption: {
type: Object,
required: true,
},
crudExcelOption: {
type: Object,
default: () => ({ column: [] }),
},
// 可选:固定操作列宽度;不传则按按钮数量自动估算(沿用既有逻辑)
menuWidth: {
type: Number,
default: null,
},
detailId: {
type: [String, Number],
default: null,
},
},
data() {
return {
List,
Location,
Search,
form: {},
suppressTransportTypeClear: false,
query: {},
loading: true,
data: [],
allDept: 0,
dialogReadonly: false,
detailBox: false,
detailLoading: false,
detailRow: {},
waybillDetailProcessNodes: [],
waybillProcessDetailTab: 'punch',
waybillHasRelatedVoucher: false,
waybillVoucherImages: [],
waybillVoucherImagesLoading: false,
waybillVoucherImagesLoaded: false,
waybillRouteChangeBox: false,
waybillRouteChangeTab: 'change',
waybillRouteChangeRows: [],
waybillRouteChangeNodes: [],
waybillRouteChangeRemark: '',
waybillRouteChangeRecords: [],
waybillRouteChangeDragIndex: -1,
waybillRouteChangeSaving: false,
waybillCommonAddressBox: false,
waybillCommonAddressLoading: false,
waybillCommonAddressQuery: {
addressName: '',
},
waybillCommonAddressRows: [],
waybillCommonAddressTarget: null,
waybillCommonAddressPage: {
pageSize: 10,
currentPage: 1,
total: 0,
},
waybillImportBox: false,
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' }],
},
draftSaveLoading: false,
crudDialogType: '',
defaultDialogButtons: null,
contractCreateActionLoading: '',
transportPlanCreateActionLoading: '',
option: (() => {
const opt = this.cloneOption(this.crudOption, estimateMenuButtonCount(this.config));
if (this.menuWidth != null) opt.menuWidth = this.menuWidth;
return opt;
})(),
selectedProjectId: '',
projectOptions: [],
projectLoading: false,
contractOptions: [],
contractLoading: false,
shippingPlanOptions: [],
shippingPlanLoading: false,
selectedOrganizationId: '',
organizationOptions: [],
organizationFlatOptions: [],
organizationLoading: false,
addressTypeOptions,
flowBox: false,
flowUrl: '',
processInstanceId: '',
dispatchBox: false,
dispatchLoading: false,
dispatchRow: {},
dispatchRows: [],
dispatchSaving: '',
dispatchItemBox: false,
dispatchItemIndex: -1,
dispatchItemForm: defaultDispatchRow(),
dispatchItemCargoRows: [],
dispatchItemAttachmentRows: [],
selectedDispatchItemAttachmentRows: [],
attachmentRows: [],
selectedAttachmentRows: [],
contractFileRows: [],
selectedContractFileRows: [],
attachmentImagePreviewVisible: false,
attachmentImagePreviewUrls: [],
attachmentImagePreviewIndex: 0,
attachmentDocumentPreviewVisible: false,
attachmentPreviewFile: {},
attachmentViewerPlugins,
attachmentViewerToolbar: {
download: true,
fullscreen: true,
print: true,
rotate: true,
zoom: true,
},
transportCargoRows: [],
taskFreightCurrency: 'RMB',
shippingTemplateFreight: defaultShippingTemplateFreight(),
shippingTemplateCurrencyOptions: [],
shippingTemplateCurrencyLoading: false,
shippingTemplateProcessConfigBox: false,
shippingTemplateProcessConfigLoading: false,
shippingTemplateProcessConfigRows: [],
hasProjectProcessConfig: false,
transportRouteBox: false,
transportRouteLoading: false,
transportRouteQuery: {},
transportRouteRows: [],
transportRouteSelected: null,
transportRoutePage: {
pageSize: 10,
currentPage: 1,
total: 0,
},
transportAddressBox: false,
transportAddressTarget: '',
transportAddressLoading: false,
transportAddressQuery: {},
transportAddressRows: [],
transportAddressSelected: null,
transportAddressPage: {
pageSize: 10,
currentPage: 1,
total: 0,
},
transportStationBox: false,
transportStationTarget: '',
transportStationLoading: false,
transportStationQuery: {},
transportStationRows: [],
transportStationPage: {
pageSize: 10,
currentPage: 1,
total: 0,
},
transportMapBox: false,
transportMapTarget: '',
transportMapLoading: false,
transportMapKeyword: '',
transportMapStatus: '可搜索地址或点击地图选点',
transportMapSelected: {},
transportAmap: null,
transportAmapMarker: null,
transportAmapGeocoder: null,
commonCargoBox: false,
commonCargoLoading: false,
commonCargoQuery: {},
commonCargoRows: [],
commonCargoSelected: [],
commonCargoPage: {
pageSize: 10,
currentPage: 1,
total: 0,
},
taskCarrierTypes,
taskCarrierOptions: [],
taskCarrierLoading: false,
taskDriverOptions: [],
taskDriverLoading: 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: [],
billingPlanRows: [],
settlementRuleForm: defaultSettlementRule(),
settlementRuleErrors: {},
reconciliationForm: defaultReconciliation(),
changeRecordRows: [],
billingPlanBox: false,
billingPlanMode: 'add',
billingPlanIndex: -1,
billingPlanForm: defaultBillingPlan(),
billingFeeCategoryOptions: [],
billingFeeCategoryLoading: false,
billingFeeCategoryRequest: null,
billingFeeItemOptionsMap: {},
billingFeeItemLoadingMap: {},
billingFeeItemRequestMap: {},
billingUnitOptions: [],
billingUnitLoading: false,
billingUnitRequest: null,
billingMatchBox: false,
billingMatchRuleIndex: -1,
billingMatchForm: defaultBillingRule().matchCondition,
billingMatchRegionOptions: [],
billingMatchRegionLoading: false,
billingMatchRegionRequest: null,
billingCargoTypeOptions: [],
billingCargoTypeFlatOptions: [],
billingCargoTypeLoading: false,
billingCargoTypeRequest: null,
billingPlanRules: {
planName: [{ required: true, message: '请输入方案名称', trigger: 'blur' }],
},
billingElementOptions: [
'按重量',
'按体积',
'按车辆',
'按里程',
'按吨·公里',
'固定金额(整单一口价)',
'按数量',
],
billingElementTypeMap: {
按重量: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
按体积: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
按车辆: ['固定单价'],
按里程: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
'按吨·公里': ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
'固定金额(整单一口价)': ['固定一口价'],
按数量: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
},
settlementTypeOptions: ['月结', '日结', '周结', '半月结', '固定天数周期结算'],
billCycleTypeOptions: ['固定截单日', '自然月'],
reconciliationModeOptions: ['明细逐行核对', '按账单汇总核对', '跳过自动核对'],
packageOptions,
quantityUnitOptions: ['吨', '千克', '立方米', '件', '车', '箱', '托盘'],
attachmentFileTypes: [
'pdf',
'bmp',
'jpeg',
'png',
'jpg',
'doc',
'docx',
'ppt',
'pptx',
'xlsx',
'xls',
'eml',
'msg',
'zip',
],
excelBox: false,
excelForm: {},
excelOptionConfig: this.cloneOption(this.crudExcelOption),
page: {
pageSize: 10,
pageSizes: [10, 20, 50, 100],
currentPage: 1,
total: 0,
},
selectionList: [],
contractExpiryScope: '',
contractSignType: '',
searchExpanded: false,
attachmentUploadMode: false,
contractExpiryStats: {
all: 0,
within30: 0,
within90: 0,
over90: 0,
expired: 0,
},
};
},
created() {
if (this.config.enableAllDept && this.isAdmin) {
this.allDept = 1;
}
if (this.config.enableProjectSelect) {
this.loadProjectOptions();
}
if (this.config.enableOrganizationSelect) {
this.loadOrganizationOptions();
}
if (this.config.enableBillingPlan) {
this.ensureBillingFeeCategoryOptions();
this.ensureBillingUnitOptions();
}
if (this.config.enableTaskInfoForm) {
this.loadTaskQuantityUnitOptions();
this.loadTaskFeeUnitOptions();
this.ensureBillingCargoTypeOptions();
}
if (this.config.enableShippingTemplateFreight) {
this.loadShippingTemplateCurrencyOptions();
this.loadTaskFeeUnitOptions();
}
if (this.config.enableWaybillFooterFreightSummary) {
this.loadShippingTemplateCurrencyOptions();
}
this.consumeTemplateCreatePayload();
},
computed: {
...mapGetters(['permission', 'userInfo']),
permissionList() {
return {
addBtn: this.canCreate,
};
},
detailSections() {
return this.config.detailSections || [];
},
isWaybillDetailLayout() {
return this.config.permission === 'waybill_manage';
},
waybillDetailSummaryFields() {
return [
['customerName', '客户', true],
['contractNo', '合同编号', true],
['projectName', '项目', true],
['originalNo', '原始单号'],
['planName', '计划名称'],
['planNo', '计划单号', true],
['masterNo', '总单号', true],
['loadingNo', '配载单号', true],
];
},
isTransportPlanPage() {
return this.config.permission === 'transport_plan';
},
isTransportPlanDetailLayout() {
return this.isTransportPlanPage && this.detailBox;
},
transportPlanGoodsText() {
const text = this.formatDetailValue(this.detailRow, 'goodsInfo');
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 (
['transport_plan', 'waybill_manage'].includes(this.config.permission) &&
Boolean(this.$route.query.fromTemplate)
);
},
completeActionLabel() {
return this.config.permission === 'transport_plan' ? '完成调度' : '完成';
},
isRoadTransport() {
return this.config.permission === 'waybill_manage' && this.transportMode === 'road';
},
isNonRoadTransport() {
return this.config.permission === 'waybill_manage' && !this.isRoadTransport;
},
transportVehicleNoLabel() {
return this.isRoadTransport ? '车牌号' : '船/航/班列号';
},
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;
},
contractSignTypeOptions() {
return this.option.column?.find(column => column.prop === 'signType')?.dicData || [];
},
contractExpiryTagOptions() {
return [
{ label: '全部', value: '', statKey: 'all' },
{ label: '30天内到期', value: 'within30', statKey: 'within30' },
{ label: '90天内到期', value: 'within90', statKey: 'within90' },
{ label: '90天以上', value: 'over90', statKey: 'over90' },
{ label: '已到期', value: 'expired', statKey: 'expired' },
];
},
contractSelectEnabled() {
return this.config.enableContractSelect || this.config.enableTransportPlanForm;
},
shippingPlanSelectEnabled() {
return this.config.enableShippingPlanSelect === true;
},
shippingInfoFormEnabled() {
return this.config.enableShippingInfoForm || this.config.enableTransportPlanForm;
},
taskInfoFormEnabled() {
return this.config.enableTaskInfoForm === true;
},
taskEntryModeOptions() {
const locked = this.hasMultipleConfiguredGoods(this.form, this.transportCargoRows);
return [
{ label: '精简录入', value: 'simple', disabled: locked },
{ label: '完整录入', value: 'full' },
];
},
dispatchTaskEntryModeOptions() {
const locked = this.hasMultipleConfiguredGoods(
this.dispatchItemForm,
this.dispatchItemCargoRows
);
return [
{ label: '精简录入', value: 'simple', disabled: locked },
{ label: '完整录入', value: 'full' },
];
},
isTaskFullMode() {
return this.form.taskEntryMode === 'full';
},
isCarrierMode() {
return this.form.carrierType === '承运商';
},
showRoadEscortFields() {
return this.isRoadTransport && !this.isCarrierMode;
},
taskCargoOptions() {
return this.getTaskCargoOptionsByPath(this.form.cargoTypePath);
},
taskCargoLoading() {
return this.isTaskCargoOptionsLoading(this.form.cargoTypePath);
},
taskCargoQuantityTotal() {
const total = this.transportCargoRows.reduce((sum, row) => {
const value = Number(row.quantity);
return Number.isFinite(value) ? sum + value : sum;
}, 0);
if (!total) return '';
return Number.isInteger(total) ? String(total) : String(Number(total.toFixed(3)));
},
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 || '吨';
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() {
return this.dispatchTableQuantityItems.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('、');
return `共${formatSummary('total')} | 已调度 ${formatSummary(
'assigned'
)},剩余${formatSummary('remaining')}`;
},
dispatchAttachmentRows() {
return this.parseJsonArray(this.dispatchRow.attachmentsJson);
},
dispatchRouteStart() {
const address = this.dispatchRow.departureAddress || this.dispatchRow.departureName || '';
return {
label: this.dispatchRow.departureName || '起',
address,
detail: this.dispatchRow.departureDetailAddress || address,
contact: [this.dispatchRow.departureContact, this.dispatchRow.departurePhone]
.filter(Boolean)
.join(' - '),
};
},
dispatchRouteEnd() {
const address = this.dispatchRow.arrivalAddress || this.dispatchRow.arrivalName || '';
return {
label: this.dispatchRow.arrivalName || '终',
address,
detail: this.dispatchRow.arrivalDetailAddress || address,
contact: [this.dispatchRow.arrivalContact, this.dispatchRow.arrivalPhone]
.filter(Boolean)
.join(' - '),
};
},
dispatchDialogTitle() {
return '计划调度';
},
dispatchItemDialogTitle() {
const mode = this.dispatchItemForm.taskEntryMode === 'full' ? '完整录入' : '精简录入';
return this.dispatchItemIndex < 0 ? `新增/编辑(${mode}` : `编辑调度信息(${mode}`;
},
dispatchTransportMode() {
return this.resolveTransportMode(this.dispatchItemForm.transportType);
},
dispatchIsRoadTransport() {
return this.dispatchTransportMode === 'road';
},
dispatchIsNonRoadTransport() {
return ['water', 'rail', 'air'].includes(this.dispatchTransportMode);
},
dispatchIsCarrierMode() {
return this.dispatchItemForm.carrierType === '承运商';
},
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');
},
taskFreightAmount() {
const quantity = Number(this.taskCargoQuantityTotal || this.form.quantity || 0);
const unitPrice = Number(this.form.unitPrice || 0);
if (!quantity || !unitPrice) return '';
const amount = quantity * unitPrice;
return Number.isInteger(amount) ? String(amount) : String(Number(amount.toFixed(2)));
},
dispatchItemFreightTotal() {
if (this.dispatchItemForm.taskEntryMode !== 'full') {
const quantity = Number(this.dispatchItemForm.quantity || 0);
const unitPrice = Number(this.dispatchItemForm.unitPrice || 0);
const otherFeeTotal = Number(this.dispatchItemForm.otherFeeTotal || 0);
const hasFreight = this.dispatchItemForm.unitPrice !== '' && quantity > 0;
const hasOtherFee = this.dispatchItemForm.otherFeeTotal !== '';
if (!hasFreight && !hasOtherFee) return '';
const total = (hasFreight ? quantity * unitPrice : 0) + (hasOtherFee ? otherFeeTotal : 0);
return Number.isInteger(total) ? String(total) : String(Number(total.toFixed(2)));
}
const otherFeeTotal = Number(this.dispatchItemForm.otherFeeTotal || 0);
const freightTotal = this.dispatchItemCargoRows.reduce(
(total, cargo) => total + Number(this.dispatchCargoFreightAmount(cargo) || 0),
0
);
const hasOtherFee = this.dispatchItemForm.otherFeeTotal !== '';
if (!freightTotal && !hasOtherFee) return '';
const total = freightTotal + (hasOtherFee ? otherFeeTotal : 0);
return Number.isInteger(total) ? String(total) : String(Number(total.toFixed(2)));
},
dispatchItemFreightSubtotal() {
if (this.dispatchItemForm.taskEntryMode !== 'full') {
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.dispatchItemCargoRows.reduce(
(sum, cargo) => sum + Number(this.dispatchCargoFreightAmount(cargo) || 0),
0
);
return total ? (Number.isInteger(total) ? String(total) : String(Number(total.toFixed(2)))) : '';
},
dispatchItemFreightCurrencyLabel() {
const value = this.dispatchItemForm.freightCurrency || 'CNY';
return this.currencyRemark(value);
},
taskFullFreightTotal() {
const total = this.transportCargoRows.reduce(
(sum, cargo) => sum + Number(this.taskFullFreightAmount(cargo) || 0),
0
);
return this.formatTaskFullFreightNumber(total);
},
taskFreightCurrencyLabel() {
const currency = this.taskFreightCurrency;
if (!currency) return '';
return this.currencyRemark(currency);
},
isAdmin() {
const authority = this.userInfo && this.userInfo.authority;
return Array.isArray(authority)
? authority.includes('admin')
: String(authority || '').includes('admin');
},
canViewAllDept() {
return this.isAdmin;
},
readonlyScope() {
return (
this.config.enableAllDept && this.config.allDeptReadonly !== false && this.allDept === 1
);
},
canCreate() {
return this.hasPermission(`${this.config.permission}_add`) && !this.readonlyScope;
},
showContractCreateActions() {
return (
this.config.enableContractCreateActions &&
this.crudDialogType === 'add' &&
!this.dialogReadonly
);
},
showTransportPlanCreateActions() {
return (
this.config.enableTransportPlanCreateActions &&
this.crudDialogType === 'add' &&
!this.dialogReadonly
);
},
organizationCascaderProps() {
return {
label: 'label',
value: 'id',
children: 'children',
checkStrictly: true,
emitPath: false,
};
},
billingMatchRegionCascaderProps() {
return {
label: 'title',
value: 'id',
children: 'children',
leaf: 'leaf',
emitPath: true,
};
},
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,
};
},
settlementRuleEnabled() {
return Number(this.settlementRuleForm.autoGenerate) === 1;
},
showSettlementBillCycleType() {
return this.settlementRuleForm.settlementType === '月结';
},
showSettlementBillCutoffDay() {
return (
this.showSettlementBillCycleType && this.settlementRuleForm.billCycleType === '固定截单日'
);
},
showSettlementCycleDays() {
return this.settlementRuleForm.settlementType === '固定天数周期结算';
},
canBatchComplete() {
return (
this.hasAction('batchComplete') &&
this.hasPermission(`${this.config.permission}_batch_complete`) &&
!this.readonlyScope
);
},
canRoadLoading() {
const permission =
this.config.roadLoadingPermission || `${this.config.permission}_road_loading`;
return this.config.roadLoading && this.hasPermission(permission) && !this.readonlyScope;
},
ids() {
return this.selectionList.map(item => item.id).join(',');
},
billCutoffDayOptions() {
return Array.from({ length: 31 }, (_, index) => {
const value = index + 1;
return {
label: `${value}日`,
value,
};
});
},
cycleDayOptions() {
return Array.from({ length: 365 }, (_, index) => {
const value = index + 1;
return {
label: `${value}天`,
value,
};
});
},
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);
},
shippingTemplateFreightVisible() {
return (
this.config.enableShippingTemplateFreight === true && this.form.templateType === '运单'
);
},
shippingTemplateCurrencyLabel() {
const currency = this.shippingTemplateFreight.currency;
if (!currency) return '';
const option = this.shippingTemplateCurrencyOptions.find(item => item.value === currency);
return this.currencyName(option?.label || currency);
},
shippingTemplateRoadFreightTotal() {
const total = this.shippingTemplateFreight.freightItems.reduce(
(sum, item, index) =>
sum + Number(this.shippingTemplateRoadFreightAmount(item, index) || 0),
0
);
return this.formatShippingTemplateFreightNumber(total);
},
shippingTemplateFooterFreightTotal() {
const freightAmount =
this.transportMode === 'road'
? Number(this.shippingTemplateRoadFreightTotal || 0)
: Number(this.shippingTemplateFreight.freightAmount || 0);
return freightAmount + Number(this.shippingTemplateFreight.otherFreightAmount || 0);
},
waybillFooterFreightTotal() {
const freightAmount = this.isTaskFullMode
? Number(this.taskFullFreightTotal || 0)
: Number(this.taskFreightAmount || 0);
return freightAmount + Number(this.form.otherFeeTotal || 0);
},
showFooterFreightSummary() {
return (
this.config.enableShippingTemplateFreight === true ||
this.config.enableWaybillFooterFreightSummary === true
);
},
footerFreightTotal() {
return this.config.enableShippingTemplateFreight === true
? this.shippingTemplateFooterFreightTotal
: this.waybillFooterFreightTotal;
},
formatFooterFreightTotal() {
return this.footerFreightTotal.toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
},
transportAddressSelectDisabled() {
return this.dialogReadonly || this.isBillingFieldEmpty(this.form.transportType);
},
transportMode() {
return this.resolveTransportMode(this.form.transportType);
},
hasTransportType() {
return !this.isBillingFieldEmpty(this.form.transportType);
},
transportStationMode() {
return ['water', 'rail', 'air'].includes(this.transportMode);
},
transportStationTypeLabel() {
const labelMap = {
water: '港口/码头',
rail: '铁路车站',
air: '空港机场',
};
return labelMap[this.transportMode] || '';
},
transportStationNamePlaceholder() {
if (!this.transportStationMode) return '行政区划自动带出';
const labelMap = {
water: '请选择港口/码头',
rail: '请选择车站',
air: '请选择空港',
};
return labelMap[this.transportMode] || '请选择站点';
},
transportAddressDetailPlaceholder() {
return this.transportStationMode ? '选择站点后带出详细地址' : '请输入';
},
},
watch: {
detailId: {
immediate: true,
handler(id) {
if (id !== undefined && id !== null && id !== '') {
this.$nextTick(() => this.openDetail({ id }));
}
},
},
waybillProcessDetailTab(tab) {
if (tab === 'batchSupplement') this.loadWaybillVoucherImages();
},
'form.transportType'(value, oldValue) {
if (!this.shippingInfoFormEnabled || this.suppressTransportTypeClear) return;
if (String(value ?? '') === String(oldValue ?? '')) return;
this.handleTransportTypeChange(value);
this.syncShippingTemplateFreightItems();
},
},
methods: {
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 = {}) {
if (row.goodsInfo || row.cargoInfo) return String(row.goodsInfo || row.cargoInfo);
const goodsRows = this.parseJsonArray(row.goodsJson);
const goods =
goodsRows.find(item => (item.quantityUnit || item.cargoUnit || item.unit) === '吨') ||
goodsRows[0] ||
{};
const name = goods.cargoName || goods.goodsName || goods.name || '';
const type = goods.cargoType || goods.goodsType || goods.typeName || '';
const quantity = goods.quantity ?? goods.cargoQuantity ?? goods.goodsQuantity ?? '';
const unit = goods.quantityUnit || goods.cargoUnit || goods.unit || '';
return [name, type, quantity === '' ? '' : `${quantity}${unit}`].filter(Boolean).join('/');
},
formatTransportPlanDistrictAddress(value) {
const text = String(value || '').trim();
if (!text) return '-';
const cityIndex = text.indexOf('市');
const provinceIndex = Math.max(text.lastIndexOf('省', cityIndex), text.lastIndexOf('自治区', cityIndex));
const cityStart = provinceIndex > -1 ? provinceIndex + 1 : 0;
const districtStart = cityIndex > -1 ? cityIndex + 1 : cityStart;
const districtMatch = text
.slice(districtStart)
.match(/(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗)/);
if (districtMatch) {
return text.slice(cityStart, districtStart + districtMatch.index + districtMatch[0].length);
}
return cityIndex > -1 ? text.slice(cityStart, cityIndex + 1) : text;
},
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;
},
hasPermission(code) {
return this.isAdmin || this.validData(this.permission && this.permission[code], false);
},
hasAction(action) {
return (this.config.actions || []).includes(action);
},
statusValue(row) {
return row[this.config.statusProp || 'status'];
},
inStatus(row, statusList) {
if (!statusList || !statusList.length) {
return true;
}
const status = this.statusValue(row);
const statusText = this.config.statusTextProp ? row[this.config.statusTextProp] : '';
return statusList.includes(status) || statusList.includes(statusText);
},
isReadonlyEditAllowed(row) {
return this.inStatus(row, this.config.ignoreReadonlyEditStatus);
},
isReadonlyDeleteAllowed(row) {
return this.inStatus(row, this.config.ignoreReadonlyDeleteStatus);
},
canEdit(row) {
return (
this.hasPermission(`${this.config.permission}_edit`) &&
!this.readonlyScope &&
(!row.readonly || this.isReadonlyEditAllowed(row)) &&
this.inStatus(row, this.config.editStatus)
);
},
canDelete(row) {
return (
this.hasPermission(`${this.config.permission}_delete`) &&
!this.readonlyScope &&
(!row.readonly || this.isReadonlyDeleteAllowed(row)) &&
this.inStatus(row, this.config.deleteStatus) &&
(!this.config.deleteStage || this.config.deleteStage.includes(row.contractStage))
);
},
canCopy(row) {
return (
this.hasAction('copy') &&
this.hasPermission(`${this.config.permission}_copy`) &&
!this.readonlyScope &&
!row.readonly
);
},
canEnable(row) {
return (
this.hasAction('enable') &&
this.hasPermission(`${this.config.permission}_enable`) &&
!this.readonlyScope &&
!row.readonly &&
[0, 2].includes(row.status)
);
},
canDisable(row) {
return (
this.hasAction('disable') &&
this.hasPermission(`${this.config.permission}_disable`) &&
!this.readonlyScope &&
!row.readonly &&
row.status === 1
);
},
canCancel(row) {
if (
!this.hasAction('cancel') ||
!this.hasPermission(`${this.config.permission}_cancel`) ||
this.readonlyScope ||
row.readonly
) {
return false;
}
const status = this.statusValue(row);
return this.config.permission === 'transport_plan'
? ['waiting_dispatch', 'dispatching'].includes(status)
: ['pending', 'processing'].includes(status);
},
canComplete(row) {
if (
!this.hasAction('complete') ||
!this.hasPermission(`${this.config.permission}_complete`) ||
this.readonlyScope ||
row.readonly
) {
return false;
}
const status = this.statusValue(row);
return this.config.permission === 'transport_plan'
? status === 'dispatching'
: status === 'processing';
},
canReassign(row) {
return (
this.hasAction('reassign') &&
this.hasPermission(`${this.config.permission}_reassign`) &&
!this.readonlyScope &&
!row.readonly &&
this.statusValue(row) === 'pending'
);
},
customOperations(row) {
return (this.config.operations || []).filter(operation => this.canOperation(operation, row));
},
menuActions(row) {
const actions = [];
if (this.canCopy(row)) {
actions.push({ key: 'copy', label: '复制', action: 'copy' });
}
if (this.canEdit(row)) {
actions.push({
key: 'edit',
label: this.editLabel(row),
action: 'edit',
});
}
this.customOperations(row).forEach(operation => {
actions.push({
...operation,
key: `${operation.action}-${operation.label}`,
});
});
if (this.canDelete(row)) {
actions.push({ key: 'delete', label: '删除', action: 'delete', type: 'danger' });
}
return actions;
},
editLabel(row) {
const status = this.statusValue(row);
return this.config.editLabelStatusMap?.[status] || '编辑';
},
handleMenuAction(action, row, index) {
if (action.action === 'copy') {
this.handleCopy(row);
return;
}
if (action.action === 'edit' || action.action === 'attachmentUpload') {
this.attachmentUploadMode = action.action === 'attachmentUpload';
this.$refs.crud.rowEdit(row, index);
return;
}
if (action.action === 'delete') {
this.rowDel(row);
return;
}
this.handleCustomOperation(action, 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(() => {
this.$refs.crud.rowAdd();
this.$nextTick(() => {
// 模板主键不能带入目标业务单据,避免提交时被后端误判为更新。
const row = { ...(payload.data || {}) };
[
'id',
'createUser',
'createUserName',
'createDept',
'createTime',
'updateUser',
'updateUserName',
'updateTime',
].forEach(key => delete row[key]);
this.suppressTransportTypeClear = true;
Object.assign(this.form, row);
this.applyWaybillTemplateTaskData(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();
this.loadProjectProcessConfigState();
if (this.config.enableTransportPlanForm) this.initTransportPlanFormRows();
if (this.taskInfoFormEnabled) this.initTaskInfoForm();
});
});
},
applyWaybillTemplateTaskData(template = {}) {
if (this.config.permission !== 'waybill_manage') return;
const goodsRows = this.parseJsonArray(template.goodsJson);
if (!goodsRows.length) return;
const freight = this.parseJsonObject(template.freightJson);
const freightItems = Array.isArray(freight.freightItems) ? freight.freightItems : [];
const enrichedGoodsRows = goodsRows.map((goods, index) => {
const freightItem = freightItems[index] || {};
return {
...goods,
unitPrice: freightItem.unitPrice ?? goods.unitPrice ?? '',
priceUnit: freightItem.priceUnit || goods.priceUnit || '',
};
});
this.form.goodsJson = JSON.stringify(enrichedGoodsRows);
this.taskFreightCurrency = freight.currency || this.taskFreightCurrency || 'RMB';
this.form.otherFeeTotal =
freight.otherFreightAmount ?? freight.otherFeeTotal ?? this.form.otherFeeTotal ?? '';
if (enrichedGoodsRows.length > 1) {
this.form.taskEntryMode = 'full';
return;
}
const [goods] = enrichedGoodsRows;
this.form.taskEntryMode = 'simple';
Object.assign(this.form, {
cargoType: goods.cargoType || goods.goodsType || '',
cargoTypeCode: goods.cargoTypeCode || goods.goodsTypeCode || '',
cargoName: goods.cargoName || goods.goodsName || '',
specification: goods.specification || goods.spec || '',
model: goods.model || goods.modelName || '',
quantity: goods.quantity ?? goods.cargoQuantity ?? goods.goodsQuantity ?? '',
quantityUnit: goods.quantityUnit || goods.goodsQuantityUnit || goods.unit || '吨',
unitPrice: goods.unitPrice,
priceUnit: goods.priceUnit || '元/吨',
});
},
canOperation(operation, row) {
const permission = operation.permission || `${this.config.permission}_${operation.action}`;
if (!this.hasPermission(permission) || this.readonlyScope || row.readonly) {
return false;
}
const prop = operation.statusProp || this.config.statusProp || 'status';
const statusList = operation.status || [];
if (statusList.length && !statusList.includes(row[prop])) {
return false;
}
if (operation.stage && !operation.stage.includes(row.contractStage)) {
return false;
}
return true;
},
showDetailButton(row) {
if (!this.config.detailButton || !this.hasPermission(`${this.config.permission}_view`))
return false;
const status = this.statusValue(row);
return status !== 'draft';
},
formatDetailValue(row, prop) {
if (!row) return '-';
if (prop === 'businessStatus') return this.displayStatus(row, prop);
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];
return value === undefined || value === null || value === '' ? '-' : value;
},
waybillTransportMode(row = {}) {
return (
row.transportModeName ||
row.transportMode ||
row.transportTypeName ||
row.transportType ||
''
);
},
waybillIsRoadTransport(row = {}) {
return this.waybillTransportModeCode(row) === 'road';
},
waybillTransportModeCode(row = {}) {
const text = [
row.transportType,
row.transportTypeName,
row.transportMode,
row.transportModeName,
this.getTransportTypeLabel(row.transportType),
]
.filter(Boolean)
.join(' ')
.toLowerCase();
if (/公路|道路|汽车|陆运|road|highway/.test(text) || text === 'gl') return 'road';
if (/水路|水运|海运|港|water|ship|sea/.test(text) || text === 'sl') return 'water';
if (/航空|空运|空港|air|airport/.test(text) || text === 'hk') return 'air';
if (/铁路|rail|train/.test(text) || text === 'tl') return 'rail';
return '';
},
waybillIsNonRoadTransport(row = {}) {
return ['water', 'rail', 'air'].includes(this.waybillTransportModeCode(row));
},
waybillIsCarrier(row = {}) {
return String(row.carrierTypeName || row.carrierType || '').trim() === '承运商';
},
waybillVehicleNoLabel(row = {}) {
return this.waybillIsRoadTransport(row) ? '车牌号' : '船/航/班列号';
},
waybillUnitPrice(row = {}) {
const price = row.unitPrice || row.price || '';
const unit = row.priceUnit || row.billingUnit || '';
return price === '' ? '' : unit ? `${price}(${unit})` : price;
},
waybillCurrencyRemark(row = {}) {
const freight = this.parseJsonObject(row.freightJson);
const value = row.currency || row.freightCurrency || freight.currency || '';
if (!value) return '';
const normalized = String(value).toUpperCase();
const option = this.shippingTemplateCurrencyOptions.find(item => {
const itemValue = String(item.value || '').toUpperCase();
return itemValue === normalized || (normalized === 'CNY' && itemValue === 'RMB');
});
const remark = String(option?.remark || '').trim();
if ((normalized === 'CNY' || normalized === 'RMB') && remark === '元') return '¥';
return remark;
},
waybillUnitPriceWithCurrency(row = {}) {
const text = this.waybillUnitPrice(row);
if (!text) return { value: '-', unit: '' };
const match = String(text).match(/^(.*)\((.*)\)$/);
const price = match ? match[1] : text;
const unit = match ? match[2] : '';
return {
value: `${this.waybillCurrencyRemark(row)}${price}`,
unit,
};
},
waybillAmountWithCurrency(row = {}, prop) {
const value = this.formatDetailValue(row, prop);
if (value === '-') return { value: '-' };
return { value: `${this.waybillCurrencyRemark(row)}${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', query: { detailId: 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 = {}) {
if (!this.isTransportPlanPage) {
return;
}
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) === transportType
);
return item?.label || transportType;
},
openDetail(row) {
this.detailBox = true;
this.detailLoading = true;
this.detailRow = { ...row };
this.transportPlanWaybillPage.currentPage = 1;
this.waybillProcessDetailTab = 'punch';
this.waybillHasRelatedVoucher = false;
this.waybillVoucherImages = [];
this.waybillVoucherImagesLoaded = false;
const request =
typeof this.api.getDetail === 'function'
? this.api.getDetail(row.id)
: Promise.resolve({ data: { data: row } });
request
.then(res => {
const detail = res?.data?.data || res?.data || row;
this.detailRow = detail;
this.ensureTransportPlanDetailTransportTypeName(detail);
if (this.isWaybillDetailLayout && detail.contractId) {
return getContractDetail(detail.contractId)
.then(contractRes => {
const contract = contractRes?.data?.data || contractRes?.data || {};
this.detailRow = {
...detail,
contractNo: contract.contractNo || detail.contractNo || detail.contractName || '',
};
this.applyFormDetail(this.detailRow);
return this.loadWaybillDetailProcessNodes(this.detailRow.projectId);
})
.catch(() => {
this.detailRow = {
...detail,
contractNo: detail.contractNo || detail.contractName || '',
};
this.applyFormDetail(this.detailRow);
return this.loadWaybillDetailProcessNodes(this.detailRow.projectId);
});
}
this.applyFormDetail(detail);
if (this.isWaybillDetailLayout)
return this.loadWaybillDetailProcessNodes(detail.projectId);
return null;
})
.finally(() => {
this.detailLoading = false;
});
},
loadWaybillDetailProcessNodes(projectId) {
this.waybillDetailProcessNodes = [];
if (!projectId) return Promise.resolve([]);
return getProcessConfigList(1, 100, { projectIds: projectId, status: 1 })
.then(res => {
const config = extractRecords(res).find(row => {
const projectIds = String(row.projectIds || '')
.split(',')
.map(item => item.trim());
return projectIds.includes(String(projectId));
});
if (!config?.id) return [];
return getProcessConfigDetail(config.id, this.detailRow.id).then(detailRes => {
const detail = detailRes?.data?.data || detailRes?.data || config;
this.waybillHasRelatedVoucher = Boolean(detail.hasRelatedVoucher);
return this.getProcessConfigEnabledNodes(detail);
});
})
.then(nodes => {
if (String(this.detailRow.projectId) === String(projectId)) {
this.waybillDetailProcessNodes = nodes;
}
return nodes;
})
.catch(() => []);
},
loadWaybillVoucherImages() {
if (!this.detailRow.id || this.waybillVoucherImagesLoading || this.waybillVoucherImagesLoaded) {
return;
}
this.waybillVoucherImagesLoading = true;
getProcessConfigVoucherImages(this.detailRow.id)
.then(res => {
this.waybillVoucherImages = extractRecords(res);
this.waybillVoucherImagesLoaded = true;
})
.finally(() => {
this.waybillVoucherImagesLoading = false;
});
},
previewWaybillVoucherImage(index) {
this.attachmentImagePreviewUrls = this.waybillVoucherImages.map(image => image.url).filter(Boolean);
this.attachmentImagePreviewIndex = index;
this.attachmentImagePreviewVisible = this.attachmentImagePreviewUrls.length > 0;
},
buildWaybillRouteNodes(rows = []) {
const nodes = [];
rows.forEach((row, rowIndex) => {
[
['departure', '发'],
['arrival', '收'],
].forEach(([field, tag]) => {
const address = row[`${field}Address`] || row[`${field}Name`];
if (!address) return;
nodes.push({
key: `${row.id || row.waybillNo || rowIndex}-${field}`,
address,
tags: [tag, row.waybillNo].filter(Boolean),
waybillId: row.id,
waybillNo: row.waybillNo,
field,
});
});
});
return nodes;
},
restoreWaybillRouteChangeRecords() {
const taskInfo = this.parseJsonObject(this.detailRow.taskInfoJson);
this.waybillRouteChangeRecords = Array.isArray(taskInfo.routeChangeRecords)
? [...taskInfo.routeChangeRecords].sort((a, b) =>
String(b.changeTime).localeCompare(String(a.changeTime))
)
: [];
},
openWaybillRouteChangeDialog() {
if (!this.detailRow.id) return;
this.restoreWaybillRouteChangeRecords();
this.waybillRouteChangeTab = 'change';
this.waybillRouteChangeRemark = '';
this.waybillRouteChangeRows = [
{
...this.detailRow,
originalDepartureAddress:
this.detailRow.departureAddress || this.detailRow.departureName || '',
originalArrivalAddress: this.detailRow.arrivalAddress || this.detailRow.arrivalName || '',
departureAddress: this.detailRow.departureAddress || this.detailRow.departureName || '',
arrivalAddress: this.detailRow.arrivalAddress || this.detailRow.arrivalName || '',
},
];
this.waybillRouteChangeNodes = this.buildWaybillRouteNodes(this.waybillRouteChangeRows);
this.waybillRouteChangeBox = true;
},
waybillRouteChangeTypeText(index) {
if (index === 0) return '起';
if (index === this.waybillRouteChangeNodes.length - 1) return '终';
return '经';
},
handleWaybillRouteChangeDragStart(index) {
this.waybillRouteChangeDragIndex = index;
},
handleWaybillRouteChangeDrop(index) {
if (this.waybillRouteChangeDragIndex < 0 || this.waybillRouteChangeDragIndex === index)
return;
const nodes = [...this.waybillRouteChangeNodes];
const [node] = nodes.splice(this.waybillRouteChangeDragIndex, 1);
nodes.splice(index, 0, node);
this.waybillRouteChangeNodes = nodes;
this.waybillRouteChangeDragIndex = -1;
},
updateWaybillRouteChangeNodes() {
this.waybillRouteChangeNodes = this.buildWaybillRouteNodes(this.waybillRouteChangeRows);
},
openWaybillCommonAddressDialog(row, field) {
this.waybillCommonAddressTarget = { row, field };
this.waybillCommonAddressQuery = { addressName: '' };
this.waybillCommonAddressPage.currentPage = 1;
this.waybillCommonAddressBox = true;
this.loadWaybillCommonAddresses();
},
loadWaybillCommonAddresses() {
this.waybillCommonAddressLoading = true;
getCommonAddressList(
this.waybillCommonAddressPage.currentPage,
this.waybillCommonAddressPage.pageSize,
{
...this.waybillCommonAddressQuery,
allDept: 0,
}
)
.then(res => {
const data = res.data?.data || {};
this.waybillCommonAddressRows = data.records || [];
this.waybillCommonAddressPage.total = data.total || 0;
})
.finally(() => {
this.waybillCommonAddressLoading = false;
});
},
selectWaybillCommonAddress(address) {
const target = this.waybillCommonAddressTarget;
if (!target) return;
target.row[`${target.field}Address`] =
[address.regionName, address.detailAddress || address.addressName].filter(Boolean).join('') ||
address.addressName ||
'';
this.updateWaybillRouteChangeNodes();
this.waybillCommonAddressBox = false;
},
async confirmWaybillRouteChange() {
const row = this.waybillRouteChangeRows[0] || {};
const changed =
row.departureAddress !== row.originalDepartureAddress ||
row.arrivalAddress !== row.originalArrivalAddress;
const routeJson = JSON.stringify(this.waybillRouteChangeNodes);
const originalRouteJson = this.detailRow.routeJson || '';
if (!changed && routeJson === originalRouteJson && !this.waybillRouteChangeRemark) {
this.$message.warning('请修改地址、调整路线或填写变更备注');
return;
}
if (typeof this.api.changeRoute !== 'function') {
this.$message.warning('当前接口未接入路线变更功能');
return;
}
const content = changed
? `${row.waybillNo || '运单'}:发货地 ${row.originalDepartureAddress || '-'} → ${
row.departureAddress || '-'
};到货地 ${row.originalArrivalAddress || '-'} → ${row.arrivalAddress || '-'}`
: '调整运输路线顺序';
const records = [
{
changeTime: this.$dayjs().format('YYYY-MM-DD HH:mm:ss'),
changeUser: this.userInfo?.realName || this.userInfo?.account || '当前用户',
content,
remark: this.waybillRouteChangeRemark,
},
...this.waybillRouteChangeRecords,
];
const taskInfo = this.parseJsonObject(this.detailRow.taskInfoJson);
const taskInfoJson = JSON.stringify({
...taskInfo,
routeChangeRecords: records,
});
this.waybillRouteChangeSaving = true;
try {
await this.api.changeRoute({
id: this.detailRow.id,
routeJson,
departureAddress: row.departureAddress,
arrivalAddress: row.arrivalAddress,
taskInfoJson,
});
this.detailRow = {
...this.detailRow,
departureAddress: row.departureAddress,
arrivalAddress: row.arrivalAddress,
routeJson,
taskInfoJson,
};
this.waybillRouteChangeRecords = records;
this.waybillRouteChangeBox = false;
this.$message.success('路线变更已保存');
this.onLoad(this.page, this.query);
} finally {
this.waybillRouteChangeSaving = false;
}
},
handleWaybillTrackAction(action) {
this.$message.info(action === 'playback' ? '暂无可回放的物流轨迹' : '暂无车辆实时定位数据');
},
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;
},
syncContractNoDisabled(type) {
if (!this.config.enableContractForm) return;
const column = this.findColumn?.(this.option.column, 'contractNo');
if (column) {
column.disabled = type !== 'add';
}
},
syncCustomCreateDialogButtons(type) {
if (
!this.config.enableContractCreateActions &&
!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';
},
stageTagType(stage) {
if (stage === 'formal') {
return 'success';
}
if (stage === 'temporary') {
return 'warning';
}
if (stage === 'terminated') {
return 'danger';
}
return 'info';
},
isMonthlySettlement(rule = this.settlementRuleForm) {
return rule.settlementType === '月结';
},
isFixedBillCutoffSettlement(rule = this.settlementRuleForm) {
return this.isMonthlySettlement(rule) && rule.billCycleType === '固定截单日';
},
isFixedCycleSettlement(rule = this.settlementRuleForm) {
return rule.settlementType === '固定天数周期结算';
},
handleSettlementTypeChange(value) {
this.clearSettlementRuleError('settlementType');
if (value === '月结') {
if (!this.settlementRuleForm.billCycleType) {
this.settlementRuleForm.billCycleType = '固定截单日';
}
if (this.settlementRuleForm.billCycleType === '固定截单日') {
this.settlementRuleForm.billCutoffDay = this.settlementRuleForm.billCutoffDay || 25;
}
this.settlementRuleForm.cycleDays = '';
return;
}
this.settlementRuleForm.billCycleType = '';
this.settlementRuleForm.billCutoffDay = '';
if (value !== '固定天数周期结算') {
this.settlementRuleForm.cycleDays = '';
}
},
handleSettlementBillCycleTypeChange(value) {
this.clearSettlementRuleError('billCycleType');
if (value === '固定截单日') {
this.settlementRuleForm.billCutoffDay = this.settlementRuleForm.billCutoffDay || 25;
return;
}
this.settlementRuleForm.billCutoffDay = '';
},
normalizeSettlementRule(rule = this.settlementRuleForm) {
const nextRule = {
...defaultSettlementRule(),
...(rule || {}),
};
if (!this.isMonthlySettlement(nextRule)) {
nextRule.billCycleType = '';
nextRule.billCutoffDay = '';
} else if (!this.isFixedBillCutoffSettlement(nextRule)) {
nextRule.billCutoffDay = '';
}
if (!this.isFixedCycleSettlement(nextRule)) {
nextRule.cycleDays = '';
}
return nextRule;
},
clearSettlementRuleError(field) {
if (!this.settlementRuleErrors[field]) return;
this.settlementRuleErrors = {
...this.settlementRuleErrors,
[field]: '',
};
},
validateSettlementRule() {
const rule = this.normalizeSettlementRule(this.settlementRuleForm);
this.settlementRuleErrors = {};
if (this.isBillingFieldEmpty(rule.billStartDate)) {
this.settlementRuleErrors = { billStartDate: '请选择账单起始日期' };
this.$message.warning('请选择账单起始日期');
return false;
}
if (this.isBillingFieldEmpty(rule.settlementType)) {
this.settlementRuleErrors = { settlementType: '请选择结算类型' };
this.$message.warning('请选择结算类型');
return false;
}
if (this.isMonthlySettlement(rule) && this.isBillingFieldEmpty(rule.billCycleType)) {
this.settlementRuleErrors = { billCycleType: '请选择账单周期类型' };
this.$message.warning('请选择账单周期类型');
return false;
}
if (this.isFixedBillCutoffSettlement(rule)) {
if (this.isBillingFieldEmpty(rule.billCutoffDay)) {
this.settlementRuleErrors = { billCutoffDay: '请选择账单截单日' };
this.$message.warning('请选择账单截单日');
return false;
}
const billCutoffDay = Number(rule.billCutoffDay);
if (!Number.isInteger(billCutoffDay) || billCutoffDay < 1 || billCutoffDay > 31) {
this.settlementRuleErrors = { billCutoffDay: '账单截单日必须为1~31的整数' };
this.$message.warning('账单截单日必须为1~31的整数');
return false;
}
}
if (this.isFixedCycleSettlement(rule)) {
const cycleDays = Number(rule.cycleDays);
if (!Number.isInteger(cycleDays) || cycleDays < 1 || cycleDays > 365) {
this.settlementRuleErrors = { cycleDays: '周期天数必须为1~365的整数' };
this.$message.warning('周期天数必须为1~365的整数');
return false;
}
}
return true;
},
normalizeRow(row, saveAsDraft = false) {
const submitRow = { ...row };
const skipValidation = saveAsDraft && this.config.skipDraftValidation === true;
if (
['shipping_template', 'transport_plan', 'waybill_manage'].includes(
this.config.permission
) &&
!this.validateMobileFields([
[submitRow.departurePhone, '发货联系方式'],
[submitRow.arrivalPhone, '收货联系方式'],
[submitRow.driverPhone, this.isNonRoadTransport ? '联系电话' : '手机号'],
[submitRow.escortPhone, '押运人手机号'],
])
) {
return false;
}
if (this.config.enableTransportPlanForm) {
if (!skipValidation && !this.validateTransportPlanForm(submitRow)) return false;
submitRow.goodsJson = JSON.stringify(this.transportCargoRows);
if (this.config.enableShippingTemplateFreight) {
if (!skipValidation && !this.validateShippingTemplateFreight()) return false;
submitRow.freightJson = this.serializeShippingTemplateFreight();
}
}
if (this.taskInfoFormEnabled) {
if (!skipValidation && !this.validateTaskInfoForm(submitRow)) return false;
this.syncTaskInfoToRow(submitRow);
}
if (this.config.enableAttachmentTable) {
submitRow.attachmentsJson = JSON.stringify(this.attachmentRows);
}
if (this.config.enableContractFileUpload) {
submitRow.contractFileJson = JSON.stringify(this.contractFileRows);
}
if (this.config.enableBillingPlan) {
submitRow.billingPlanJson = JSON.stringify(this.billingPlanRows);
}
if (this.config.enableSettlementRule) {
if (!skipValidation && this.settlementRuleEnabled && !this.validateSettlementRule()) {
return false;
}
submitRow.settlementRuleJson = JSON.stringify(this.normalizeSettlementRule());
}
if (this.config.enableReconciliation) {
submitRow.reconciliationJson = JSON.stringify(this.reconciliationForm);
}
if (this.config.enableChangeRecord) {
submitRow.changeRecordJson = JSON.stringify(this.changeRecordRows);
}
if (this.config.saveAsDraft && saveAsDraft) {
submitRow[this.config.draftStatusProp || 'approvalStatus'] =
this.config.draftStatus || 'draft';
}
delete submitRow.basicInfoTitle;
delete submitRow.templateInfoTitle;
delete submitRow.shippingInfoTitle;
delete submitRow.taskInfoTitle;
delete submitRow.taskInfoForm;
delete submitRow.goodsInfoTitle;
delete submitRow.contractFileTitle;
delete submitRow.billingInfoTitle;
delete submitRow.settlementRuleTitle;
delete submitRow.reconciliationTitle;
delete submitRow.attachmentTitle;
delete submitRow.changeRecordTitle;
delete submitRow.contractPeriod;
Object.keys(submitRow).forEach(key => {
if (typeof submitRow[key] === 'string') {
submitRow[key] = submitRow[key].trim();
}
});
return submitRow;
},
validateMobileFields(fields = []) {
const invalidField = fields.find(
([value]) => String(value || '').trim() && !isMobile(value)
);
if (!invalidField) return true;
this.$message.warning(`${invalidField[1]}格式不正确`);
return false;
},
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.shippingTemplateCurrencyOptions.find(
item => String(item.value).toUpperCase() === String(value || '').toUpperCase()
);
return option?.remark || this.currencyName(option?.label || value);
},
loadShippingTemplateCurrencyOptions() {
if (this.shippingTemplateCurrencyOptions.length || this.shippingTemplateCurrencyLoading) {
return Promise.resolve(this.shippingTemplateCurrencyOptions);
}
this.shippingTemplateCurrencyLoading = true;
return getSystemDictionary({ code: 'currency_type' })
.then(res => {
const currencyNameMap = {
CNY: '人民币',
RMB: '人民币',
USD: '美元',
EUR: '欧元',
JPY: '日元',
GBP: '英镑',
};
this.shippingTemplateCurrencyOptions = 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.shippingTemplateCurrencyOptions.find(
item => String(item.value).toUpperCase() === normalized
);
if (exact) return exact.value;
if (normalized === 'CNY') {
const rmb = this.shippingTemplateCurrencyOptions.find(
item => String(item.value).toUpperCase() === 'RMB'
);
return rmb?.value || value;
}
return value;
};
this.dispatchItemForm.freightCurrency = resolveCurrencyValue(
this.dispatchItemForm.freightCurrency
);
this.taskFreightCurrency = resolveCurrencyValue(this.taskFreightCurrency);
this.shippingTemplateFreight.currency = resolveCurrencyValue(
this.shippingTemplateFreight.currency
);
if (!this.shippingTemplateFreight.currency) {
this.shippingTemplateFreight.currency =
this.shippingTemplateCurrencyOptions[0]?.value || '';
}
return this.shippingTemplateCurrencyOptions;
})
.finally(() => {
this.shippingTemplateCurrencyLoading = false;
});
},
initShippingTemplateFreight() {
if (!this.config.enableShippingTemplateFreight) return;
const freight = this.parseJsonObject(this.form.freightJson);
this.shippingTemplateFreight = {
...defaultShippingTemplateFreight(),
...freight,
freightItems: (freight.freightItems || []).map(item => ({
...defaultShippingTemplateFreightItem(),
...item,
})),
};
this.syncShippingTemplateFreightItems();
this.loadShippingTemplateCurrencyOptions();
},
syncShippingTemplateFreightItems() {
if (!this.config.enableShippingTemplateFreight || this.transportMode !== 'road') return;
const rows = this.shippingTemplateFreight.freightItems || [];
this.shippingTemplateFreight.freightItems = this.transportCargoRows.map((_, index) => ({
...defaultShippingTemplateFreightItem(),
...(rows[index] || {}),
}));
},
handleShippingTemplateFreightNumberInput(row, prop, value) {
const text = String(value || '').replace(/[^\d.]/g, '');
const parts = text.split('.');
row[prop] =
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0];
},
shippingTemplateRoadFreightQuantity(index) {
return this.formatShippingTemplateFreightNumber(
this.transportCargoRows[index]?.quantity || 0
);
},
shippingTemplateRoadFreightAmount(item, index) {
const amount =
Number(item?.unitPrice || 0) * Number(this.transportCargoRows[index]?.quantity || 0);
return this.formatShippingTemplateFreightNumber(amount);
},
formatShippingTemplateFreightNumber(value) {
const number = Number(value || 0);
if (!Number.isFinite(number)) return '';
return Number(number.toFixed(2)).toString();
},
serializeShippingTemplateFreight() {
if (this.form.templateType !== '运单') return '';
const freight = {
currency: this.shippingTemplateFreight.currency || '',
otherFreightAmount: this.shippingTemplateFreight.otherFreightAmount || '',
};
if (this.transportMode === 'road') {
freight.totalFreightAmount = this.shippingTemplateRoadFreightTotal;
freight.freightItems = this.shippingTemplateFreight.freightItems.map((item, index) => ({
cargoIndex: index,
unitPrice: item.unitPrice || '',
priceUnit: item.priceUnit || '',
quantity: this.shippingTemplateRoadFreightQuantity(index),
freightAmount: this.shippingTemplateRoadFreightAmount(item, index),
}));
} else {
freight.quantity = this.transportCargoQuantityTotal;
freight.freightAmount = this.shippingTemplateFreight.freightAmount || '';
}
return JSON.stringify(freight);
},
validateShippingTemplateFreight() {
if (!this.shippingTemplateFreightVisible) return true;
if (!this.shippingTemplateFreight.currency) {
this.$message.warning('请选择币种');
return false;
}
if (this.transportMode === 'road') {
const invalid = this.shippingTemplateFreight.freightItems.findIndex(
item => Number(item.unitPrice || 0) < 0
);
if (invalid > -1) {
this.$message.warning(`第${invalid + 1}条货物单价不能小于0`);
return false;
}
const missingPriceUnit = this.shippingTemplateFreight.freightItems.findIndex(
item => !item.priceUnit
);
if (missingPriceUnit > -1) {
this.$message.warning(`请选择第${missingPriceUnit + 1}条货物的计价单位`);
return false;
}
} else if (Number(this.shippingTemplateFreight.freightAmount || 0) < 0) {
this.$message.warning('运费不能小于0');
return false;
}
if (Number(this.shippingTemplateFreight.otherFreightAmount || 0) < 0) {
this.$message.warning('其他运费合计不能小于0');
return false;
}
return true;
},
openShippingTemplateProcessConfig() {
if (!this.form.projectId || this.shippingTemplateProcessConfigLoading) return;
this.shippingTemplateProcessConfigBox = true;
this.shippingTemplateProcessConfigLoading = true;
this.getProjectProcessConfigRows(this.form.projectId)
.then(rows => {
return Promise.all(
rows.map(row =>
getProcessConfigDetail(row.id)
.then(res => ({ ...row, ...(res.data?.data || res.data || {}) }))
.catch(() => row)
)
);
})
.then(rows => {
this.shippingTemplateProcessConfigRows = rows;
})
.finally(() => {
this.shippingTemplateProcessConfigLoading = false;
});
},
getProjectProcessConfigRows(projectId) {
if (!projectId) return Promise.resolve([]);
return getProcessConfigList(1, 100, { projectIds: projectId, status: 1 }).then(res =>
extractRecords(res).filter(row => {
const projectIds = String(row.projectIds || '')
.split(',')
.map(item => item.trim());
return Number(row.status) === 1 && projectIds.includes(String(projectId));
})
);
},
loadProjectProcessConfigState() {
const projectId = this.form.projectId;
this.hasProjectProcessConfig = false;
if (!projectId) return Promise.resolve([]);
return this.getProjectProcessConfigRows(projectId)
.then(rows => {
if (String(this.form.projectId) === String(projectId)) {
this.hasProjectProcessConfig = rows.length > 0;
}
return rows;
})
.catch(() => []);
},
getProcessConfigEnabledNodes(row = {}) {
let storedNodes = [];
try {
const parsed = JSON.parse(row.nodeConfigJson || '[]');
storedNodes = Array.isArray(parsed) ? parsed : parsed.nodes || [];
} catch (error) {
storedNodes = [];
}
const includedNodes = String(row.includedNodes || '')
.split(',')
.map(item => item.trim())
.filter(Boolean);
return storedNodes.filter(
node =>
node.enabled !== false && (!includedNodes.length || includedNodes.includes(node.name))
);
},
processConfigTimelineDescriptions(node = {}) {
const descriptions = [];
if (node.key === 'accept') {
descriptions.push(
`是否接单:${node.confirmMode === 'no_confirm_accept' ? '无需确认接单' : '是'}`
);
if (node.confirmMode === 'yes' && node.confirmDriver) {
descriptions.push('确认接单人:司机');
}
return descriptions;
}
if (node.key === 'return') {
descriptions.push(
`是否确认:${node.confirmMode === 'no_confirm_complete' ? '无需确认完成' : '是'}`
);
if (node.confirmMode === 'yes' && node.confirmInternal) {
descriptions.push('确认完成:内部人员');
}
} else {
descriptions.push(`打卡操作:${node.punch ? '是' : '否'}`);
descriptions.push(`打卡定位:${node.location ? '是' : '否'}`);
if (node.key === 'transit' && node.punch) {
descriptions.push(`打卡频次:每${node.frequencyDays || 1}天打卡1次`);
descriptions.push(`打卡时段:${node.timeStart || '00:00'}-${node.timeEnd || '23:59'}`);
}
if (node.supportCargo && node.uploadCargo && node.cargoTypes?.length) {
descriptions.push(`上传货量:${node.cargoTypes.join('、')}`);
}
}
if (node.supportVoucher && node.uploadVoucher && node.voucherTypes?.length) {
descriptions.push(`上传凭证:${node.voucherTypes.join('、')}`);
}
return descriptions;
},
formatShippingTemplateProcessNodes(nodes) {
if (Array.isArray(nodes)) return nodes.join('、');
return String(nodes || '')
.split(',')
.filter(Boolean)
.join('、');
},
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();
},
error => {
window.console.log(error);
this.stopSubmitLoading(loading);
}
);
},
rowUpdate(row, index, done, loading) {
const submitRow = this.normalizeRow(row);
if (!submitRow) {
this.stopSubmitLoading(loading);
return;
}
const request =
this.attachmentUploadMode && typeof this.api.updateAttachments === 'function'
? this.api.updateAttachments
: this.getSaveRequest();
request(submitRow).then(
() => {
this.attachmentUploadMode = false;
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
done();
},
error => {
this.attachmentUploadMode = false;
window.console.log(error);
this.stopSubmitLoading(loading);
}
);
},
getSaveRequest() {
return this.api.submit;
},
buildContractCreateSubmitRow(action) {
const contractStage = action === 'draft' ? 'draft' : 'temporary';
const submitRow = this.normalizeRow({
...this.form,
contractStage,
approvalStatus: 'draft',
});
return submitRow;
},
validateContractCreateAction(action, row) {
if (!row) return false;
if (this.isBillingFieldEmpty(row.contractName)) {
this.$message.warning('请输入合同名称');
return false;
}
if (this.isBillingFieldEmpty(row.contractCategory)) {
this.$message.warning('请选择合同类别');
return false;
}
if (['temporary', 'formal'].includes(action) && this.isBillingFieldEmpty(row.signType)) {
this.$message.warning('请选择签约类型');
return false;
}
if (action !== 'formal') return true;
const formalFields = [
['partyA', '甲方'],
['partyB', '乙方'],
['startDate', '合同期限'],
['signDate', '签订日期'],
];
const emptyField = formalFields.find(([prop]) => this.isBillingFieldEmpty(row[prop]));
if (emptyField) {
this.$message.warning(`请选择或输入${emptyField[1]}`);
return false;
}
return true;
},
extractSavedContract(res, fallback = {}) {
const data = res?.data?.data || res?.data || {};
if (data && typeof data === 'object' && !Array.isArray(data)) {
return {
...fallback,
...data,
};
}
return fallback;
},
getSavedContractId(savedContract) {
const id = savedContract?.id || this.form.id;
if (!id) {
this.$message.warning('合同保存成功但未返回主键,无法继续流转');
return '';
}
return id;
},
handleContractCreateAction(action) {
if (!this.config.enableContractCreateActions || this.contractCreateActionLoading) return;
const submitRow = this.buildContractCreateSubmitRow(action);
if (!this.validateContractCreateAction(action, submitRow)) return;
this.contractCreateActionLoading = action;
this.api
.saveDraft(submitRow)
.then(res => {
const savedContract = this.extractSavedContract(res, submitRow);
this.form.id = savedContract.id || this.form.id;
if (action === 'draft') {
return savedContract;
}
const id = this.getSavedContractId(savedContract);
if (!id) return Promise.reject(new Error('合同主键为空'));
if (action === 'formal') {
return this.api.submitFormal(id).then(() => savedContract);
}
return savedContract;
})
.then(() => {
const messageMap = {
draft: '保存成功',
temporary: '临时合同提交成功',
formal: '正式合同提交成功',
};
this.$message.success(messageMap[action]);
this.onLoad(this.page, this.query);
this.closeCrudDialog();
})
.catch(error => {
if (error?.message !== '合同主键为空') {
window.console.log(error);
}
})
.finally(() => {
this.contractCreateActionLoading = '';
});
},
handleTransportPlanCreateAction(action) {
if (!this.config.enableTransportPlanCreateActions || 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 = '';
});
},
handleDraftSave() {
const request =
typeof this.api.saveDraft === 'function' ? this.api.saveDraft : this.api.submit;
const submitRow = this.normalizeRow(this.form, true);
if (!submitRow) return;
this.draftSaveLoading = true;
request(submitRow)
.then(() => {
this.$message.success(`${this.config.draftSaveText || '保存'}成功`);
this.onLoad(this.page, this.query);
this.closeCrudDialog();
})
.finally(() => {
this.draftSaveLoading = false;
});
},
closeCrudDialog() {
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.settlementRuleErrors = {};
this.syncCustomCreateDialogButtons(type);
this.dialogReadonly = type === 'view';
this.syncContractNoDisabled(type);
if (type === 'add') {
this.form = {
...(this.config.defaultForm || {}),
};
this.form.taskEntryMode = 'simple';
this.selectedProjectId = '';
this.selectedOrganizationId = '';
this.attachmentRows = [];
this.selectedAttachmentRows = [];
this.contractFileRows = [];
this.selectedContractFileRows = [];
this.shippingPlanOptions = [];
if (this.contractSelectEnabled) {
this.contractOptions = [];
this.form.contractId = '';
this.form.contractName = '';
}
if (this.shippingPlanSelectEnabled) {
this.clearShippingPlan();
}
this.transportCargoRows = [];
this.taskFreightCurrency = 'RMB';
this.hasProjectProcessConfig = false;
this.shippingTemplateFreight = defaultShippingTemplateFreight();
this.billingPlanRows = [];
this.settlementRuleForm = defaultSettlementRule();
this.reconciliationForm = defaultReconciliation();
this.changeRecordRows = [];
this.applyCurrentUserHandler();
this.initTaskInfoForm();
this.initTransportPlanFormRows();
this.syncContractPeriod(false);
this.syncBillingPlanJson();
if (this.config.enableTemplateCodePreview && typeof this.api.nextCode === 'function') {
this.api
.nextCode()
.then(res => {
this.form.templateCode = res.data?.data || '';
})
.finally(done);
return;
}
done();
return;
}
if (['edit', 'view'].includes(type)) {
this.api.getDetail(this.form.id).then(res => {
this.applyFormDetail(res.data.data || {});
done();
});
return;
}
done();
},
applyFormDetail(row) {
this.suppressTransportTypeClear = true;
this.form = { ...(row || {}) };
['quantity', 'mileage', 'unitPrice'].forEach(prop => {
if (Number(this.form[prop]) === -1) this.form[prop] = '';
});
this.form.mileage = this.normalizeMileageValue(this.form.mileage);
this.$nextTick(() => {
this.suppressTransportTypeClear = false;
});
this.selectedProjectId = this.form.projectId || '';
this.loadProjectProcessConfigState();
this.selectedOrganizationId = this.form.organizationId || '';
this.attachmentRows = this.parseJsonArray(this.form.attachmentsJson);
this.selectedAttachmentRows = [];
this.contractFileRows = this.parseJsonArray(this.form.contractFileJson);
this.selectedContractFileRows = [];
this.initTransportPlanFormRows();
this.billingPlanRows = this.parseJsonArray(this.form.billingPlanJson);
this.settlementRuleForm = this.normalizeSettlementRule({
...defaultSettlementRule(),
...this.parseJsonObject(this.form.settlementRuleJson),
});
this.reconciliationForm = {
...defaultReconciliation(),
...this.parseJsonObject(this.form.reconciliationJson),
};
this.changeRecordRows = this.parseJsonArray(this.form.changeRecordJson);
this.applyCurrentUserHandler();
this.initTaskInfoForm();
this.syncContractPeriod(false);
this.syncBillingPlanJson();
if (this.config.enableOrganizationSelect) {
this.syncCurrentOrganizationOption();
}
if (this.config.enableProjectSelect) {
this.form.remainingFundLimit = this.calculateRemainingFundLimit();
this.syncCurrentProjectOption();
}
if (this.contractSelectEnabled) {
this.syncCurrentContractOption();
}
if (this.shippingPlanSelectEnabled) {
this.syncCurrentShippingPlanOption();
}
},
parseJsonArray(value) {
if (!value) return [];
if (Array.isArray(value)) return value;
try {
const list = JSON.parse(value);
return Array.isArray(list) ? list : [];
} catch (error) {
return [];
}
},
parseJsonObject(value) {
if (!value) return {};
if (typeof value === 'object' && !Array.isArray(value)) return value;
try {
const data = JSON.parse(value);
return data && typeof data === 'object' && !Array.isArray(data) ? data : {};
} catch (error) {
return {};
}
},
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 || '吨';
},
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;
},
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).filter(
item => !this.config.excludeVoidedProjects || item.approvalStatus !== 'voided'
);
this.syncCurrentProjectOption();
})
.finally(() => {
this.projectLoading = false;
});
},
syncCurrentProjectOption() {
if (!this.form.projectId || !this.form.projectName) return;
if (this.config.excludeVoidedProjects) return;
const exists = this.projectOptions.some(
item => String(item.id) === String(this.form.projectId)
);
if (!exists) {
this.projectOptions.unshift({
id: this.form.projectId,
projectName: this.form.projectName,
projectCode: this.form.projectCode,
undertakeDeptName: this.form.undertakeDeptName,
fundLimit: this.form.projectFundLimit,
});
}
},
calculateRemainingFundLimit() {
const projectFundLimit = Number(this.form.projectFundLimit);
const usedFundLimit = Number(this.form.usedFundLimit);
if (!Number.isFinite(projectFundLimit) || !Number.isFinite(usedFundLimit)) return '';
return Math.round((projectFundLimit - usedFundLimit + Number.EPSILON) * 100) / 100;
},
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 = '';
this.form.projectFundLimit = '';
this.form.usedFundLimit = 0;
this.form.remainingFundLimit = 0;
if (this.contractSelectEnabled) {
this.form.contractId = '';
this.form.contractName = '';
this.contractOptions = [];
}
if (this.shippingPlanSelectEnabled) {
this.clearShippingPlan();
this.shippingPlanOptions = [];
}
this.hasProjectProcessConfig = false;
return;
}
this.form.projectId = project.id;
this.form.projectName = project.projectName || '';
this.form.projectCode = project.projectCode || '';
this.form.undertakeDeptName = project.undertakeDeptName || '';
this.form.projectFundLimit = project.fundLimit || project.projectFundLimit || 0;
this.form.usedFundLimit = 0;
this.form.remainingFundLimit = this.calculateRemainingFundLimit();
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);
}
if (this.shippingPlanSelectEnabled) {
this.clearShippingPlan();
this.loadShippingPlanOptions();
}
this.loadProjectProcessConfigState();
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,
})
.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,
});
}
},
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.customerName =
contract.customerName || contract.customerNames || this.form.customerName || '';
this.$refs.crud?.validateField?.('contractName');
},
loadShippingPlanOptions() {
if (!this.shippingPlanSelectEnabled || this.shippingPlanLoading) {
return Promise.resolve([]);
}
this.shippingPlanLoading = true;
return getTransportPlanList(1, 9999, {
projectId: this.form.projectId,
projectName: this.form.projectName,
})
.then(res => {
this.shippingPlanOptions = extractRecords(res);
this.syncCurrentShippingPlanOption();
return this.shippingPlanOptions;
})
.finally(() => {
this.shippingPlanLoading = false;
});
},
syncCurrentShippingPlanOption() {
if (!this.form.planName) return;
const exists = this.shippingPlanOptions.some(
item =>
String(item.id) === String(this.form.planId) ||
String(item.planName) === String(this.form.planName)
);
if (!exists) {
this.shippingPlanOptions.unshift({
id: this.form.planId || this.form.planName,
planName: this.form.planName,
});
}
},
handleShippingPlanChange(planId) {
const plan = this.shippingPlanOptions.find(item => String(item.id) === String(planId));
if (!plan) {
this.clearShippingPlan();
return;
}
this.shippingPlanLoading = true;
getTransportPlanDetail(plan.id)
.then(res => this.applyShippingPlan(res.data?.data || res.data || plan))
.finally(() => {
this.shippingPlanLoading = false;
});
},
applyShippingPlan(plan = {}) {
this.suppressTransportTypeClear = true;
this.form.planId = plan.id || '';
this.form.planName = plan.planName || '';
[
'contractId',
'contractName',
'customerName',
'transportType',
'transportMode',
'transportTypeName',
'transportModeName',
'departureAddressId',
'departureName',
'departureAddress',
'departureContact',
'departurePhone',
'departureProvince',
'departureCity',
'departureDistrict',
'arrivalAddressId',
'arrivalName',
'arrivalAddress',
'arrivalContact',
'arrivalPhone',
'arrivalProvince',
'arrivalCity',
'arrivalDistrict',
].forEach(prop => {
if (plan[prop] !== undefined) this.form[prop] = plan[prop];
});
this.form.estimatedStartTime = plan.planStartDate || plan.estimatedStartTime || '';
this.form.estimatedEndTime = plan.planEndDate || plan.estimatedEndTime || '';
this.form.goodsJson =
typeof plan.goodsJson === 'string' ? plan.goodsJson : JSON.stringify(plan.goodsJson || []);
this.attachmentRows = this.parseJsonArray(plan.attachmentsJson);
this.form.attachmentsJson = JSON.stringify(this.attachmentRows);
const goodsRows = this.parseJsonArray(this.form.goodsJson);
this.form.taskEntryMode = goodsRows.length > 1 ? 'full' : 'simple';
const firstGoods = goodsRows[0] || {};
[
'cargoType',
'cargoTypeCode',
'cargoName',
'specification',
'model',
'quantity',
'quantityUnit',
].forEach(prop => {
if (firstGoods[prop] !== undefined) this.form[prop] = firstGoods[prop];
});
this.form.cargoTypePath = this.normalizeBillingCargoTypePath(firstGoods.cargoTypePath);
this.initTaskInfoForm();
this.ensureBillingCargoTypeOptions().then(() => {
if (this.form.cargoTypePath.length) return;
const cargoType = this.findBillingCargoTypeByCodeOrName({
cargoTypeCode: this.form.cargoTypeCode,
cargoType: this.form.cargoType,
});
if (cargoType?.path) this.form.cargoTypePath = cargoType.path;
});
if (this.isTaskFullMode) {
this.initTaskFullCargoRows(goodsRows);
}
if (this.contractSelectEnabled) this.syncCurrentContractOption();
this.$refs.crud?.validateField?.('planName');
this.$nextTick(() => {
this.suppressTransportTypeClear = false;
});
},
clearShippingPlan() {
this.form.planId = '';
this.form.planName = '';
},
initTaskInfoForm() {
if (!this.taskInfoFormEnabled) return;
const taskInfo = this.parseJsonObject(this.form.taskInfoJson);
const freightInfo = this.parseJsonObject(this.form.freightJson);
const carrierRows = this.parseJsonArray(this.form.carrierJson);
const carrierInfo = carrierRows[0] || this.parseJsonObject(this.form.carrierJson);
const goodsRows = this.parseJsonArray(this.form.goodsJson);
const goodsInfo = goodsRows[0] || {};
this.form.taskEntryMode =
this.form.taskEntryMode || taskInfo.taskEntryMode || carrierInfo.taskEntryMode || 'simple';
if (this.hasMultipleConfiguredGoods(this.form, goodsRows)) {
this.form.taskEntryMode = 'full';
}
this.form.carrierType =
this.form.carrierType ||
taskInfo.carrierType ||
carrierInfo.carrierType ||
carrierInfo.type ||
'承运商';
this.form.carrierName =
this.form.carrierName ||
taskInfo.carrierName ||
carrierInfo.carrierName ||
carrierInfo.customerName ||
carrierInfo.name ||
'';
this.form.driverName =
this.form.driverName || taskInfo.driverName || carrierInfo.driverName || '';
this.form.driverPhone =
this.form.driverPhone ||
taskInfo.driverPhone ||
carrierInfo.driverPhone ||
carrierInfo.mobile ||
'';
this.form.vehicleNo =
this.form.vehicleNo || taskInfo.vehicleNo || carrierInfo.vehicleNo || '';
this.form.captainName = this.form.captainName || taskInfo.captainName || '';
this.form.cabinNo = this.form.cabinNo || taskInfo.cabinNo || '';
this.form.containerNo = this.form.containerNo || taskInfo.containerNo || '';
this.form.trailerVehicleNo =
this.form.trailerVehicleNo ||
taskInfo.trailerVehicleNo ||
carrierInfo.trailerVehicleNo ||
'';
this.form.escortName =
this.form.escortName || taskInfo.escortName || carrierInfo.escortName || '';
this.form.escortPhone =
this.form.escortPhone || taskInfo.escortPhone || carrierInfo.escortPhone || '';
this.form.cargoType =
this.form.cargoType ||
taskInfo.cargoType ||
goodsInfo.cargoType ||
goodsInfo.goodsType ||
'';
this.form.cargoTypeCode =
this.form.cargoTypeCode ||
taskInfo.cargoTypeCode ||
goodsInfo.cargoTypeCode ||
goodsInfo.goodsTypeCode ||
'';
this.form.cargoName =
this.form.cargoName ||
taskInfo.cargoName ||
goodsInfo.cargoName ||
goodsInfo.goodsName ||
'';
this.form.specification =
this.form.specification || taskInfo.specification || goodsInfo.specification || '';
this.form.model = this.form.model || taskInfo.model || goodsInfo.model || '';
this.form.quantity = this.form.quantity || taskInfo.quantity || goodsInfo.quantity || '';
this.form.quantityUnit =
this.form.quantityUnit || taskInfo.quantityUnit || goodsInfo.quantityUnit || '吨';
const mileage = this.form.mileage || taskInfo.mileage || goodsInfo.mileage || '';
this.form.mileage = this.normalizeMileageValue(mileage);
this.form.unitPrice = this.form.unitPrice || taskInfo.unitPrice || goodsInfo.unitPrice || '';
['quantity', 'mileage', 'unitPrice'].forEach(prop => {
if (Number(this.form[prop]) === -1) this.form[prop] = '';
});
this.form.priceUnit =
this.form.priceUnit || taskInfo.priceUnit || goodsInfo.priceUnit || '元/吨';
this.form.otherFeeTotal =
this.form.otherFeeTotal || taskInfo.otherFeeTotal || goodsInfo.otherFeeTotal || '';
if (Number(this.form.otherFeeTotal) === -1) {
this.form.otherFeeTotal = '';
}
this.form.estimatedStartTime =
this.form.estimatedStartTime ||
taskInfo.estimatedStartTime ||
goodsInfo.estimatedStartTime ||
'';
this.form.estimatedEndTime =
this.form.estimatedEndTime || taskInfo.estimatedEndTime || goodsInfo.estimatedEndTime || '';
this.form.taskRemark = this.form.taskRemark || taskInfo.taskRemark || goodsInfo.remark || '';
this.taskFreightCurrency = freightInfo.currency || this.taskFreightCurrency || 'RMB';
this.loadShippingTemplateCurrencyOptions();
this.form.cargoTypePath = this.normalizeBillingCargoTypePath(this.form.cargoTypePath);
if (!this.form.cargoTypePath.length) {
this.ensureBillingCargoTypeOptions().then(() => {
const cargoType = this.findBillingCargoTypeByCodeOrName({
cargoTypeCode: this.form.cargoTypeCode,
cargoType: this.form.cargoType,
});
if (cargoType?.path) {
this.form.cargoTypePath = cargoType.path;
}
});
}
if (this.isTaskFullMode) {
this.initTaskFullCargoRows(goodsRows);
}
},
handleTaskEntryModeChange(value) {
if (value === 'simple' && this.hasMultipleConfiguredGoods(this.form, this.transportCargoRows)) {
this.form.taskEntryMode = 'full';
this.$message.warning('当前配置了多条货物信息,不能切换为精简录入');
return;
}
this.form.taskEntryMode = value || 'simple';
if (this.form.taskEntryMode === 'full') {
this.initTaskFullCargoRows(this.parseJsonArray(this.form.goodsJson));
}
},
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()];
}
},
handleTaskFullFreightNumberInput(cargo, prop, value) {
const text = String(value || '').replace(/[^\d.]/g, '');
const parts = text.split('.');
cargo[prop] =
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0];
this.syncTransportCargoJson();
},
taskFullFreightQuantity(index) {
return this.formatTaskFullFreightNumber(this.transportCargoRows[index]?.quantity || 0);
},
taskFullFreightAmount(cargo = {}) {
return this.formatTaskFullFreightNumber(
Number(cargo.unitPrice || 0) * Number(cargo.quantity || 0)
);
},
formatTaskFullFreightNumber(value) {
const number = Number(value || 0);
if (!Number.isFinite(number)) return '';
return Number(number.toFixed(2)).toString();
},
handleTaskCarrierTypeChange(value) {
const nextValue = value || '承运商';
if (nextValue === '承运商') {
this.form.trailerVehicleNo = '';
this.form.escortName = '';
this.form.escortPhone = '';
} else {
this.form.carrierName = '';
this.form.carrierId = '';
}
this.form.carrierType = nextValue;
},
loadTaskCarrierOptions() {
if (!this.taskInfoFormEnabled || this.taskCarrierLoading) return Promise.resolve([]);
this.taskCarrierLoading = true;
return getCarrierCustomerList(1, 9999, { customerType: '承运商', status: 1 })
.then(res => {
this.taskCarrierOptions = extractRecords(res).filter(item => Number(item.status) === 1);
return this.taskCarrierOptions;
})
.finally(() => {
this.taskCarrierLoading = false;
});
},
handleTaskCarrierChange(value) {
const carrier = this.taskCarrierOptions.find(item =>
[item.customerName, item.carrierName, item.fullName, item.name].some(
name => String(name || '') === value
)
);
this.form.carrierId = carrier?.id || '';
this.form.carrierName = value || '';
},
loadTaskDriverOptions() {
if (!this.taskInfoFormEnabled || this.taskDriverLoading) return Promise.resolve([]);
this.taskDriverLoading = true;
return getDriverList(1, 9999, {})
.then(res => {
this.taskDriverOptions = extractRecords(res);
return this.taskDriverOptions;
})
.finally(() => {
this.taskDriverLoading = false;
});
},
fetchTaskDriverSuggestions(queryString, callback) {
const keyword = String(queryString || '').trim();
this.taskDriverLoading = true;
getDriverList(1, 20, keyword ? { driverName: keyword } : {})
.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;
});
},
handleTaskDriverSelect(target, item = {}) {
const value = item.driverName || item.name || '';
const driverPhone = item.mobile || item.phone || item.driverPhone || '';
if (target === 'dispatch') {
this.dispatchItemForm.driverId = item.id || '';
this.dispatchItemForm.driverName = value;
if (driverPhone) this.dispatchItemForm.driverPhone = driverPhone;
return;
}
this.form.driverId = item.id || '';
this.form.driverName = value;
if (driverPhone) this.form.driverPhone = driverPhone;
},
handleTaskDriverChange(value) {
const driver = this.taskDriverOptions.find(item =>
[item.driverName, item.name].some(name => String(name || '') === String(value || ''))
);
this.form.driverId = driver?.id || '';
this.form.driverName = value || '';
if (driver) {
this.form.driverPhone =
driver.mobile || driver.phone || driver.driverPhone || this.form.driverPhone || '';
}
},
getTaskCargoTypeOptionsKey(path = []) {
const normalizedPath = this.normalizeBillingCargoTypePath(path);
if (!normalizedPath.length) return '';
const cargoType = this.findBillingCargoTypeByPath(normalizedPath);
return String(cargoType?.cargoCode || cargoType?.code || normalizedPath.join('/'));
},
getTaskCargoOptionsByPath(path = []) {
const key = this.getTaskCargoTypeOptionsKey(path);
return key ? this.taskCargoOptionsMap[key] || [] : [];
},
isTaskCargoOptionsLoading(path = []) {
const key = this.getTaskCargoTypeOptionsKey(path);
return key ? !!this.taskCargoLoadingMap[key] : false;
},
setTaskCargoOptionsByKey(key, options = []) {
if (!key) return;
this.taskCargoOptionsMap = {
...this.taskCargoOptionsMap,
[key]: options,
};
},
setTaskCargoLoadingByKey(key, loading) {
if (!key) return;
this.taskCargoLoadingMap = {
...this.taskCargoLoadingMap,
[key]: loading,
};
},
buildTaskCargoListQueryParams(path = []) {
const normalizedPath = this.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);
if (!keyword && !normalizedPath.length) {
callback([]);
return;
}
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);
});
},
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 || '';
this.syncTransportCargoJson();
},
loadTaskCargoOptionsForForm() {
return this.loadTaskCargoOptionsByPath(this.form.cargoTypePath);
},
handleTaskCargoTypeChange(value) {
const path = this.normalizeBillingCargoTypePath(value);
const cargoType = this.findBillingCargoTypeByPath(path);
const labels = this.getBillingCargoTypePathLabels(path);
this.form.cargoTypePath = path;
this.form.cargoType = labels.length ? labels[labels.length - 1] : '';
this.form.cargoTypeCode = cargoType?.cargoCode || cargoType?.code || cargoType?.id || '';
this.form.cargoName = '';
this.form.specification = '';
this.form.model = '';
this.loadTaskCargoOptionsForForm();
},
handleTaskCargoChange(value) {
const cargo = this.taskCargoOptions.find(
item => this.formatBillingCargoTypeLabel(item) === value
);
this.form.cargoName = value || '';
if (cargo) {
this.form.specification =
cargo.specification || cargo.spec || this.form.specification || '';
this.form.model = cargo.model || cargo.modelName || this.form.model || '';
}
},
handleTaskCargoSelect(item = {}) {
this.form.cargoName = this.formatBillingCargoTypeLabel(item);
this.form.specification = item.specification || item.spec || this.form.specification || '';
this.form.model = item.model || item.modelName || this.form.model || '';
},
getTaskCargoRowOptions(row = {}) {
return this.getTaskCargoOptionsByPath(row.cargoTypePath);
},
handleTaskCargoRowNameChange(row, value) {
const cargo = this.getTaskCargoRowOptions(row).find(
item => this.formatBillingCargoTypeLabel(item) === value
);
row.cargoName = value || '';
if (cargo) {
row.specification = cargo.specification || cargo.spec || row.specification || '';
row.model = cargo.model || cargo.modelName || row.model || '';
}
this.syncTransportCargoJson();
},
initTaskFullCargoRows(goodsRows = []) {
if (!this.taskInfoFormEnabled) return;
const rows = (goodsRows.length ? goodsRows : this.transportCargoRows).map((row, index) => {
const cargo = this.normalizeTransportCargoRow(row);
if (index === 0 && !cargo.unitPrice && this.form.unitPrice) {
cargo.unitPrice = this.form.unitPrice;
}
cargo.priceUnit = cargo.priceUnit || this.form.priceUnit || '元/吨';
return cargo;
});
while (rows.length < 1) {
rows.push(this.normalizeTransportCargoRow());
}
this.transportCargoRows = rows;
this.syncTransportCargoJson();
},
loadTaskQuantityUnitOptions() {
if (this.taskQuantityUnitOptions.length || this.taskQuantityUnitLoading) {
return Promise.resolve(this.taskQuantityUnitOptions);
}
this.taskQuantityUnitLoading = true;
return getDictionary({ code: 'quantity_unit' })
.then(res => {
this.taskQuantityUnitOptions = this.normalizeDictOptions(res.data.data || []);
return this.taskQuantityUnitOptions;
})
.finally(() => {
this.taskQuantityUnitLoading = false;
});
},
loadTaskFeeUnitOptions() {
if (this.taskFeeUnitOptions.length || this.taskFeeUnitLoading) {
return Promise.resolve(this.taskFeeUnitOptions);
}
this.taskFeeUnitLoading = true;
return getDictionary({ code: 'unit_fee' })
.then(res => {
this.taskFeeUnitOptions = this.normalizeDictOptions(res.data.data || []);
return this.taskFeeUnitOptions;
})
.finally(() => {
this.taskFeeUnitLoading = false;
});
},
normalizeDictOptions(list = []) {
return (list || []).map(item => ({
label: item.dictValue || item.label || item.value || '',
value: item.dictKey || item.value || item.dictValue || '',
remark: item.remark || '',
}));
},
handleTaskNumberInput(prop, value) {
this.form[prop] = this.normalizeTaskNumber(value);
},
handleTaskMileageInput(value) {
this.form.mileage = String(value || '')
.replace(/\D/g, '')
.slice(0, 10);
},
normalizeMileageValue(value) {
if (value === undefined || value === null || value === '' || Number(value) === -1) return '';
const mileage = Number(value);
return Number.isFinite(mileage) && mileage >= 0 ? String(Math.trunc(mileage)) : '';
},
handleDispatchMileageInput(value) {
this.dispatchItemForm.mileage = String(value || '')
.replace(/\D/g, '')
.slice(0, 10);
},
normalizeTaskNumber(value) {
const text = String(value || '').replace(/[^\d.]/g, '');
const parts = text.split('.');
return parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('')}` : parts[0];
},
validateTaskCargoRows() {
if (!this.transportCargoRows.length) {
this.$message.warning('请至少添加一条货物信息');
return false;
}
for (const [index, cargo] of this.transportCargoRows.entries()) {
const requiredFields = [
['cargoName', '货物名称'],
['cargoType', '货物类型'],
['quantity', '数量'],
['quantityUnit', '数量单位'],
];
const emptyField = requiredFields.find(([prop]) => this.isBillingFieldEmpty(cargo[prop]));
if (emptyField) {
this.$message.warning(`第${index + 1}行${emptyField[1]}不能为空`);
return false;
}
const invalidField = [['quantity', '数量']].find(([prop]) => {
const value = cargo[prop];
return value !== undefined && value !== null && value !== '' && Number(value) < 0;
});
if (invalidField) {
this.$message.warning(`第${index + 1}行${invalidField[1]}不能小于0`);
return false;
}
const invalidPrice = Number(cargo.unitPrice || 0) < 0;
if (invalidPrice) {
this.$message.warning(`第${index + 1}行单价不能小于0`);
return false;
}
}
return true;
},
validateTaskInfoForm(row = {}) {
if (this.isTaskFullMode && !this.validateTaskCargoRows()) {
return false;
}
if (this.isTaskFullMode && !this.taskFreightCurrency) {
this.$message.warning('请选择币种');
return false;
}
const requiredFields = this.config.transportFormRequiredFields || [
['carrierType', '承运类型'],
['vehicleNo', this.transportVehicleNoLabel],
];
if (row.carrierType === '承运商') {
requiredFields.push(['carrierName', '承运商']);
}
if (this.isRoadTransport && row.carrierType === '自运') {
requiredFields.push(['driverName', '司机']);
requiredFields.push(['driverPhone', '手机号']);
}
const emptyField = requiredFields.find(([prop]) => this.isBillingFieldEmpty(row[prop]));
if (emptyField) {
this.$message.warning(`请选择或输入${emptyField[1]}`);
return false;
}
const numberFields = [
['quantity', '数量'],
['mileage', '里程'],
['otherFeeTotal', '其他费用合计'],
];
if (!this.isTaskFullMode) numberFields.push(['unitPrice', '单价']);
const invalidField = numberFields.find(([prop]) => {
const value = row[prop];
return value !== undefined && value !== null && value !== '' && Number(value) < 0;
});
if (invalidField) {
this.$message.warning(`${invalidField[1]}不能小于0`);
return false;
}
if (this.isRoadTransport) {
const invalidLengthField = [
['driverName', '司机', 20],
['driverPhone', '手机号', 20],
['vehicleNo', '车牌号', 30],
['trailerVehicleNo', '挂车车牌号', 30],
['escortName', '押运人', 20],
['escortPhone', '押运人手机号', 20],
].find(([prop, , max]) => String(row[prop] || '').length > max);
if (invalidLengthField) {
this.$message.warning(`${invalidLengthField[1]}不能超过${invalidLengthField[2]}个字符`);
return false;
}
} else {
const invalidLengthField = [
['vehicleNo', '船/航/班列号', 30],
['captainName', '船长', 20],
['cabinNo', '舱位', 30],
['containerNo', '箱号', 30],
].find(([prop, , max]) => String(row[prop] || '').length > max);
if (invalidLengthField) {
this.$message.warning(`${invalidLengthField[1]}不能超过${invalidLengthField[2]}个字符`);
return false;
}
}
if (row.mileage && (!/^\d{1,10}$/.test(String(row.mileage)) || Number(row.mileage) <= 0)) {
this.$message.warning('里程必须为不超过10位的正整数');
return false;
}
if (
row.estimatedStartTime &&
row.estimatedEndTime &&
row.estimatedEndTime < row.estimatedStartTime
) {
this.$message.warning('预计完成日期不能早于预计发货日期');
return false;
}
if (row.taskRemark && row.taskRemark.length > 200) {
this.$message.warning('任务备注不能超过200个字符');
return false;
}
return true;
},
syncTaskInfoToRow(row) {
row.taskEntryMode = row.taskEntryMode || 'simple';
['quantity', 'mileage', 'unitPrice'].forEach(prop => {
if (this.isBillingFieldEmpty(row[prop]) || Number(row[prop]) === -1) row[prop] = '';
});
const taskInfo = {
taskEntryMode: row.taskEntryMode,
carrierType: row.carrierType,
carrierId: row.carrierId,
carrierName: row.carrierName,
driverId: row.driverId,
driverName: row.driverName,
driverPhone: row.driverPhone,
vehicleNo: row.vehicleNo,
captainName: row.captainName,
cabinNo: row.cabinNo,
containerNo: row.containerNo,
trailerVehicleNo: row.trailerVehicleNo,
escortName: row.escortName,
escortPhone: row.escortPhone,
mileage: row.mileage,
estimatedStartTime: row.estimatedStartTime,
estimatedEndTime: row.estimatedEndTime,
unitPrice: row.unitPrice,
priceUnit: row.priceUnit,
taskRemark: row.taskRemark,
};
if (!this.isBillingFieldEmpty(row.otherFeeTotal) && Number(row.otherFeeTotal) !== -1) {
taskInfo.otherFeeTotal = row.otherFeeTotal;
} else {
delete row.otherFeeTotal;
}
row.carrierJson = JSON.stringify([
{
carrierType: row.carrierType,
carrierId: row.carrierId,
carrierName: row.carrierName,
driverId: row.driverId,
driverName: row.driverName,
driverPhone: row.driverPhone,
vehicleNo: row.vehicleNo,
captainName: row.captainName,
cabinNo: row.cabinNo,
containerNo: row.containerNo,
trailerVehicleNo: row.trailerVehicleNo,
escortName: row.escortName,
escortPhone: row.escortPhone,
mileage: row.mileage,
taskEntryMode: row.taskEntryMode,
},
]);
const goodsRows = this.isTaskFullMode
? this.transportCargoRows.map(item => this.normalizeTransportCargoRow(item))
: [
{
cargoType: row.cargoType,
cargoTypeCode: row.cargoTypeCode,
cargoTypePath: row.cargoTypePath,
cargoName: row.cargoName,
specification: row.specification,
model: row.model,
quantity: row.quantity,
quantityUnit: row.quantityUnit,
mileage: row.mileage,
unitPrice: row.unitPrice,
priceUnit: row.priceUnit,
remark: row.taskRemark,
},
];
if (this.isTaskFullMode) {
const firstCargo = goodsRows[0] || {};
taskInfo.unitPrice = firstCargo.unitPrice || '';
taskInfo.priceUnit = firstCargo.priceUnit || '';
row.freightJson = this.serializeTaskFullFreight();
}
row.taskInfoJson = JSON.stringify(taskInfo);
row.goodsJson = JSON.stringify(goodsRows);
const firstGoodsRow =
goodsRows.find(
item =>
!this.isBillingFieldEmpty(item.cargoName) || !this.isBillingFieldEmpty(item.cargoType)
) ||
goodsRows[0] ||
{};
row.cargoType = firstGoodsRow.cargoType || row.cargoType || '';
row.cargoTypeCode = firstGoodsRow.cargoTypeCode || row.cargoTypeCode || '';
row.cargoTypePath = firstGoodsRow.cargoTypePath || row.cargoTypePath || [];
row.cargoName = firstGoodsRow.cargoName || row.cargoName || '';
row.specification = firstGoodsRow.specification || row.specification || '';
row.model = firstGoodsRow.model || row.model || '';
row.quantity = firstGoodsRow.quantity || row.quantity || '';
row.quantityUnit = firstGoodsRow.quantityUnit || row.quantityUnit || '';
row.mileage = firstGoodsRow.mileage || row.mileage || '';
row.unitPrice = firstGoodsRow.unitPrice || (this.isTaskFullMode ? '' : row.unitPrice) || '';
row.priceUnit = firstGoodsRow.priceUnit || (this.isTaskFullMode ? '' : row.priceUnit) || '';
row.taskRemark = firstGoodsRow.remark || row.taskRemark || '';
},
serializeTaskFullFreight() {
return JSON.stringify({
currency: this.taskFreightCurrency || 'RMB',
totalFreightAmount: this.taskFullFreightTotal,
otherFreightAmount: this.form.otherFeeTotal || '',
freightItems: this.transportCargoRows.map((cargo, index) => ({
cargoIndex: index,
cargoName: cargo.cargoName || '',
cargoType: cargo.cargoType || '',
unitPrice: cargo.unitPrice || '',
priceUnit: cargo.priceUnit || '',
quantity: this.taskFullFreightQuantity(index),
quantityUnit: cargo.quantityUnit || '',
freightAmount: this.taskFullFreightAmount(cargo),
})),
});
},
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());
}
if (this.hasMultipleConfiguredGoods(this.form, this.transportCargoRows)) {
this.form.taskEntryMode = 'full';
}
this.initShippingTemplateFreight();
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 || '',
};
['quantity', 'mileage', 'unitPrice'].forEach(prop => {
if (Number(cargo[prop]) === -1) cargo[prop] = '';
});
return cargo;
},
hasMultipleConfiguredGoods(source = {}, rows = []) {
const configuredRows = this.parseJsonArray(
source?.goodsJson || source?.goodsRows || source?.goodsList
);
const list = configuredRows.length ? configuredRows : Array.isArray(rows) ? rows : [];
return list.length > 1;
},
syncTransportCargoJson() {
if (!this.config.enableTransportPlanForm && !this.isTaskFullMode) return;
this.form.goodsJson = JSON.stringify(
this.transportCargoRows.map(row => this.normalizeTransportCargoRow(row))
);
},
addTransportCargoRow(index = -1, row = {}) {
const nextRow = this.normalizeTransportCargoRow({
priceUnit: this.form.priceUnit || '元/吨',
...row,
});
if (
index < 0 &&
this.transportCargoRows.length === 1 &&
this.isEmptyTransportCargoRow(this.transportCargoRows[0])
) {
this.transportCargoRows.splice(0, 1, nextRow);
} else if (index > -1) {
this.transportCargoRows.splice(index + 1, 0, nextRow);
} else {
this.transportCargoRows.push(nextRow);
}
this.syncShippingTemplateFreightItems();
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) {
if (this.config.enableShippingTemplateFreight && this.transportMode === 'road') {
this.shippingTemplateFreight.freightItems.splice(index, 1);
}
this.transportCargoRows.splice(index, 1);
if (
!this.transportCargoRows.length &&
(this.isTaskFullMode || this.config.enableTransportPlanForm)
) {
this.transportCargoRows.push(this.normalizeTransportCargoRow());
}
this.syncShippingTemplateFreightItems();
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.syncShippingTemplateFreightItems();
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.transportRouteSelected = null;
this.transportAddressSelected = null;
this.transportMapSelected = {};
this.transportRouteBox = false;
this.transportAddressBox = false;
this.transportStationBox = false;
this.transportMapBox = false;
this.transportAddressTarget = '';
this.transportStationTarget = '';
this.transportMapTarget = '';
this.transportMapKeyword = '';
this.transportMapStatus = '可搜索地址或点击地图选点';
if (this.transportAmapMarker) {
this.transportAmapMarker.setMap(null);
this.transportAmapMarker = null;
}
this.$refs.crud?.clearValidate?.([
'departureAddress',
'arrivalAddress',
'departureName',
'arrivalName',
'departureContact',
'departurePhone',
'arrivalContact',
'arrivalPhone',
]);
},
getTransportTypeLabel(value) {
const column = (this.option.column || []).find(item => item.prop === 'transportType');
const dictionary = column?.dicData || [];
const item = dictionary.find(
dic =>
String(dic.value ?? dic.dictKey ?? '') === String(value ?? '') ||
String(dic.dictKey ?? '') === String(value ?? '')
);
return item?.label || item?.dictValue || '';
},
resolveTransportMode(value) {
const text = [value, this.form.transportTypeName, this.getTransportTypeLabel(value)]
.filter(Boolean)
.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.shippingInfoFormEnabled || !this.isBillingFieldEmpty(this.form.transportType)) {
return true;
}
this.$message.warning('请先选择运输方式');
return false;
},
openTransportRouteDialog() {
if (!this.ensureTransportTypeBeforeAddress()) return;
this.transportRouteSelected = null;
this.transportRoutePage.currentPage = 1;
this.transportRouteQuery = {};
this.transportRouteBox = true;
},
loadTransportRouteList() {
this.transportRouteLoading = true;
getCommonRouteList(this.transportRoutePage.currentPage, this.transportRoutePage.pageSize, {
...this.transportRouteQuery,
allDept: 0,
})
.then(res => {
const data = res.data.data || {};
this.transportRouteRows = data.records || [];
this.transportRoutePage.total = data.total || 0;
})
.finally(() => {
this.transportRouteLoading = false;
});
},
handleTransportRouteSearch() {
this.transportRoutePage.currentPage = 1;
this.loadTransportRouteList();
},
handleTransportRouteReset() {
this.transportRouteQuery = {};
this.transportRoutePage.currentPage = 1;
this.loadTransportRouteList();
},
handleTransportRouteSizeChange(pageSize) {
this.transportRoutePage.pageSize = pageSize;
this.transportRoutePage.currentPage = 1;
this.loadTransportRouteList();
},
handleTransportRouteCurrentChange(currentPage) {
this.transportRoutePage.currentPage = currentPage;
this.loadTransportRouteList();
},
formatTransportRouteUpdateUserName(row) {
return formatUpdateUserName(row);
},
selectTransportRouteRow(row) {
this.transportRouteSelected = row;
this.applyTransportRoute(row);
this.transportRouteBox = false;
},
applyTransportRoute(route = {}) {
this.applyTransportAddress('departure', {
addressName: route.departureName,
detailAddress: route.departureAddress,
longitude: route.departureLongitude,
latitude: route.departureLatitude,
contactName: route.departureContact,
contactPhone: route.departurePhone,
addressCode: route.departureAddressCode,
siteCode: route.departureSiteCode,
});
this.applyTransportAddress('arrival', {
addressName: route.arrivalName,
detailAddress: route.arrivalAddress,
longitude: route.arrivalLongitude,
latitude: route.arrivalLatitude,
contactName: route.arrivalContact,
contactPhone: route.arrivalPhone,
addressCode: route.arrivalAddressCode,
siteCode: route.arrivalSiteCode,
});
},
openTransportStationDialog(target) {
if (!this.ensureTransportTypeBeforeAddress()) return;
if (!this.transportStationMode) return;
this.transportStationTarget = target;
this.transportStationQuery = {};
this.transportStationPage.currentPage = 1;
this.transportStationBox = true;
},
getTransportStationListRequest() {
const requestMap = {
water: getPortTerminalList,
rail: getRailwayStationList,
air: getAirportMasterList,
};
return requestMap[this.transportMode];
},
loadTransportStationList() {
const request = this.getTransportStationListRequest();
if (!request) return;
this.transportStationLoading = true;
request(
this.transportStationPage.currentPage,
this.transportStationPage.pageSize,
this.buildTransportStationQueryParams()
)
.then(res => {
const data = res.data.data || {};
this.transportStationRows = (data.records || []).map(row =>
this.normalizeTransportStationRow(row)
);
this.transportStationPage.total = data.total || 0;
})
.finally(() => {
this.transportStationLoading = false;
});
},
buildTransportStationQueryParams() {
const query = this.transportStationQuery || {};
return Object.keys(query).reduce((params, key) => {
const value = query[key];
if (value !== undefined && value !== null && value !== '') {
params[key] = value;
}
return params;
}, {});
},
handleTransportStationSearch() {
this.transportStationPage.currentPage = 1;
this.loadTransportStationList();
},
handleTransportStationReset() {
this.transportStationQuery = {};
this.transportStationPage.currentPage = 1;
this.loadTransportStationList();
},
handleTransportStationSizeChange(pageSize) {
this.transportStationPage.pageSize = pageSize;
this.transportStationPage.currentPage = 1;
this.loadTransportStationList();
},
handleTransportStationCurrentChange(currentPage) {
this.transportStationPage.currentPage = currentPage;
this.loadTransportStationList();
},
normalizeTransportStationRow(row = {}) {
const regionName =
row.regionName ||
[row.provinceName, row.cityName, row.districtName].filter(Boolean).join('');
return {
...row,
stationName: row.name || row.stationName || row.addressName || '',
stationType: row.category || this.transportStationTypeLabel,
stationCode: row.code || row.tmisCode || row.iataCode || row.icaoCode || row.siteCode || '',
detailAddress: row.detailAddress || row.address || '',
regionName,
};
},
selectTransportStationRow(row) {
const station = this.normalizeTransportStationRow(row);
this.applyTransportAddress(this.transportStationTarget, {
addressName: station.stationName,
addressType: this.transportStationTypeLabel,
detailAddress: station.detailAddress,
longitude: station.longitude,
latitude: station.latitude,
regionName: station.regionName,
regionCode: station.regionCode,
siteCode: station.stationCode,
siteCodeDisplay: station.stationCode,
remark: station.remark,
});
this.transportStationBox = false;
},
openTransportAddressDialog(target) {
if (!this.ensureTransportTypeBeforeAddress()) return;
this.transportAddressTarget = target;
this.transportAddressSelected = null;
this.resetTransportAddressQuery();
this.transportAddressPage.currentPage = 1;
this.transportAddressBox = true;
},
openTransportAddressPicker(target) {
if (
this.transportStationMode &&
!(this.isTransportPlanPage && this.transportMode === 'water')
) {
this.openTransportStationDialog(target);
return;
}
this.openTransportAddressDialog(target);
},
resetTransportAddressQuery() {
const addressType = this.getTransportFixedAddressType();
this.transportAddressQuery = addressType ? { addressType } : {};
},
getTransportFixedAddressType() {
if (!this.config.fixedTransportAddressType) return '';
const typeMap = {
road: '常规地址',
rail: '铁路车站',
water: '港口/码头',
air: '空港机场',
};
return typeMap[this.transportMode] || '';
},
loadTransportAddressList() {
this.transportAddressLoading = true;
getCommonAddressList(
this.transportAddressPage.currentPage,
this.transportAddressPage.pageSize,
{
...this.transportAddressQuery,
allDept: 0,
}
)
.then(res => {
const data = res.data.data || {};
this.transportAddressRows = data.records || [];
this.transportAddressPage.total = data.total || 0;
})
.finally(() => {
this.transportAddressLoading = false;
});
},
handleTransportAddressSearch() {
this.transportAddressPage.currentPage = 1;
this.loadTransportAddressList();
},
handleTransportAddressReset() {
this.resetTransportAddressQuery();
this.transportAddressPage.currentPage = 1;
this.loadTransportAddressList();
},
handleTransportAddressSizeChange(pageSize) {
this.transportAddressPage.pageSize = pageSize;
this.transportAddressPage.currentPage = 1;
this.loadTransportAddressList();
},
handleTransportAddressCurrentChange(currentPage) {
this.transportAddressPage.currentPage = currentPage;
this.loadTransportAddressList();
},
formatTransportAddressSiteCode(row) {
return row.addressType === '常规地址' ? '/' : row.siteCodeDisplay || row.siteCode || '';
},
selectTransportAddressRow(row) {
this.transportAddressSelected = row;
this.applyTransportAddress(this.transportAddressTarget, {
...row,
preferRegionName: true,
});
this.transportAddressBox = false;
},
applyTransportAddress(target, address = {}) {
const prefix = target === 'departure' ? 'departure' : 'arrival';
this.form[`${prefix}Name`] = address.preferRegionName
? address.regionName || address.addressName || ''
: address.addressName || address.regionName || '';
this.form[`${prefix}Address`] = address.detailAddress || address.address || '';
this.form[`${prefix}AddressCode`] = address.addressCode || '';
this.form[`${prefix}SiteCode`] =
address.addressType === '常规地址'
? '/'
: address.siteCodeDisplay || address.siteCode || '';
this.form[`${prefix}Longitude`] = address.longitude || '';
this.form[`${prefix}Latitude`] = address.latitude || '';
this.form[`${prefix}Contact`] = address.contactName || this.form[`${prefix}Contact`] || '';
this.form[`${prefix}Phone`] = address.contactPhone || this.form[`${prefix}Phone`] || '';
this.$refs.crud?.validateField?.(`${prefix}Address`);
},
openTransportMapDialog(target) {
if (this.dialogReadonly) return;
if (!this.ensureTransportTypeBeforeAddress()) return;
const prefix = target === 'departure' ? 'departure' : 'arrival';
this.transportMapTarget = prefix;
this.transportMapKeyword = this.form[`${prefix}Address`] || this.form[`${prefix}Name`] || '';
this.transportMapSelected = this.buildTransportMapSelection({
lng: this.form[`${prefix}Longitude`],
lat: this.form[`${prefix}Latitude`],
address: this.form[`${prefix}Address`],
regionName: this.form[`${prefix}Name`],
});
this.transportMapStatus = this.transportMapSelected.longitude
? '已加载当前选点,可重新选点'
: '可搜索地址或点击地图选点';
this.transportMapBox = true;
},
loadAmap() {
if (window.AMap && window.AMap.Map) {
return Promise.resolve();
}
if (!amapLoader) {
amapLoader = new Promise((resolve, reject) => {
window._AMapSecurityConfig = {
securityJsCode: AMAP_SECURITY_CODE,
};
const script = document.createElement('script');
script.src = `https://webapi.amap.com/maps?v=2.0&key=${AMAP_KEY}`;
script.async = true;
script.onload = resolve;
script.onerror = () => reject(new Error('高德地图组件加载失败'));
document.body.appendChild(script);
});
}
return amapLoader;
},
initTransportAmap() {
this.loadAmap()
.then(() => {
this.$nextTick(() => {
if (!this.transportAmap) {
const center = this.toLngLat(
this.transportMapSelected.longitude || 116.40769,
this.transportMapSelected.latitude || 39.89945
);
this.transportAmap = new window.AMap.Map(this.$refs.transportAmap, {
center,
zoom: this.transportMapSelected.longitude ? 14 : 11,
});
window.AMap.plugin(['AMap.ToolBar', 'AMap.Geocoder'], () => {
this.transportAmap.addControl(new window.AMap.ToolBar());
if (!this.transportAmapGeocoder) {
this.transportAmapGeocoder = new window.AMap.Geocoder();
}
});
this.transportAmap.on('click', event => this.pickTransportMapPoint(event.lnglat));
} else if (typeof this.transportAmap.resize === 'function') {
this.transportAmap.resize();
}
if (this.transportMapSelected.longitude) {
this.renderTransportMapMarker(
this.toLngLat(
this.transportMapSelected.longitude,
this.transportMapSelected.latitude
)
);
}
});
})
.catch(() => {
this.$message.error('高德地图组件加载失败,请稍后重试或重新打开弹窗');
this.transportMapBox = false;
});
},
searchTransportMapKeyword() {
const keyword = String(this.transportMapKeyword || '').trim();
if (!keyword) {
this.$message.warning('请输入地址关键词');
return;
}
this.loadAmap()
.then(() => {
this.transportMapLoading = true;
this.ensureTransportAmapGeocoder()
.then(() => this.runAmapGeocode('location', keyword))
.then(result => {
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('高德地图组件加载失败,请稍后重试或重新打开弹窗');
});
},
pickTransportMapPoint(lnglat, keyword) {
const longitude = this.getPointLng(lnglat);
const latitude = this.getPointLat(lnglat);
if (longitude === undefined || latitude === undefined) {
this.$message.warning('选点坐标无效');
return;
}
const point = this.toLngLat(longitude, latitude);
this.renderTransportMapMarker(point);
this.transportMapSelected = this.buildTransportMapSelection({
lng: longitude,
lat: latitude,
address: keyword,
});
this.transportMapStatus = '正在反查地址...';
this.ensureTransportAmapGeocoder()
.then(() => this.runAmapGeocode('address', point))
.then(result => {
const address = this.resolveMapAddress(result);
this.transportMapSelected = {
...this.transportMapSelected,
detailAddress: address.detailAddress || this.transportMapSelected.detailAddress,
regionName: address.regionName || this.transportMapSelected.regionName,
regionCode: address.regionCode || this.transportMapSelected.regionCode,
};
this.transportMapKeyword =
this.transportMapSelected.detailAddress || this.transportMapKeyword;
this.transportMapStatus = this.transportMapSelected.detailAddress || '已选点,可确认回填';
})
.catch(() => {
this.transportMapStatus = '反查地址失败';
this.$message.error('反查地址失败,请重新选点');
});
},
renderTransportMapMarker(point) {
if (!this.transportAmap || !window.AMap) return;
if (this.transportAmapMarker) {
this.transportAmapMarker.setMap(null);
}
this.transportAmapMarker = new window.AMap.Marker({
position: point,
});
this.transportAmapMarker.setMap(this.transportAmap);
this.transportAmap.setCenter(point);
},
confirmTransportMapPick() {
if (!this.transportMapSelected.longitude) {
this.$message.warning('请先搜索或点击地图完成选点');
return;
}
const prefix = this.transportMapTarget;
this.form[`${prefix}Address`] =
this.transportMapSelected.detailAddress || this.form[`${prefix}Address`];
this.form[`${prefix}Name`] =
this.transportMapSelected.regionName || this.form[`${prefix}Name`];
this.form[`${prefix}Longitude`] = this.transportMapSelected.longitude;
this.form[`${prefix}Latitude`] = this.transportMapSelected.latitude;
this.transportMapBox = false;
this.$refs.crud?.validateField?.(`${prefix}Address`);
},
buildTransportMapSelection({ lng, lat, address, regionName, regionCode }) {
const longitude = this.formatCoordinate(lng);
const latitude = this.formatCoordinate(lat);
return {
longitude,
latitude,
detailAddress: address || '',
regionName: regionName || '',
regionCode: regionCode || '',
};
},
formatCoordinate(value) {
if (value === undefined || value === null || value === '') return '';
const numberValue = Number(value);
return Number.isFinite(numberValue) ? numberValue.toFixed(6) : '';
},
toLngLat(lng, lat) {
return new window.AMap.LngLat(Number(lng), Number(lat));
},
getPointLng(point) {
if (!point) return undefined;
if (typeof point.getLng === 'function') return point.getLng();
return point.lng ?? point.lon;
},
getPointLat(point) {
if (!point) return undefined;
if (typeof point.getLat === 'function') return point.getLat();
return point.lat;
},
ensureTransportAmapGeocoder() {
if (this.transportAmapGeocoder) {
return Promise.resolve(this.transportAmapGeocoder);
}
return new Promise((resolve, reject) => {
if (!window.AMap || !window.AMap.plugin) {
reject(new Error('高德地图组件未就绪'));
return;
}
window.AMap.plugin(['AMap.Geocoder'], () => {
try {
this.transportAmapGeocoder = new window.AMap.Geocoder();
resolve(this.transportAmapGeocoder);
} catch (error) {
reject(error);
}
});
});
},
runAmapGeocode(action, input) {
return new Promise((resolve, reject) => {
const timer = window.setTimeout(() => {
reject(new Error('高德地图请求超时'));
}, 10000);
const done = (status, result) => {
window.clearTimeout(timer);
if (status === 'complete' && result) {
resolve(result);
return;
}
reject(new Error('高德地图请求失败'));
};
try {
if (action === 'location') {
this.transportAmapGeocoder.getLocation(input, done);
} else {
this.transportAmapGeocoder.getAddress(input, done);
}
} catch (error) {
window.clearTimeout(timer);
reject(error);
}
});
},
resolveMapPoint(result) {
if (!result) return null;
const geocode = Array.isArray(result.geocodes) ? result.geocodes[0] : null;
if (geocode && geocode.location) return geocode.location;
if (result.location) return result.location;
if (result.lnglat) return result.lnglat;
if (result.getLng || result.lng || result.lon) return result;
return null;
},
resolveMapAddress(result = {}) {
const regeocode = result.regeocode || {};
const component = regeocode.addressComponent || {};
const city = Array.isArray(component.city) ? '' : component.city;
const regionName = [component.province, city, component.district].filter(Boolean).join('');
return {
detailAddress: regeocode.formattedAddress || '',
regionName,
regionCode: component.adcode || '',
};
},
openCommonCargoDialog() {
this.commonCargoSelected = [];
this.commonCargoQuery = {};
this.commonCargoPage.currentPage = 1;
this.commonCargoBox = true;
},
loadCommonCargoList() {
this.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) {
this.commonCargoSelected = [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');
});
},
loadOrganizationOptions() {
if (!this.config.enableOrganizationSelect || this.organizationLoading) return;
this.organizationLoading = true;
getDeptTree(this.userInfo?.tenantId)
.then(res => {
this.organizationOptions = this.normalizeOrganizationOptions(res.data.data || []);
this.organizationFlatOptions = this.flattenOrganizationOptions(this.organizationOptions);
this.syncOrganizationSearchDic();
this.syncCurrentOrganizationOption();
})
.finally(() => {
this.organizationLoading = false;
});
},
normalizeOrganizationOptions(tree = []) {
return (tree || []).map(item => {
const name = item.title || item.deptName || item.name || item.label || '';
const children = this.normalizeOrganizationOptions(item.children || []);
return {
...item,
id: String(item.id),
label: name,
rawLabel: name,
children: children.length ? children : undefined,
};
});
},
flattenOrganizationOptions(tree = []) {
const result = [];
tree.forEach(item => {
result.push({
id: String(item.id),
label: item.label,
rawLabel: item.rawLabel || item.label,
});
if (item.children && item.children.length) {
result.push(...this.flattenOrganizationOptions(item.children));
}
});
return result;
},
syncOrganizationSearchDic() {
const column = this.findColumn?.(this.option.column, 'organizationName');
if (column) {
column.dicData = this.organizationOptions;
}
},
syncCurrentOrganizationOption() {
if (!this.form.organizationId || !this.form.organizationName) return;
const exists = this.organizationFlatOptions.some(
item => String(item.id) === String(this.form.organizationId)
);
if (!exists) {
const current = {
id: String(this.form.organizationId),
label: this.form.organizationName,
rawLabel: this.form.organizationName,
};
this.organizationOptions.unshift(current);
this.organizationFlatOptions.unshift(current);
this.syncOrganizationSearchDic();
}
},
findOrganizationOption(organizationId) {
const value = Array.isArray(organizationId)
? organizationId[organizationId.length - 1]
: organizationId;
return this.organizationFlatOptions.find(item => String(item.id) === String(value));
},
handleOrganizationChange(organizationId) {
const organization = this.findOrganizationOption(organizationId);
if (!organization) {
this.form.organizationId = '';
this.form.organizationName = '';
return;
}
this.form.organizationId = organization.id;
this.form.organizationName = organization.rawLabel || organization.label || '';
this.$refs.crud?.validateField?.('organizationName');
},
handleAmountInput(prop, value) {
const text = String(value || '').replace(/[^\d.]/g, '');
const parts = text.split('.');
this.form[prop] =
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0];
},
handleIntegerInput(prop, value) {
this.form[prop] = String(value || '').replace(/\D/g, '');
},
handleContractDateChange() {
this.syncContractPeriod(true);
},
syncContractPeriod(validate = true) {
if (!this.config.enableContractPeriod) return;
this.form.contractPeriod =
this.form.startDate && this.form.endDate
? `${this.form.startDate}至${this.form.endDate}`
: '';
if (validate) {
this.$refs.crud?.validateField?.('contractPeriod');
}
},
applyCurrentUserHandler() {
if (!this.config.enableCurrentUserHandler || this.form.handlerUserName) return;
this.form.handlerUserName =
this.userInfo?.realName || this.userInfo?.userName || this.userInfo?.account || '';
},
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 => this.downloadAttachment(row));
},
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 || [];
},
handleContractFileChange(list) {
const userName = this.userInfo?.realName || this.userInfo?.userName || '';
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
this.contractFileRows = (list || []).map(item => ({
...item,
uploadUserName: item.uploadUserName || userName,
uploadTime: item.uploadTime || uploadTime,
}));
this.selectedContractFileRows = [];
this.form.contractFileJson = JSON.stringify(this.contractFileRows);
},
handleContractFileSelectionChange(rows) {
this.selectedContractFileRows = rows || [];
},
removeContractFile(index) {
this.contractFileRows.splice(index, 1);
this.selectedContractFileRows = this.selectedContractFileRows.filter(item =>
this.contractFileRows.includes(item)
);
this.form.contractFileJson = JSON.stringify(this.contractFileRows);
},
handleContractFileBatchDownload() {
const rows = this.selectedContractFileRows.length
? this.selectedContractFileRows
: this.contractFileRows;
rows.forEach(row => this.downloadAttachment(row));
},
cloneData(data) {
return JSON.parse(JSON.stringify(data));
},
syncBillingPlanJson() {
if (!this.config.enableBillingPlan) return;
if (
this.form.billingEnabled === undefined ||
this.form.billingEnabled === null ||
this.form.billingEnabled === ''
) {
this.form.billingEnabled = 1;
}
this.form.billingPlanJson = JSON.stringify(this.billingPlanRows);
},
ensureBillingMatchRegionOptions() {
if (!this.config.enableBillingPlan && !this.config.enableTransportPlanForm) {
return Promise.resolve([]);
}
if (this.billingMatchRegionOptions.length) {
return Promise.resolve(this.billingMatchRegionOptions);
}
if (this.billingMatchRegionRequest) {
return this.billingMatchRegionRequest;
}
this.billingMatchRegionLoading = true;
this.billingMatchRegionRequest = getRegionLazyTree()
.then(res => {
const regionTree = this.buildBillingMatchRegionTree(res.data.data || []);
const regions = this.extractBillingMatchChinaRegions(regionTree);
this.billingMatchRegionOptions = regions
.map(item => this.normalizeBillingMatchRegionNode(item, 1))
.filter(Boolean);
return this.billingMatchRegionOptions;
})
.finally(() => {
this.billingMatchRegionLoading = false;
this.billingMatchRegionRequest = null;
});
return this.billingMatchRegionRequest;
},
ensureBillingCargoTypeOptions() {
if (
!this.config.enableBillingPlan &&
!this.config.enableTransportPlanForm &&
!this.taskInfoFormEnabled
) {
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;
},
buildBillingMatchRegionTree(regions = []) {
const flatRegions = [];
const collectRegions = list => {
(list || []).forEach(item => {
flatRegions.push({
...item,
children: undefined,
});
if (item.children?.length) collectRegions(item.children);
});
};
collectRegions(regions);
const nodeMap = new Map();
flatRegions.forEach(item => {
const id = item.id ?? item.code ?? item.value;
if (id === undefined || id === null) return;
nodeMap.set(String(id), {
...item,
id: String(id),
parentId:
item.parentId === undefined || item.parentId === null
? String(item.parentCode || '')
: String(item.parentId),
children: [],
});
});
const rootNodes = [];
nodeMap.forEach(node => {
const parent = nodeMap.get(node.parentId);
if (parent && parent !== node) {
parent.children.push(node);
} else {
rootNodes.push(node);
}
});
return rootNodes;
},
normalizeBillingMatchRegionNode(region, level = 1) {
if (level > 2) return null;
const children =
level === 1
? (region.children || [])
.map(item => this.normalizeBillingMatchRegionNode(item, level + 1))
.filter(Boolean)
: [];
return {
...region,
title: region.title || region.name || '',
leaf: level === 2,
children: children.length ? children : undefined,
};
},
isBillingMatchChinaRegion(region = {}) {
const title = String(region.title || region.name || '').trim();
return title === '中国' || title === '中华人民共和国';
},
extractBillingMatchChinaRegions(regions) {
const china = (regions || []).find(item => this.isBillingMatchChinaRegion(item));
if (china?.children?.length) return china.children;
if (regions.length === 1 && regions[0].children?.length) return regions[0].children;
return regions;
},
getBillingMatchRegionPathLabels(path = []) {
let options = this.billingMatchRegionOptions;
const labels = [];
(path || []).forEach(value => {
const option = (options || []).find(item => String(item.id) === String(value));
if (!option) return;
labels.push(option.title || option.name || '');
options = option.children || [];
});
return labels.filter(Boolean);
},
handleBillingMatchRegionChange(prop, value) {
const pathProp = `${prop}Path`;
const path = Array.isArray(value) && value.length >= 2 ? value.slice(0, 2) : [];
const labels = this.getBillingMatchRegionPathLabels(path);
this.billingMatchForm[pathProp] = path;
if (!path.length) {
this.billingMatchForm[prop] = '';
this.billingMatchForm[`${prop}Code`] = '';
return;
}
if (labels.length) {
this.billingMatchForm[prop] = labels.join('');
}
this.billingMatchForm[`${prop}Code`] = String(path[path.length - 1]);
},
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
);
},
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 || [];
},
handleBillingMatchCargoTypeChange(value) {
const path =
Array.isArray(value) && value.length >= 2
? value.slice(0, 2).map(item => String(item))
: [];
const cargoType = this.findBillingCargoTypeByPath(path);
const labels = this.getBillingCargoTypePathLabels(path);
this.billingMatchForm.cargoTypePath = path;
if (!path.length || !cargoType) {
this.billingMatchForm.cargoType = '';
this.billingMatchForm.cargoTypeCode = '';
return;
}
this.billingMatchForm.cargoType =
labels.length > 1
? labels.join('/')
: labels[0] || this.formatBillingCargoTypeLabel(cargoType || {});
this.billingMatchForm.cargoTypeCode =
cargoType?.cargoCode || cargoType?.code || cargoType?.id || '';
},
normalizeBillingCargoTypePath(path) {
return Array.isArray(path) && path.length >= 2
? path.slice(0, 2).map(value => String(value))
: [];
},
resolveBillingMatchCargoTypePath(condition = {}) {
const path = this.normalizeBillingCargoTypePath(condition.cargoTypePath);
if (path.length) return path;
const cargoType = this.findBillingCargoTypeByCodeOrName(condition);
return cargoType?.path || [];
},
normalizeBillingMatchRegionPath(path) {
return Array.isArray(path) && path.length >= 2
? path.slice(0, 2).map(value => String(value))
: [];
},
normalizeBillingMatchCondition(condition = {}) {
const defaults = defaultBillingRule().matchCondition;
const nextCondition = {
...defaults,
...(condition || {}),
};
nextCondition.originPath = this.normalizeBillingMatchRegionPath(condition?.originPath);
nextCondition.destinationPath = this.normalizeBillingMatchRegionPath(
condition?.destinationPath
);
nextCondition.cargoTypePath = this.normalizeBillingCargoTypePath(condition?.cargoTypePath);
return nextCondition;
},
ensureBillingFeeCategoryOptions() {
if (!this.config.enableBillingPlan) return Promise.resolve([]);
if (this.billingFeeCategoryOptions.length) {
return Promise.resolve(this.billingFeeCategoryOptions);
}
if (this.billingFeeCategoryRequest) {
return this.billingFeeCategoryRequest;
}
this.billingFeeCategoryLoading = true;
this.billingFeeCategoryRequest = getDictionary({ code: 'fee_category' })
.then(res => {
this.billingFeeCategoryOptions = res.data.data || [];
return this.billingFeeCategoryOptions;
})
.finally(() => {
this.billingFeeCategoryLoading = false;
this.billingFeeCategoryRequest = null;
});
return this.billingFeeCategoryRequest;
},
ensureBillingUnitOptions() {
if (!this.config.enableBillingPlan) return Promise.resolve([]);
if (this.billingUnitOptions.length) {
return Promise.resolve(this.billingUnitOptions);
}
if (this.billingUnitRequest) {
return this.billingUnitRequest;
}
this.billingUnitLoading = true;
this.billingUnitRequest = getDictionary({ code: 'unit_fee' })
.then(res => {
this.billingUnitOptions = res.data.data || [];
return this.billingUnitOptions;
})
.finally(() => {
this.billingUnitLoading = false;
this.billingUnitRequest = null;
});
return this.billingUnitRequest;
},
resolveBillingFeeType(value) {
const feeType = String(value || '').trim();
if (!feeType) return '';
const option = this.billingFeeCategoryOptions.find(
item => String(item.dictKey || '') === feeType || String(item.dictValue || '') === feeType
);
return String((option && option.dictKey) || feeType);
},
getBillingFeeItemOptions(feeType) {
const key = this.resolveBillingFeeType(feeType);
return key ? this.billingFeeItemOptionsMap[key] || [] : [];
},
loadBillingFeeItemOptions(feeType) {
const key = this.resolveBillingFeeType(feeType);
if (!key) return Promise.resolve([]);
if (this.billingFeeItemOptionsMap[key]) {
return Promise.resolve(this.billingFeeItemOptionsMap[key]);
}
if (this.billingFeeItemRequestMap[key]) {
return this.billingFeeItemRequestMap[key];
}
this.billingFeeItemLoadingMap[key] = true;
this.billingFeeItemRequestMap[key] = getFeeItemList(1, 9999, { feeCategory: key })
.then(res => {
const records = extractRecords(res);
this.billingFeeItemOptionsMap[key] = records;
return records;
})
.finally(() => {
this.billingFeeItemLoadingMap[key] = false;
delete this.billingFeeItemRequestMap[key];
});
return this.billingFeeItemRequestMap[key];
},
loadBillingRuleFeeItems(rules = []) {
const feeTypes = Array.from(
new Set((rules || []).map(row => this.resolveBillingFeeType(row.feeType)).filter(Boolean))
);
feeTypes.forEach(feeType => this.loadBillingFeeItemOptions(feeType));
},
handleBillingFeeTypeChange(row, value) {
row.feeType = this.resolveBillingFeeType(value);
row.feeItem = '';
this.loadBillingFeeItemOptions(row.feeType);
},
openBillingPlan(row, index = -1) {
this.billingPlanMode = index > -1 ? 'edit' : 'add';
this.billingPlanIndex = index;
const plan = row ? this.cloneData(row) : defaultBillingPlan();
this.billingPlanForm = {
...defaultBillingPlan(),
...plan,
rules: this.normalizeBillingRules(
Array.isArray(plan.rules) && plan.rules.length ? plan.rules : [defaultBillingRule()]
),
};
this.billingPlanBox = true;
this.ensureBillingUnitOptions();
this.ensureBillingFeeCategoryOptions().then(() => {
this.billingPlanForm.rules = this.normalizeBillingRules(this.billingPlanForm.rules);
this.loadBillingRuleFeeItems(this.billingPlanForm.rules);
});
this.$nextTick(() => this.$refs.billingPlanFormRef?.clearValidate?.());
},
submitBillingPlan() {
this.$refs.billingPlanFormRef?.validate(valid => {
if (!valid) return;
if (!this.validateBillingRules()) return;
const plan = this.cloneData(this.billingPlanForm);
plan.rules = this.normalizeBillingRules(plan.rules).map(rule => {
const ranges = this.canEditBillingLimit(rule) ? rule.limitRanges : [];
return {
...rule,
limitRanges: ranges,
lowerLimit: ranges[0]?.lowerLimit || '',
upperLimit: ranges[0]?.upperLimit || '',
minimumBillingWeight: this.canEditMinimumBillingWeight(rule)
? rule.minimumBillingWeight
: '',
};
});
if (!this.billingPlanRows.length && this.billingPlanIndex === -1) {
plan.defaultPlan = true;
}
if (plan.defaultPlan) {
this.billingPlanRows.forEach(item => {
item.defaultPlan = false;
});
}
if (this.billingPlanIndex > -1) {
this.billingPlanRows.splice(this.billingPlanIndex, 1, plan);
} else {
this.billingPlanRows.push(plan);
}
this.syncBillingPlanJson();
this.billingPlanBox = false;
});
},
validateBillingRules() {
const groups = {};
const requiredFields = [
['feeType', '费用类型'],
['feeItem', '费用项'],
['billingElement', '计费要素'],
['billingType', '计费类型'],
['billingUnit', '计费单位'],
['unitPrice', '单价'],
];
for (const [index, row] of (this.billingPlanForm.rules || []).entries()) {
const emptyField = requiredFields.find(([prop]) => this.isBillingFieldEmpty(row[prop]));
if (emptyField) {
this.$message.warning(`第${index + 1}行${emptyField[1]}不能为空`);
return false;
}
if (row.billingElement && !this.getBillingTypeOptions(row).includes(row.billingType)) {
this.$message.warning('请选择计费要素对应的计费类型');
return false;
}
if (!this.canEditBillingLimit(row)) {
continue;
}
const key = row.billingElement || '';
groups[key] = groups[key] || [];
const ranges = this.getBillingLimitRanges(row);
for (const range of ranges) {
const lower = Number(range.lowerLimit);
const upper = Number(range.upperLimit);
if (
range.lowerLimit === '' ||
range.upperLimit === '' ||
Number.isNaN(lower) ||
Number.isNaN(upper)
) {
this.$message.warning('请完整填写计费要素上下限');
return false;
}
if (lower > upper) {
this.$message.warning('计费要素下限不能大于上限');
return false;
}
groups[key].push({ lower, upper });
}
}
return this.validateBillingLimitRangeGroups(groups);
},
validateBillingLimitRangeGroups(groups) {
const precision = 0.000001;
for (const [billingElement, ranges] of Object.entries(groups)) {
if (ranges.length <= 1) continue;
const sortedRanges = [...ranges].sort((prev, next) => {
if (prev.lower !== next.lower) return prev.lower - next.lower;
return prev.upper - next.upper;
});
for (let index = 1; index < sortedRanges.length; index += 1) {
const prevRange = sortedRanges[index - 1];
const currentRange = sortedRanges[index];
if (currentRange.lower < prevRange.upper - precision) {
this.$message.warning(`${billingElement}的计费要素区间不能重叠`);
return false;
}
if (currentRange.lower > prevRange.upper + precision) {
this.$message.warning(`${billingElement}的计费要素区间必须连续,不能存在间隙`);
return false;
}
}
}
return true;
},
isBillingFieldEmpty(value) {
return value === undefined || value === null || String(value).trim() === '';
},
removeBillingPlan(index) {
this.$confirm('确定删除该计费方案?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
this.billingPlanRows.splice(index, 1);
this.syncBillingPlanJson();
});
},
addBillingRule() {
const row = defaultBillingRule();
this.billingPlanForm.rules.push(row);
this.ensureBillingFeeCategoryOptions();
},
copyBillingRule(row) {
const nextRow = this.cloneData(row);
this.billingPlanForm.rules.push(nextRow);
this.loadBillingFeeItemOptions(nextRow.feeType);
},
removeBillingRule(index) {
this.billingPlanForm.rules.splice(index, 1);
if (!this.billingPlanForm.rules.length) {
this.billingPlanForm.rules.push(defaultBillingRule());
}
},
getBillingTypeOptions(row) {
return this.billingElementTypeMap[row.billingElement] || [];
},
handleBillingElementChange(row) {
if (!this.getBillingTypeOptions(row).includes(row.billingType)) {
row.billingType = '';
}
if (!this.canEditMinimumBillingWeight(row)) {
row.minimumBillingWeight = '';
}
this.handleBillingTypeChange(row);
},
handleBillingTypeChange(row) {
if (!this.canEditBillingLimit(row)) {
row.limitRanges = [{ lowerLimit: '', upperLimit: '' }];
row.lowerLimit = '';
row.upperLimit = '';
return;
}
row.limitRanges = this.normalizeBillingLimitRanges(row);
this.syncBillingLegacyLimit(row);
},
canEditBillingLimit(row) {
return (
!this.dialogReadonly &&
Boolean(row.billingElement) &&
Boolean(row.billingType) &&
(row.billingType.includes('区间') || row.billingType.includes('阶梯'))
);
},
canEditMinimumBillingWeight(row) {
return !this.dialogReadonly && ['按重量', '按吨·公里'].includes(row.billingElement);
},
getBillingLimitRanges(row) {
return Array.isArray(row.limitRanges) && row.limitRanges.length
? row.limitRanges
: [{ lowerLimit: '', upperLimit: '' }];
},
normalizeBillingRules(rules = []) {
return (rules || []).map(rule => {
const nextRule = {
...defaultBillingRule(),
...rule,
};
nextRule.feeType = this.resolveBillingFeeType(nextRule.feeType);
nextRule.limitRanges = this.normalizeBillingLimitRanges(nextRule);
this.syncBillingLegacyLimit(nextRule);
return nextRule;
});
},
normalizeBillingLimitRanges(row) {
const ranges = Array.isArray(row.limitRanges) ? row.limitRanges : [];
const nextRanges = ranges
.map(item => ({
lowerLimit: item?.lowerLimit ?? '',
upperLimit: item?.upperLimit ?? '',
}))
.filter(item => item.lowerLimit !== '' || item.upperLimit !== '');
if (!nextRanges.length && (row.lowerLimit !== '' || row.upperLimit !== '')) {
nextRanges.push({
lowerLimit: row.lowerLimit ?? '',
upperLimit: row.upperLimit ?? '',
});
}
return nextRanges.length ? nextRanges : [{ lowerLimit: '', upperLimit: '' }];
},
syncBillingLegacyLimit(row) {
const firstRange = row.limitRanges?.[0] || {};
row.lowerLimit = firstRange.lowerLimit || '';
row.upperLimit = firstRange.upperLimit || '';
},
addBillingLimitRange(row, index) {
row.limitRanges = this.getBillingLimitRanges(row);
row.limitRanges.splice(index + 1, 0, { lowerLimit: '', upperLimit: '' });
},
removeBillingLimitRange(row, index) {
row.limitRanges = this.getBillingLimitRanges(row);
row.limitRanges.splice(index, 1);
if (!row.limitRanges.length) {
row.limitRanges.push({ lowerLimit: '', upperLimit: '' });
}
this.syncBillingLegacyLimit(row);
},
handleBillingLimitInput(row, index, prop, value) {
row.limitRanges = this.getBillingLimitRanges(row);
const text = String(value || '').replace(/[^\d.]/g, '');
const parts = text.split('.');
row.limitRanges[index][prop] =
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0];
this.syncBillingLegacyLimit(row);
},
openBillingMatch(row, index) {
this.billingMatchRuleIndex = index;
this.billingMatchForm = this.normalizeBillingMatchCondition(row.matchCondition);
this.billingMatchBox = true;
this.ensureBillingMatchRegionOptions();
this.ensureBillingCargoTypeOptions().then(() => {
const path = this.resolveBillingMatchCargoTypePath(this.billingMatchForm);
if (path.length) {
this.handleBillingMatchCargoTypeChange(path);
}
});
},
saveBillingMatch() {
const row = this.billingPlanForm.rules[this.billingMatchRuleIndex];
if (row) {
this.handleBillingMatchRegionChange('origin', this.billingMatchForm.originPath);
this.handleBillingMatchRegionChange('destination', this.billingMatchForm.destinationPath);
this.handleBillingMatchCargoTypeChange(this.billingMatchForm.cargoTypePath);
row.matchCondition = this.cloneData(this.billingMatchForm);
}
this.billingMatchBox = false;
},
handleBillingDecimalInput(row, prop, value) {
const text = String(value || '').replace(/[^\d.]/g, '');
const parts = text.split('.');
row[prop] =
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0];
},
searchReset() {
this.query = {};
this.contractExpiryScope = '';
this.contractSignType = '';
this.page.currentPage = 1;
this.onLoad(this.page);
},
searchChange(params, done) {
this.query = {
...params,
...(this.contractSignType ? { signType: this.contractSignType } : {}),
};
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);
},
handleSearchIconChange(expanded) {
this.searchExpanded = expanded;
},
handleContractExpiryScopeChange(expireScope) {
this.contractExpiryScope = expireScope;
this.page.currentPage = 1;
this.onLoad(this.page, this.query);
},
refreshContractExpiryStats(query) {
if (!this.config.enableContractExpiryTags || typeof this.api.expireStats !== 'function')
return;
const statsQuery = { ...query };
delete statsQuery.expireScope;
this.api.expireStats(statsQuery).then(res => {
this.contractExpiryStats = {
...this.contractExpiryStats,
...(res.data.data || {}),
};
});
},
handleScopeChange() {
if (!this.canViewAllDept) {
this.allDept = 0;
}
this.page.currentPage = 1;
this.onLoad(this.page);
},
normalizeSearchRangeValue(value, suffix) {
if (!value) return undefined;
return suffix ? `${value} ${suffix}` : value;
},
normalizeSearchRangeParams(params = {}) {
const query = { ...params };
const rangeMap = this.config.searchRangeMap || {};
Object.entries(rangeMap).forEach(([rangeProp, mapping]) => {
const [startProp, endProp, startSuffix, endSuffix] = Array.isArray(mapping)
? mapping
: [mapping.startProp, mapping.endProp, mapping.startSuffix, mapping.endSuffix];
const range = query[rangeProp];
if (Array.isArray(range) && range.length === 2) {
query[startProp] = this.normalizeSearchRangeValue(range[0], startSuffix);
query[endProp] = this.normalizeSearchRangeValue(range[1], endSuffix);
}
delete query[rangeProp];
});
return query;
},
normalizeQueryParams(params = {}) {
let query = { ...params };
if (Object.keys(this.config.searchRangeMap || {}).length) {
query = this.normalizeSearchRangeParams(query);
}
if (this.config.enableOrganizationSelect && query.organizationName) {
const organizationId = Array.isArray(query.organizationName)
? query.organizationName[query.organizationName.length - 1]
: query.organizationName;
if (!organizationId) {
delete query.organizationId;
delete query.organizationName;
return query;
}
const organization = this.findOrganizationOption(organizationId);
query.organizationId = organizationId;
if (organization) {
query.organizationName = organization.rawLabel || organization.label || '';
} else {
delete query.organizationName;
}
if (this.canViewAllDept) {
query.allDept = 1;
}
}
return query;
},
onLoad(page, params = {}) {
this.loading = true;
const query = this.normalizeQueryParams({
...params,
...this.query,
allDept: this.allDept,
...(this.contractSignType ? { signType: this.contractSignType } : {}),
...(this.contractExpiryScope ? { expireScope: this.contractExpiryScope } : {}),
});
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();
this.refreshContractExpiryStats(query);
})
.finally(() => {
this.loading = false;
});
},
buildExportParams() {
const query = this.normalizeQueryParams({ ...this.query, allDept: this.allDept });
return {
...query,
ids: this.ids,
...(this.config.exportColumns
? { exportColumns: JSON.stringify(this.config.exportColumns) }
: {}),
[this.website.tokenHeader]: getToken(),
};
},
handleExport() {
this.$confirm(`是否导出${this.config.exportName || this.config.title}?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
NProgress.start();
exportBlob(this.config.exportUrl, this.buildExportParams(), { feedback: true })
.then(res => {
downloadXls(
res.data,
`${this.config.exportName || this.config.title}${this.$dayjs().format(
'YYYY-MM-DD HH:mm:ss'
)}.xlsx`
);
})
.finally(() => {
NProgress.done();
});
});
},
handleTemplate() {
NProgress.start();
exportBlob(
this.config.templateUrl,
{
[this.website.tokenHeader]: getToken(),
},
{ feedback: true }
)
.then(res => {
downloadXls(res.data, `${this.config.title}模板.xlsx`);
})
.finally(() => {
NProgress.done();
});
},
handleImport() {
if (this.config.enableWaybillImport) {
this.waybillImportBox = true;
return;
}
if (this.config.enableTransportPlanImport) {
this.transportPlanImportBox = true;
this.loadProjectOptions();
return;
}
const column = this.findColumn(this.excelOptionConfig.column, 'excelFile');
column.action = this.config.importUrl;
openImportDialog(this, this.config.title, () => this.onLoad(this.page, this.query));
},
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 })
.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 = {
enable: '启用',
disable: '停用',
cancel: '取消',
complete: this.completeActionLabel,
reassign: '重新派单',
}[action];
this.$confirm(`确定${actionName}该${this.config.title}?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => this.api[action](row.id))
.then(() => {
this.$message.success(`${actionName}成功`);
this.onLoad(this.page, this.query);
});
},
handleCustomOperation(operation, row) {
if (operation.action === 'createFromTemplate') {
this.api.getDetail(row.id).then(res => {
const template = res.data?.data || row;
const templateType = template.templateType || template.templateTypeName;
const target =
templateType === '运单' ? '/business/waybill-manage' : '/business/transport-plan';
sessionStorage.setItem(
'business-template-create',
JSON.stringify({ target: target.slice('/business/'.length), data: template })
);
this.$router.push({ path: target, query: { fromTemplate: Date.now().toString() } });
});
return;
}
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.dispatchRow = { ...row };
this.dispatchBox = true;
this.dispatchLoading = true;
this.dispatchRows = [];
this.loadDispatchTransportTypeOptions();
const request =
this.api && typeof this.api.getDetail === 'function'
? this.api.getDetail(row.id)
: Promise.resolve({ data: { data: row } });
request
.then(res => {
const detail = {
...row,
...(res?.data?.data || {}),
};
this.dispatchRow = detail;
this.dispatchRows = this.buildDispatchRows(detail);
})
.finally(() => {
this.dispatchLoading = false;
});
},
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 || '';
},
resetDispatchDialog() {
this.dispatchRow = {};
this.dispatchRows = [];
this.dispatchLoading = false;
this.dispatchSaving = '';
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 || '承运商',
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 ||
'吨',
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 freight =
calculatedFreight !== ''
? calculatedFreight
: item.freight ??
item.freightAmount ??
item.transportFee ??
plan.freight ??
plan.freightAmount ??
freightInfo.freightAmount ??
freightInfo.transportFee ??
'';
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, 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.cargoType ||
goods.secondCargoTypeName ||
goods.secondCargoType ||
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 || ''}`
)
.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 options = new Map();
filtered.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());
},
handleDispatchConfiguredCargoTypeChange(row, value) {
const option = this.getDispatchConfiguredCargoTypeOptions().find(
item => String(item.value) === String(value || '')
);
row.cargoType = option?.label || '';
row.cargoTypeCode = option?.cargoTypeCode || '';
row.cargoTypePath = option?.cargoTypePath || [];
row.cargoName = '';
row.specification = '';
row.model = '';
},
handleDispatchConfiguredCargoNameChange(row, value) {
const option = this.getDispatchConfiguredCargoNameOptions(row).find(
item => String(item.value) === String(value || '')
);
row.cargoName = option?.value || '';
if (!option) return;
row.cargoType = option.cargoTypeLabel || row.cargoType || '';
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:
index >= 0
? defaultGoods.quantity || ''
: this.formatDispatchQuantity(this.getDispatchRemainingQuantity(defaultUnit)),
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 || '承运商';
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 =
freightInfo.currency || this.dispatchItemForm.freightCurrency || 'CNY';
this.loadShippingTemplateCurrencyOptions();
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 || '元/吨',
}));
}
this.dispatchItemAttachmentRows = this.parseJsonArray(
index >= 0 ? row.attachmentsJson : this.dispatchRow.attachmentsJson
);
this.selectedDispatchItemAttachmentRows = [];
this.dispatchItemBox = true;
},
handleDispatchCarrierTypeChange(value) {
const nextValue = value || '';
if (nextValue === '承运商') {
this.dispatchItemForm.trailerVehicleNo = '';
this.dispatchItemForm.escortName = '';
this.dispatchItemForm.escortPhone = '';
} else {
this.dispatchItemForm.carrierId = '';
this.dispatchItemForm.carrierName = '';
}
this.dispatchItemForm.carrierType = nextValue;
},
loadDispatchCarrierOptions() {
if (this.taskCarrierLoading) return Promise.resolve([]);
this.taskCarrierLoading = true;
return getCarrierCustomerList(1, 9999, { customerType: '承运商', status: 1 })
.then(res => {
this.taskCarrierOptions = extractRecords(res);
return this.taskCarrierOptions;
})
.finally(() => {
this.taskCarrierLoading = false;
});
},
handleDispatchCarrierChange(value) {
const carrier = this.taskCarrierOptions.find(item =>
[item.customerName, item.carrierName, item.fullName, item.name].some(
name => String(name || '') === String(value || '')
)
);
this.dispatchItemForm.carrierId = carrier?.id || '';
this.dispatchItemForm.carrierName = value || '';
},
loadDispatchDriverOptions() {
if (this.taskDriverLoading) return Promise.resolve([]);
this.taskDriverLoading = true;
return getDriverList(1, 9999, {})
.then(res => {
this.taskDriverOptions = extractRecords(res);
return this.taskDriverOptions;
})
.finally(() => {
this.taskDriverLoading = false;
});
},
handleDispatchDriverChange(value) {
const driver = this.taskDriverOptions.find(item =>
[item.driverName, item.name].some(name => String(name || '') === String(value || ''))
);
this.dispatchItemForm.driverId = driver?.id || '';
this.dispatchItemForm.driverName = value || '';
if (driver) {
this.dispatchItemForm.driverPhone =
driver.mobile ||
driver.phone ||
driver.driverPhone ||
this.dispatchItemForm.driverPhone ||
'';
}
},
getDispatchCargoRowOptions(row = {}) {
return this.getTaskCargoRowOptions(row);
},
handleDispatchCargoTypeChange(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.loadTaskCargoOptionsByPath(path);
},
handleDispatchCargoRowNameChange(row, value) {
const cargo = this.getDispatchCargoRowOptions(row).find(
item => this.formatBillingCargoTypeLabel(item) === value
);
row.cargoName = value || '';
if (cargo) {
row.specification = cargo.specification || cargo.spec || row.specification || '';
row.model = cargo.model || cargo.modelName || row.model || '';
}
},
handleDispatchCargoRowSelect(row, item = {}) {
row.cargoName = this.formatBillingCargoTypeLabel(item);
row.specification = item.specification || item.spec || row.specification || '';
row.model = item.model || item.modelName || row.model || '';
},
handleDispatchCargoSelect(item = {}) {
this.dispatchItemForm.cargoName = this.formatBillingCargoTypeLabel(item);
this.dispatchItemForm.specification =
item.specification || item.spec || this.dispatchItemForm.specification || '';
this.dispatchItemForm.model =
item.model || item.modelName || this.dispatchItemForm.model || '';
},
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.handleTaskFullFreightNumberInput(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 = {}) {
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)));
},
dispatchItemRemainingQuantity(row = {}) {
const unit = this.getDispatchQuantityUnit(row);
let 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;
}, []);
},
saveDispatchItem() {
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.dispatchItemForm.freightCurrency || 'CNY',
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.getDispatchFeeFields({}, nextRow).freight;
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()) {
if (row.taskEntryMode === 'full') {
const goodsRows = this.parseJsonArray(row.goodsJson);
if (!goodsRows.length || goodsRows.some(item => !item.cargoName || !item.cargoType || !item.quantity || !item.quantityUnit)) {
this.$message.warning(`第${index + 1}条调度明细的完整录入货物信息不完整`);
return false;
}
}
const invalidField = [
[row.departurePhone, '发货联系方式'],
[row.arrivalPhone, '收货联系方式'],
[row.driverPhone, '手机号'],
[row.escortPhone, '押运人手机号'],
].find(([value]) => String(value || '').trim() && !isMobile(value));
if (invalidField) {
this.$message.warning(`第${index + 1}条调度明细的${invalidField[1]}格式不正确`);
return false;
}
}
if (mode === 'draft') return true;
const invalidRow = this.dispatchRows.find(row => {
const selfTransport = row.carrierType !== '承运商';
return (
!row.transportType ||
!row.cargoName ||
!row.cargoType ||
!row.quantity ||
!row.quantityUnit ||
!row.carrierType ||
!row.vehicleNo ||
!row.departureAddress ||
!row.arrivalAddress ||
(row.carrierType === '承运商' && !row.carrierName) ||
(selfTransport &&
(!row.driverName || !row.driverPhone || !row.mileage))
);
});
if (invalidRow) {
this.$message.warning('请补全调度明细中的必填信息');
return false;
}
const invalidMileageRow = this.dispatchRows.find(
row =>
row.mileage &&
(!/^\d{1,10}$/.test(String(row.mileage)) || Number(row.mileage) <= 0)
);
if (invalidMileageRow) {
this.$message.warning('里程必须为不超过10位的正整数');
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 = '';
});
},
dispatchRowKey(row, index) {
return row._key || `${index}-${row.vehicleNo || row.cargoInfo || row.dispatchTime || 'row'}`;
},
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 };
},
handleDispatchConfirm() {
this.handleDispatchSubmit('submit');
},
handleChangeRecordFlow(row) {
const processInstanceId = row.processInstanceId || row.processInstanceID || row.procInstId;
if (!processInstanceId) {
this.$message.warning('该变更记录暂无流程实例');
return;
}
this.handleFlow(row, true);
},
handleBatchComplete() {
if (!this.selectionList.length) {
this.$message.warning('请选择至少一条数据');
return;
}
this.$confirm('确定完成选中的运单?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => this.api.batchComplete(this.ids))
.then(res => {
const result = res.data.data || {};
this.$message.success(
`成功完成${result.successCount || 0}条,跳过${result.skippedCount || 0}条`
);
this.onLoad(this.page, this.query);
});
},
handleRoadLoading() {
if (!this.selectionList.length) {
this.$message.warning('请选择至少一条数据');
return;
}
const nonRoadWaybills = this.selectionList.filter(row => {
const transportType = String(row.transportType || '').trim();
return transportType !== '公路运输' && transportType.toLowerCase() !== 'road';
});
if (nonRoadWaybills.length) {
this.$message.warning('公路配置仅支持运输方式为“公路运输”的运单');
return;
}
if (typeof this.api.roadLoading !== 'function') {
this.$message.warning('公路配载功能暂未配置');
return;
}
this.$confirm('确定对选中的运单进行公路配载?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => this.api.roadLoading(this.ids))
.then(() => {
this.$message.success('公路配载成功');
this.onLoad(this.page, this.query);
});
},
},
};
</script>
<style lang="scss" scoped>
// 对齐 customer-archive 的 .archive-form 风格:label 固定宽折行 + 控件 250px
.business-crud-page__task-form,
.business-crud-page__freight-form,
.business-crud-page__module-form,
.business-crud-page__dispatch-item-form,
.business-crud-page__match-form,
.business-crud-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-input-number),
:deep(.el-form-item__content > .el-date-editor) {
width: 250px;
max-width: 100%;
}
}
.business-crud-page {
&__field {
width: 100%;
}
&__contract-expiry-search {
display: flex;
align-items: center;
gap: 8px;
margin-left: auto;
min-width: 0;
:deep(.el-select) {
width: 160px;
}
}
&__contract-expiry-label {
flex: 0 0 auto;
min-width: 140px;
color: #606266;
text-align: right;
white-space: nowrap;
}
&__contract-expiry-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
:deep(.el-check-tag) {
margin: 0;
white-space: nowrap;
}
}
.dialog-section-title {
display: flex;
align-items: center;
width: 100%;
color: #303133;
font-size: 15px;
font-weight: 600;
&::before {
display: block;
width: 4px;
height: 16px;
margin-right: 8px;
background: #409eff;
content: '';
}
&--action {
justify-content: space-between;
}
}
&__shipping-title,
&__goods-title {
min-width: 100%;
}
&__goods-title &__section-actions,
&__shipping-title .el-button {
margin-left: auto;
}
&__section-actions {
display: flex;
flex: 0 0 auto;
align-items: center;
gap: 8px;
}
&__task {
width: 100%;
}
&__task-form {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
column-gap: 24px;
row-gap: 16px;
width: 100%;
:deep(.el-form-item) {
min-width: 0;
margin-bottom: 0;
}
:deep(.el-form-item__content) {
min-width: 0;
}
:deep(.el-select),
:deep(.el-cascader),
:deep(.el-date-editor.el-input) {
width: 100%;
}
:deep(.el-radio-group) {
width: 100%;
}
:deep(.el-radio-button) {
flex: 1;
}
:deep(.el-radio-button__inner) {
width: 100%;
}
}
&__task-form--full {
display: block;
}
&__task-row {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
column-gap: 24px;
row-gap: 16px;
margin-bottom: 16px;
width: 100%;
&:last-child {
margin-bottom: 0;
}
:deep(.el-form-item) {
min-width: 0;
margin-bottom: 0;
}
:deep(.el-form-item__content) {
min-width: 0;
}
:deep(.el-select),
:deep(.el-cascader),
:deep(.el-date-editor.el-input) {
width: 100%;
}
}
&__task-span-4 {
grid-column: 1 / -1;
}
&__task-span-1 {
grid-column: span 1;
}
&__task-carrier-type {
:deep(.el-form-item__content) {
flex: 0 1 260px;
max-width: 260px;
}
}
&__task-remark {
grid-column: span 3;
}
&__task-note {
grid-column: 1 / -1;
:deep(.el-input) {
width: 100%;
}
}
&__suffix-select {
width: 72px;
margin-right: -8px;
:deep(.el-select__wrapper),
:deep(.el-select__wrapper:hover),
:deep(.el-select__wrapper.is-focused) {
min-height: 24px;
padding: 0 4px;
border: 0;
box-shadow: none !important;
}
}
&__suffix-select--wide {
width: 104px;
}
&__append-select {
width: 92px;
margin-right: -8px;
:deep(.el-select__wrapper),
:deep(.el-select__wrapper:hover),
:deep(.el-select__wrapper.is-focused) {
min-height: 24px;
padding: 0 4px;
border: 0;
box-shadow: none !important;
}
}
&__append-select--wide {
width: 108px;
min-width: 108px;
}
:deep(.business-crud-page__goods-info-cell .cell) {
overflow: visible;
height: auto;
max-height: none;
line-height: 20px;
white-space: normal;
word-break: break-word;
text-overflow: clip;
}
:deep(.business-crud-page__dispatch-progress-cell .cell) {
overflow: visible;
height: auto;
max-height: none;
line-height: 20px;
white-space: pre-line;
word-break: break-word;
text-overflow: clip;
}
:deep(.business-crud-page__waybill-address-cell) {
display: flex;
flex-direction: column;
gap: 2px;
line-height: 20px;
white-space: normal;
word-break: break-word;
}
&__route-address-row {
display: grid;
grid-template-columns: 260px minmax(280px, 1fr) 40px 58px minmax(160px, 220px) 72px minmax(
160px,
220px
);
gap: 8px;
align-items: center;
width: 100%;
:deep(.el-input),
:deep(.el-select),
:deep(.el-cascader) {
width: 100%;
}
.el-button {
width: 32px;
min-width: 32px;
padding: 0;
}
}
&__dispatch-shipping-form {
:deep(.el-form-item) {
margin-bottom: 16px;
}
}
&__dispatch-shipping-form &__route-address-row {
grid-template-columns: 260px minmax(280px, 1fr) 58px minmax(160px, 220px) 72px minmax(
160px,
220px
);
}
&__dispatch-item-attachments {
display: flex;
flex-wrap: wrap;
gap: 12px 20px;
min-height: 32px;
padding: 4px 0 16px 12px;
color: #909399;
font-size: 14px;
}
&__dispatch-carrier-title {
flex-basis: 100%;
margin-top: 16px;
}
&__dispatch-task-title {
margin-right: auto;
}
&__route-label {
color: #606266;
font-size: 14px;
line-height: 32px;
text-align: right;
white-space: nowrap;
}
&__map-suffix {
cursor: pointer;
color: #909399;
&:hover {
color: #409eff;
}
}
&__cargo-wrap {
display: block;
width: 100%;
min-width: 0;
overflow: hidden;
}
&__cargo-import {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 56px;
padding: 12px 0 8px;
:deep(.el-upload__tip) {
margin: 16px 0 0;
color: #606266;
font-size: 16px;
}
}
&__cargo-import-dialog {
:deep(.el-dialog__footer) {
text-align: center;
}
}
&__cargo-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;
}
}
:deep(.business-crud-page__full-form-item) {
width: 100%;
max-width: 100%;
}
:deep(.business-crud-page__full-form-item .el-form-item) {
width: 100%;
}
:deep(.business-crud-page__full-form-item .el-form-item__label) {
display: none;
width: 0 !important;
padding: 0 !important;
}
:deep(.business-crud-page__full-form-item .el-form-item__content) {
flex: 1 1 100%;
width: 100%;
max-width: 100%;
margin-left: 0 !important;
}
:deep(.business-crud-page__full-form-item .el-form-item__content > div) {
width: 100%;
min-width: 0;
}
&__cargo-total {
display: flex;
justify-content: flex-start;
padding-top: 12px;
color: #303133;
font-weight: 600;
}
&__freight-section {
margin-top: 20px;
}
&__freight-table {
width: 100%;
:deep(.el-table__header th) {
background: #f5f7fa;
color: #303133;
font-weight: 600;
}
:deep(.el-table__cell) {
border-color: #eff1f7;
}
:deep(.el-input),
:deep(.el-select) {
width: 100%;
}
}
&__freight-summary {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
&__freight-form {
display: grid;
grid-template-columns: repeat(3, minmax(220px, 1fr));
gap: 16px 24px;
width: min(100%, 920px);
:deep(.el-form-item) {
margin-bottom: 0;
}
:deep(.el-input),
:deep(.el-select) {
width: 100%;
}
&--non-road {
width: 100%;
grid-template-columns: repeat(4, minmax(220px, 1fr));
}
}
&__shipping-template-footer-summary {
position: absolute;
left: 16px;
bottom: 10px;
display: inline-flex;
align-items: center;
gap: 24px;
height: 28px;
color: #303133;
font-size: 13px;
strong {
margin-left: 4px;
font-size: 15px;
}
:deep(.el-link) {
font-size: 13px;
}
}
&__process-config-dialog {
max-height: 70vh;
overflow-y: auto;
:deep(.el-dialog__body) {
padding-top: 8px;
}
}
&__process-config-section {
padding: 16px 0;
border-bottom: 1px solid #eff1f7;
&:last-child {
border-bottom: 0;
}
}
&__process-config-meta {
display: flex;
align-items: center;
gap: 24px;
margin-bottom: 16px;
color: #606266;
span:first-child {
color: #303133;
font-size: 16px;
font-weight: 600;
}
}
&__process-timeline {
position: relative;
display: flex;
width: 100%;
min-width: 0;
box-sizing: border-box;
padding: 32px 20px 20px;
overflow: hidden;
background: #fff;
border: 1px solid #eff1f7;
}
&__process-timeline-line {
position: absolute;
top: 26px;
right: 40px;
left: 40px;
height: 2px;
background: #dcdfe6;
}
&__process-timeline-item {
position: relative;
flex: 1 1 0;
min-width: 0;
padding: 16px 12px 0;
text-align: center;
}
&__process-timeline-dot {
position: absolute;
top: -12px;
left: calc(50% - 7px);
width: 10px;
height: 10px;
border: 2px solid #fff;
border-radius: 50%;
background: #409eff;
}
&__process-timeline-name {
color: #409eff;
font-weight: 600;
}
&__process-timeline-desc {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 4px 12px;
margin-top: 10px;
color: #606266;
font-size: 13px;
line-height: 20px;
div {
width: 100%;
}
}
&__dialog-search {
padding: 12px 12px 4px;
margin-bottom: 12px;
border: 1px solid #eff1f7;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
:deep(.el-form-item) {
margin-bottom: 8px;
}
:deep(.el-input),
:deep(.el-select),
:deep(.el-cascader) {
width: 100%;
}
}
&__dialog-search-actions {
display: flex;
align-items: flex-start;
justify-content: flex-end;
}
&__freight-form {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
column-gap: 24px;
row-gap: 16px;
width: 100%;
:deep(.el-form-item) {
min-width: 0;
margin-bottom: 0;
}
:deep(.el-form-item__content) {
min-width: 0;
}
:deep(.el-input),
:deep(.el-select) {
width: 100%;
}
&--road {
grid-template-columns: repeat(3, minmax(0, 1fr));
column-gap: 56px;
row-gap: 16px;
:deep(.business-crud-page__append-select) {
width: 112px;
}
}
}
&__dialog-pagination {
display: flex;
justify-content: flex-end;
margin-top: 12px;
}
&__map-toolbar {
display: flex;
gap: 8px;
margin-bottom: 12px;
.el-input {
flex: 1;
min-width: 0;
}
}
&__map {
width: 100%;
height: 420px;
border: 1px solid #eff1f7;
}
&__map-info {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 10px;
color: #606266;
line-height: 1.6;
}
&__date-range {
display: flex;
align-items: center;
width: 100%;
min-width: 0;
}
&__date {
flex: 1;
min-width: 0;
}
&__date-separator {
flex: 0 0 auto;
padding: 0 8px;
color: #303133;
}
&__inline-number {
display: flex;
align-items: center;
width: 100%;
.el-input {
flex: 1;
min-width: 0;
}
span {
flex: 0 0 auto;
margin-left: 8px;
color: #303133;
}
}
&__billing {
width: 100%;
}
&__billing-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
&__billing-switch {
display: flex;
align-items: center;
gap: 12px;
color: #303133;
}
&__billing-table {
width: 100%;
:deep(.el-table__header th) {
background: #f5f7fa;
color: #303133;
font-weight: 600;
}
:deep(.el-table__cell) {
border-color: #eff1f7;
}
}
&__settlement-rule {
width: 100%;
}
&__settlement-switch {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 18px;
color: #303133;
}
&__module-form {
width: 100%;
:deep(.el-form-item) {
margin-bottom: 16px;
}
:deep(.el-select),
:deep(.el-date-editor.el-input) {
width: 100%;
}
}
&__module-form--three {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
column-gap: 72px;
}
&__module-form--two {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
column-gap: 96px;
}
&__billing-dialog {
:deep(.el-dialog__body) {
padding-top: 28px;
}
:deep(.el-dialog__body),
:deep(.el-form) {
width: 100%;
}
}
&__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-head {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
padding: 0 0 12px;
}
&__dispatch-list-title {
font-size: 18px;
font-weight: 600;
color: #303133;
}
&__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;
}
}
&__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 {
:deep(.el-dialog__body) {
padding-top: 20px;
}
}
&__dispatch-item-form {
width: 100%;
padding: 16px;
border: 1px solid #ebeef5;
background: #fff;
> .dialog-section-title {
margin-top: 16px;
&:first-child {
margin-top: 0;
}
}
> .business-crud-page__dispatch-shipping-form,
> .business-crud-page__cargo-wrap,
> .business-crud-page__dispatch-item-grid,
> .business-crud-page__attachment {
background: #fff;
}
> .business-crud-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(4, 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% - 72px) / 4);
}
&__billing-form-row {
display: grid;
grid-template-columns: minmax(360px, 520px) 1fr;
column-gap: 48px;
align-items: center;
}
&__billing-default {
margin-bottom: 18px;
}
&__billing-plan-remark {
width: 80%;
}
&__billing-rule-head {
display: flex;
align-items: center;
justify-content: space-between;
margin: 20px 0 12px;
}
&__required-column {
&::before {
margin-right: 4px;
color: #f56c6c;
content: '*';
}
}
&__billing-tip {
display: flex;
max-width: 620px;
color: #ff6f86;
line-height: 1.7;
span {
position: relative;
display: inline-flex;
flex: 0 0 auto;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
margin-right: 12px;
border-radius: 50%;
background: #ffd6ea;
color: #ff4f92;
font-weight: 600;
&::after {
position: absolute;
right: 6px;
bottom: -8px;
width: 0;
height: 0;
border: 8px solid transparent;
border-top-color: #ffd6ea;
content: '';
}
}
}
&__billing-rule-wrap {
width: 100%;
overflow-x: auto;
}
&__billing-rule-table {
width: 100%;
min-width: 2230px;
:deep(.el-table__header th) {
background: #f5f7fa;
color: #303133;
font-weight: 600;
}
:deep(.el-table__cell) {
border-color: #eff1f7;
vertical-align: top;
}
}
&__limit-list {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
}
&__limit-row {
display: flex;
align-items: center;
gap: 8px;
.el-input {
flex: 1;
min-width: 0;
}
.el-button {
flex: 0 0 auto;
padding: 0;
}
}
&__dialog-footer {
display: flex;
justify-content: center;
gap: 16px;
}
&__match-form {
padding: 12px 32px 4px;
.el-select,
.el-cascader {
width: 100%;
}
}
&__attachment {
width: 100%;
}
&__attachment-description {
margin-bottom: 12px;
color: #606266;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
}
&__attachment-description-item + &__attachment-description-item {
margin-top: 4px;
}
&__attachment-description-label {
color: #303133;
font-weight: 600;
}
&__attachment-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
&__attachment-table {
width: 100%;
min-width: 0;
:deep(.el-table__header th) {
background: #f5f7fa;
color: #303133;
font-weight: 600;
}
:deep(.el-table__cell) {
border-color: #eff1f7;
}
}
&__change-table {
width: 100%;
:deep(.el-table__header th) {
background: #f5f7fa;
color: #303133;
font-weight: 600;
}
:deep(.el-table__cell) {
border-color: #eff1f7;
}
}
&__detail-content {
display: flex;
flex-direction: column;
gap: 12px;
.section-card {
:deep(.el-descriptions__label) {
width: 140px;
color: #606266;
font-weight: 600;
text-align: right;
}
:deep(.el-descriptions__content) {
min-height: 42px;
white-space: pre-wrap;
word-break: break-word;
}
}
}
&__detail-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 {
border: 1px solid #dfe3e8;
background: #fff;
}
&__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;
}
}
&__waybill-detail-summary {
:deep(.el-card__body) {
padding: 18px 22px;
}
}
&__waybill-heading {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 18px;
strong {
font-size: 20px;
}
}
&__waybill-route-change-link {
margin-left: auto;
}
&__waybill-summary-grid {
display: grid;
grid-template-columns: repeat(4, minmax(130px, 1fr)) minmax(420px, 2.5fr);
gap: 16px 24px;
}
&__waybill-summary-item {
display: flex;
flex-direction: column;
gap: 8px;
min-width: 0;
}
&__waybill-summary-item.is-plan {
grid-column: 1;
grid-row: 2;
}
&__waybill-label {
color: #909399;
}
&__waybill-value {
overflow: hidden;
color: #303133;
font-weight: 500;
text-overflow: ellipsis;
white-space: nowrap;
&.is-link {
color: #409eff;
}
}
&__waybill-route {
grid-row: 1 / span 3;
grid-column: 5;
min-width: 0;
padding: 8px 0;
}
&__waybill-route-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
b {
display: inline-flex;
flex: 0 0 auto;
width: 34px;
height: 34px;
align-items: center;
justify-content: center;
color: #fff;
border-radius: 4px;
background: #409eff;
&.end {
background: #e6a23c;
}
}
strong {
min-width: 0;
overflow: hidden;
color: #303133;
font-size: 18px;
text-overflow: ellipsis;
white-space: nowrap;
}
&::before {
flex: 1;
order: 2;
height: 4px;
content: '';
background: #e4e7ed;
}
b.end {
order: 3;
}
strong:last-child {
order: 4;
}
}
&__waybill-route-text {
margin-top: 12px;
overflow: hidden;
color: #606266;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
}
&__waybill-attachments {
grid-column: 1 / 5;
display: flex;
flex-direction: column;
gap: 8px;
div {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
}
&__waybill-carrier-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 18px 32px;
padding: 4px 20px 8px;
}
&__waybill-detail-field {
display: flex;
align-items: baseline;
gap: 12px;
min-width: 0;
span {
flex: 0 0 auto;
color: #909399;
}
strong {
min-width: 0;
overflow: hidden;
color: #303133;
font-weight: 500;
text-overflow: ellipsis;
white-space: nowrap;
}
}
&__waybill-detail-field--remark {
grid-column: 3 / -1;
}
&__waybill-fee-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
.business-crud-page__waybill-fee-value {
color: #409eff;
font-size: 16px;
}
}
&__waybill-detail-bottom {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
.section-card {
min-height: 300px;
}
}
&__waybill-map-empty {
display: grid;
min-height: 230px;
place-items: center;
color: #909399;
border: 1px solid #eff1f7;
}
&__waybill-track-actions {
display: flex;
gap: 8px;
}
&__waybill-process-actions {
display: flex;
gap: 8px;
}
&__voucher-image-grid {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 8px;
min-height: 120px;
}
&__voucher-image-item {
aspect-ratio: 1;
overflow: hidden;
cursor: pointer;
border: 1px solid #eff1f7;
border-radius: 4px;
.el-image {
display: block;
width: 100%;
height: 100%;
}
}
&__route-change-dialog {
:deep(.el-dialog__body) {
padding-top: 12px;
}
.section-card {
margin-bottom: 12px;
}
}
&__route-change-original {
margin-bottom: 6px;
color: #909399;
font-size: 12px;
}
&__route-change-location {
color: #409eff;
cursor: pointer;
}
&__route-change-hint {
color: #909399;
font-size: 12px;
}
&__route-change-list {
display: flex;
flex-direction: column;
gap: 8px;
}
&__route-change-item {
display: grid;
grid-template-columns: 42px minmax(0, 1fr) minmax(120px, auto) 24px;
align-items: center;
gap: 12px;
padding: 10px 12px;
border: 1px solid #eff1f7;
border-radius: 4px;
background: #fff;
cursor: move;
}
&__route-change-type {
display: inline-flex;
width: 28px;
height: 28px;
align-items: center;
justify-content: center;
color: #fff;
border-radius: 50%;
background: #409eff;
}
&__route-change-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
justify-content: flex-end;
}
&__route-change-record-content {
white-space: pre-wrap;
}
&__common-address-toolbar {
display: flex;
gap: 8px;
margin-bottom: 12px;
.el-input {
width: 260px;
}
}
@media (max-width: 1200px) {
&__waybill-summary-grid {
grid-template-columns: repeat(4, minmax(130px, 1fr));
}
&__waybill-route {
grid-row: auto;
grid-column: 1 / -1;
}
&__waybill-attachments {
grid-column: 1 / -1;
}
}
@media (max-width: 800px) {
&__waybill-summary-grid,
&__waybill-detail-bottom {
grid-template-columns: 1fr;
}
&__waybill-carrier-grid {
grid-template-columns: 1fr;
}
&__waybill-detail-field--remark {
grid-column: auto;
}
}
&__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);
}
&__route-address-row &__route-label:nth-of-type(2),
&__route-address-row .el-input:last-child {
grid-column: auto;
}
}
@media (max-width: 960px) {
&__route-address-row {
grid-template-columns: 1fr;
}
&__route-label {
text-align: left;
}
&__dialog-search-actions {
justify-content: flex-start;
}
}
}
.section-card {
margin-bottom: 0 !important;
}
/* 编辑弹窗:含分组的 Avue form 渲染为白卡风格 */
:deep(.avue-form__group:has(.dialog-section-title)) {
background: #fff;
border-radius: 6px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
padding: 16px;
margin-bottom: 16px;
.avue-form__group--title {
padding: 0 !important;
margin-bottom: 12px;
}
.avue-form__row {
padding: 0 !important;
}
}
</style>