Files
tms-erp-web/src/views/business/components/business-crud-page.vue
b2894lxlx 433c611195 1、完善发货模板
2、完善运输计划
3、完善运单管理
2026-08-05 15:52:24 +08:00

9660 lines
341 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"
@on-load="onLoad"
>
<template #menu-left>
<el-button
type="primary"
:icon="actionButtonLikeSearch ? undefined : 'el-icon-plus'"
:plain="!actionButtonLikeSearch"
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 #basicInfoTitle-form>
<div class="dialog-section-title">基本信息</div>
</template>
<template #templateInfoTitle-form>
<div class="dialog-section-title">模板信息</div>
</template>
<template #contractCategory-label>合同类型</template>
<template #projectName-form>
<el-select
v-model="selectedProjectId"
class="business-crud-page__field"
placeholder="请选择项目名称"
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.planName"
class="business-crud-page__field"
placeholder="请选择发货计划"
filterable
clearable
:loading="shippingPlanLoading"
:disabled="dialogReadonly"
@visible-change="visible => visible && loadShippingPlanOptions()"
@change="handleShippingPlanChange"
>
<el-option
v-for="item in shippingPlanOptions"
:key="item.id || item.planName"
:label="item.planName"
:value="item.planName"
/>
</el-select>
<el-input
v-else
v-model="form.planName"
class="business-crud-page__field"
placeholder="请输入计划名称"
clearable
maxlength="50"
show-word-limit
:disabled="dialogReadonly"
/>
</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"
:suffix-icon="transportStationMode ? Search : undefined"
:disabled="transportAddressSelectDisabled"
@click="openTransportPrimaryAddressPicker('departure')"
/>
<el-input
v-model="form.departureAddress"
readonly
:placeholder="transportAddressDetailPlaceholder"
:suffix-icon="transportStationMode ? undefined : Location"
:disabled="transportAddressSelectDisabled"
@click="openTransportSecondaryAddressPicker('departure')"
/>
<el-tooltip content="选择常用地址" placement="top">
<el-button
type="primary"
:icon="List"
link
:disabled="transportAddressSelectDisabled"
@click="openTransportAddressDialog('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"
:suffix-icon="transportStationMode ? Search : undefined"
:disabled="transportAddressSelectDisabled"
@click="openTransportPrimaryAddressPicker('arrival')"
/>
<el-input
v-model="form.arrivalAddress"
readonly
:placeholder="transportAddressDetailPlaceholder"
:suffix-icon="transportStationMode ? undefined : Location"
:disabled="transportAddressSelectDisabled"
@click="openTransportSecondaryAddressPicker('arrival')"
/>
<el-tooltip content="选择常用地址" placement="top">
<el-button
type="primary"
:icon="List"
link
:disabled="transportAddressSelectDisabled"
@click="openTransportAddressDialog('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">任务信息</div>
</template>
<template #taskInfoForm-form>
<div class="business-crud-page__task">
<div class="business-crud-page__task-toolbar">
<el-segmented
v-model="form.taskEntryMode"
:options="taskEntryModeOptions"
:disabled="dialogReadonly"
@change="handleTaskEntryModeChange"
/>
</div>
<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="240" align="center">
<template #header>
<span class="business-crud-page__required-column">货物名称</span>
</template>
<template #default="{ row }">
<el-select
v-model="row.cargoName"
placeholder="请输入"
filterable
allow-create
default-first-option
clearable
:disabled="dialogReadonly"
:loading="isTaskCargoOptionsLoading(row.cargoTypePath)"
@visible-change="
visible => visible && loadTaskCargoOptionsByPath(row.cargoTypePath)
"
@change="value => handleTaskCargoRowNameChange(row, value)"
>
<el-option
v-for="item in getTaskCargoRowOptions(row)"
:key="item.id || item.cargoCode || item.cargoName"
:label="formatBillingCargoTypeLabel(item)"
:value="formatBillingCargoTypeLabel(item)"
/>
</el-select>
</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="从货物类型获取"
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="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="primary"
@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="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 label="司机" class="business-crud-page__task-span-1">
<el-select
v-model="form.driverName"
placeholder="请输入"
filterable
clearable
allow-create
default-first-option
:suffix-icon="Search"
:loading="taskDriverLoading"
:disabled="dialogReadonly"
@visible-change="visible => visible && loadTaskDriverOptions()"
@change="handleTaskDriverChange"
>
<el-option
v-for="item in taskDriverOptions"
:key="item.id || item.driverName || item.mobile"
:label="item.driverName || item.name"
:value="item.driverName || item.name"
/>
</el-select>
</el-form-item>
<el-form-item label="手机号" class="business-crud-page__task-span-1">
<el-input
v-model="form.driverPhone"
placeholder="请输入"
clearable
: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
:disabled="dialogReadonly"
/>
</el-form-item>
</div>
<div class="business-crud-page__task-row">
<el-form-item label="里程(km)" class="business-crud-page__task-span-1">
<el-input
v-model="form.mileage"
placeholder="请输入"
clearable
:disabled="dialogReadonly"
@input="value => handleTaskNumberInput('mileage', value)"
/>
</el-form-item>
<el-form-item label="预计发货日期" 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="预计完成日期" 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>
<div class="business-crud-page__task-row">
<el-form-item label="司机" class="business-crud-page__task-span-1">
<el-select
v-model="form.driverName"
placeholder="请输入"
filterable
clearable
allow-create
default-first-option
:suffix-icon="Search"
:loading="taskDriverLoading"
:disabled="dialogReadonly"
@visible-change="visible => visible && loadTaskDriverOptions()"
@change="handleTaskDriverChange"
>
<el-option
v-for="item in taskDriverOptions"
:key="item.id || item.driverName || item.mobile"
:label="item.driverName || item.name"
:value="item.driverName || item.name"
/>
</el-select>
</el-form-item>
<el-form-item label="司机手机号" required class="business-crud-page__task-span-1">
<el-input
v-model="form.driverPhone"
placeholder="请输入"
clearable
: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
:disabled="dialogReadonly"
/>
</el-form-item>
<el-form-item label="挂车车牌号" class="business-crud-page__task-span-1">
<el-input
v-model="form.trailerVehicleNo"
placeholder="请输入"
clearable
: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.escortName"
placeholder="请输入"
clearable
:disabled="dialogReadonly"
/>
</el-form-item>
<el-form-item label="押运人手机号" class="business-crud-page__task-span-1">
<el-input
v-model="form.escortPhone"
placeholder="请输入"
clearable
: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
:disabled="dialogReadonly"
@input="value => handleTaskNumberInput('mileage', value)"
/>
</el-form-item>
</div>
<div class="business-crud-page__task-row">
<el-form-item label="预计发货日期" 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="预计完成日期" 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>
</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"
>
<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"
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
:model-value="taskCargoQuantityTotal"
disabled
placeholder="自动汇总货物数量"
>
<template #append>
<el-select
v-model="form.quantityUnit"
class="business-crud-page__append-select"
disabled
placeholder="吨"
>
<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="运费">
<el-input :model-value="taskFreightAmount" disabled placeholder="自动计算">
<template #append>元</template>
</el-input>
</el-form-item>
<el-form-item label="其他运费合计">
<el-input
v-model="form.otherFeeTotal"
placeholder="请输入"
clearable
:disabled="dialogReadonly"
@input="value => handleTaskNumberInput('otherFeeTotal', value)"
>
<template #append>元</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>
<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 label="司机">
<el-select
v-model="form.driverName"
placeholder="请选择或输入司机"
filterable
clearable
allow-create
default-first-option
:loading="taskDriverLoading"
:disabled="dialogReadonly"
@visible-change="visible => visible && loadTaskDriverOptions()"
@change="handleTaskDriverChange"
>
<el-option
v-for="item in taskDriverOptions"
:key="item.id || item.driverName || item.mobile"
:label="item.driverName || item.name"
:value="item.driverName || item.name"
/>
</el-select>
</el-form-item>
<el-form-item label="手机号" :required="form.carrierType === '自运'">
<el-input
v-model="form.driverPhone"
placeholder="请输入手机号"
clearable
:disabled="dialogReadonly"
/>
</el-form-item>
<el-form-item label="车牌号" required>
<el-input
v-model="form.vehicleNo"
placeholder="请输入车牌号"
clearable
: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-select
v-model="form.cargoName"
placeholder="请选择货物"
filterable
clearable
:disabled="dialogReadonly || !form.cargoType"
:loading="taskCargoLoading"
@visible-change="visible => visible && loadTaskCargoOptionsForForm()"
@change="handleTaskCargoChange"
>
<el-option
v-for="item in taskCargoOptions"
:key="item.id || item.cargoCode || item.cargoName"
:label="formatBillingCargoTypeLabel(item)"
:value="formatBillingCargoTypeLabel(item)"
/>
</el-select>
</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 #suffix>
<el-select
v-model="form.quantityUnit"
class="business-crud-page__suffix-select"
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="里程(公里)">
<el-input
v-model="form.mileage"
placeholder="请输入里程"
clearable
:disabled="dialogReadonly"
@input="value => handleTaskNumberInput('mileage', value)"
/>
</el-form-item>
<el-form-item label="预计发货日期">
<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="预计完成日期">
<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 #suffix>
<el-select
v-model="form.priceUnit"
class="business-crud-page__suffix-select business-crud-page__suffix-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="备注" 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>
</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-input v-model="row.cargoName" placeholder="请输入" :disabled="dialogReadonly" />
</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="primary"
@click="removeTransportCargoRow($index)"
>
删除
</el-link>
</div>
</template>
</el-table-column>
</el-table>
<div class="business-crud-page__cargo-total">
运输数量合计:{{ transportCargoQuantityTotal }}
</div>
</div>
</template>
<template #organizationName-form>
<el-cascader
v-model="selectedOrganizationId"
class="business-crud-page__field"
placeholder="请选择所属组织"
: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"
/>
</div>
<el-table :data="contractFileRows" border class="business-crud-page__attachment-table">
<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" />
<el-table-column label="操作" width="120" align="center">
<template #default="{ row, $index }">
<el-link
v-if="!dialogReadonly"
type="primary"
@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">
<template #default="{ row, $index }">
<el-link type="primary" @click="openBillingPlan(row, $index)">
编辑
</el-link>
<el-link
v-if="!dialogReadonly"
type="primary"
@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
: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>
<el-date-picker
v-model="settlementRuleForm.billStartDate"
type="date"
placeholder="请选择账单起始日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
:disabled="dialogReadonly"
/>
</el-form-item>
<el-form-item label="结算类型" required>
<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>
<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="账单截单日">
<el-select
v-model="settlementRuleForm.billCutoffDay"
placeholder="请选择账单截单日"
:disabled="dialogReadonly"
>
<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>
<el-select
v-model="settlementRuleForm.cycleDays"
placeholder="请选择周期天数"
:disabled="dialogReadonly"
>
<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">
<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" />
<el-table-column label="操作" width="100" align="center">
<template #default="{ row, $index }">
<el-link
v-if="!dialogReadonly"
type="primary"
@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" />
<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">
<template #default="{ row }">
<el-link type="primary" @click="handleChangeRecordFlow(row)"> 流程 </el-link>
</template>
</el-table-column>
</el-table>
</template>
<template #menu-form-before>
<el-button
v-if="config.enableDraftSave && !dialogReadonly"
type="primary"
plain
:loading="draftSaveLoading"
@click="handleDraftSave"
>
保存
</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)"
>
完成
</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="primary" 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="`${config.title}详情`"
append-to-body
width="96%"
class="business-crud-page__detail-dialog"
>
<div v-loading="detailLoading">
<section
v-for="section in detailSections"
:key="section.title"
class="business-crud-page__detail-section"
>
<div class="dialog-section-title">{{ section.title }}</div>
<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>
<section v-if="config.enableTransportPlanForm" class="business-crud-page__detail-section">
<div class="dialog-section-title">货物信息</div>
<el-table :data="transportCargoRows" border 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>
<section v-if="config.enableAttachmentTable" class="business-crud-page__detail-section">
<div class="dialog-section-title">{{ config.attachmentTitle || '附件' }}</div>
<el-table :data="attachmentRows" border empty-text="暂无附件">
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column label="文件名" min-width="260">
<template #default="{ row }">{{ row.originalName || row.name || '-' }}</template>
</el-table-column>
<el-table-column 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="180" />
</el-table>
</section>
</div>
<template #footer>
<el-button type="primary" @click="detailBox = 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="操作" width="150" 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-split">|</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">
{{ dispatchRow.transportType || '公路整车' }}
</el-tag>
</div>
</div>
<div class="business-crud-page__dispatch-summary">
<div class="business-crud-page__dispatch-summary-left">
<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">
{{ dispatchPlanCargoInfo || '-' }}
</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 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>
</div>
<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-line" />
<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>
</div>
</div>
<div class="business-crud-page__dispatch-list">
<div class="business-crud-page__dispatch-list-head">
<div class="business-crud-page__dispatch-list-title">待调度列表</div>
<el-button
type="primary"
:disabled="dispatchSummaryRemainingQuantity <= 0"
@click="handleDispatchAddRow"
>
新增
</el-button>
<div class="business-crud-page__dispatch-list-summary">
{{ dispatchSummaryText }}
</div>
</div>
<el-table
:data="dispatchRows"
border
height="260"
row-key="_key"
class="business-crud-page__dispatch-table"
>
<el-table-column type="index" label="序号" width="70" align="center" fixed="left" />
<el-table-column
prop="transportType"
label="运输方式"
min-width="180"
align="center"
show-overflow-tooltip
/>
<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
prop="departureAddress"
label="发货地址"
min-width="220"
align="center"
show-overflow-tooltip
/>
<el-table-column
prop="arrivalAddress"
label="收货地址"
min-width="220"
align="center"
show-overflow-tooltip
/>
<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
/>
<el-table-column label="操作" width="180" 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>
</div>
</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="taskEntryModeOptions"
class="business-crud-page__dispatch-entry-mode"
/>
</div>
<template v-if="dispatchItemForm.taskEntryMode === 'full'">
<div class="dialog-section-title">货物信息</div>
<div class="business-crud-page__dispatch-item-grid">
<el-form-item label="货物名称" required>
<el-input v-model="dispatchItemForm.cargoName" placeholder="请输入" />
</el-form-item>
<el-form-item label="货物类型" required>
<el-input v-model="dispatchItemForm.cargoType" 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>
</el-form-item>
<el-form-item label="包装">
<el-select v-model="dispatchItemForm.packageType" clearable placeholder="请选择">
<el-option
v-for="item in packageOptions"
: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>
</div>
<div class="dialog-section-title">费用信息</div>
<div class="business-crud-page__dispatch-item-grid">
<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="请输入" />
</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-select
v-model="dispatchItemForm.driverName"
placeholder="请输入"
filterable
clearable
allow-create
default-first-option
:suffix-icon="Search"
:loading="taskDriverLoading"
@visible-change="visible => visible && loadDispatchDriverOptions()"
@change="handleDispatchDriverChange"
>
<el-option
v-for="item in taskDriverOptions"
:key="item.id || item.driverName || item.mobile"
:label="item.driverName || item.name"
:value="item.driverName || item.name"
/>
</el-select>
</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="挂车车牌号"
required
>
<el-input v-model="dispatchItemForm.trailerVehicleNo" placeholder="请输入" />
</el-form-item>
<el-form-item v-if="dispatchItemForm.carrierType !== '承运商'" label="押运人" required>
<el-input v-model="dispatchItemForm.escortName" placeholder="请输入" />
</el-form-item>
<el-form-item
v-if="dispatchItemForm.carrierType !== '承运商'"
label="押运人手机号"
required
>
<el-input v-model="dispatchItemForm.escortPhone" placeholder="请输入" />
</el-form-item>
<el-form-item label="货物类型" required>
<el-input v-model="dispatchItemForm.cargoType" placeholder="请输入" />
</el-form-item>
<el-form-item label="货物名称" required>
<el-input v-model="dispatchItemForm.cargoName" placeholder="请输入" />
</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>
</el-form-item>
<el-form-item label="里程(公里)" :required="dispatchItemForm.carrierType !== '承运商'">
<el-input v-model="dispatchItemForm.mileage" placeholder="请输入" />
</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="备注" 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"
>
<el-form-item label="承运类型" required class="business-crud-page__dispatch-item-span-2">
<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="dispatchItemForm.carrierType"
label="司机"
:required="dispatchItemForm.carrierType !== '承运商'"
>
<el-select
v-model="dispatchItemForm.driverName"
placeholder="请输入"
filterable
clearable
allow-create
default-first-option
:suffix-icon="Search"
:loading="taskDriverLoading"
@visible-change="visible => visible && loadDispatchDriverOptions()"
@change="handleDispatchDriverChange"
>
<el-option
v-for="item in taskDriverOptions"
:key="item.id || item.driverName || item.mobile"
:label="item.driverName || item.name"
:value="item.driverName || item.name"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="dispatchItemForm.carrierType"
:label="dispatchItemForm.carrierType === '承运商' ? '手机号' : '司机手机号'"
:required="dispatchItemForm.carrierType !== '承运商'"
>
<el-input v-model="dispatchItemForm.driverPhone" placeholder="请输入" />
</el-form-item>
<el-form-item
v-if="dispatchItemForm.carrierType"
label="车牌号/航班号/船号/班列号"
required
>
<el-input v-model="dispatchItemForm.vehicleNo" placeholder="请输入" />
</el-form-item>
<el-form-item
v-if="dispatchItemForm.carrierType && dispatchItemForm.carrierType !== '承运商'"
label="挂车车牌号"
required
>
<el-input v-model="dispatchItemForm.trailerVehicleNo" placeholder="请输入" />
</el-form-item>
<el-form-item
v-if="dispatchItemForm.carrierType && dispatchItemForm.carrierType !== '承运商'"
label="押运人"
required
>
<el-input v-model="dispatchItemForm.escortName" placeholder="请输入" />
</el-form-item>
<el-form-item
v-if="dispatchItemForm.carrierType && dispatchItemForm.carrierType !== '承运商'"
label="押运人手机号"
required
>
<el-input v-model="dispatchItemForm.escortPhone" placeholder="请输入" />
</el-form-item>
<el-form-item
v-if="dispatchItemForm.carrierType && dispatchItemForm.carrierType !== '承运商'"
label="里程(km)"
required
>
<el-input v-model="dispatchItemForm.mileage" placeholder="请输入" />
</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>
<template v-if="dispatchItemForm.taskEntryMode === 'full'">
<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"
>
<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="请输入附件描述" />
</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" />
<el-table-column label="操作" width="100" align="center">
<template #default="{ $index }">
<el-link type="danger" @click="removeDispatchItemAttachment($index)">删除</el-link>
</template>
</el-table-column>
</el-table>
</div>
</template>
</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="96%"
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"
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="primary"
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="primary"
@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" />
<el-table-column prop="createTime" label="创建时间" min-width="160" />
<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="请选择"
>
<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" />
<el-table-column prop="createTime" label="创建时间" min-width="160" />
<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="选择站点"
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" />
<el-table-column prop="createTime" label="创建时间" min-width="160" />
<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" />
<el-table-column prop="createTime" label="创建时间" min-width="160" />
<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">
<el-upload
drag
action="#"
accept=".xls,.xlsx"
:auto-upload="false"
:show-file-list="false"
:disabled="cargoImportLoading"
:on-change="handleCargoImportFileChange"
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将 Excel 文件拖到此处,或点击上传</div>
<template #tip>
<div class="el-upload__tip">
按货物信息表头读取:货物名称、货物类型、数量、数量单位、包装、品牌、规格、型号、物料编码、设备编码、备注
</div>
</template>
</el-upload>
<template #footer>
<el-button @click="cargoImportBox = 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 } 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 { 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 { getList as getTransportPlanList } from '@/api/business/transport-plan';
import { getList as getCarrierCustomerList } from '@/api/vehicle/customer-archive';
import { getList as getDriverList } from '@/api/transportCapacity/driver';
import { getDictionary } from '@/api/system/dictbiz';
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 { List, Location, Search } from '@element-plus/icons-vue';
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 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: '',
packageType: '',
brand: '',
specification: '',
model: '',
materialCode: '',
deviceCode: '',
remark: '',
});
const defaultDispatchRow = () => ({
taskEntryMode: 'simple',
transportType: '',
carrierType: '承运商',
carrierId: '',
carrierName: '',
driverId: '',
driverName: '',
driverPhone: '',
vehicleNo: '',
trailerVehicleNo: '',
escortName: '',
escortPhone: '',
mileage: '',
cargoType: '',
cargoName: '',
cargoInfo: '',
quantity: '',
quantityUnit: '吨',
specification: '',
model: '',
packageType: '',
brand: '',
materialCode: '',
unitPrice: '',
priceUnit: '元/吨',
otherFeeTotal: '',
estimatedStartTime: '',
estimatedEndTime: '',
departureAddress: '',
departureName: '',
departureContact: '',
departurePhone: '',
arrivalAddress: '',
arrivalName: '',
arrivalContact: '',
arrivalPhone: '',
dispatchUserName: '',
dispatchTime: '',
attachmentsJson: '',
remark: '',
});
const taskCarrierTypes = ['承运商', '自运', '网货平台'];
export default {
components: { WaybillImportDialog },
name: 'BusinessCrudPage',
props: {
api: {
type: Object,
required: true,
},
config: {
type: Object,
required: true,
},
crudOption: {
type: Object,
required: true,
},
crudExcelOption: {
type: Object,
default: () => ({ column: [] }),
},
},
data() {
return {
List,
Location,
Search,
form: {},
suppressTransportTypeClear: false,
query: {},
loading: true,
data: [],
allDept: 0,
dialogReadonly: false,
detailBox: false,
detailLoading: false,
detailRow: {},
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: this.cloneOption(this.crudOption, estimateMenuButtonCount(this.config)),
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(),
dispatchItemAttachmentRows: [],
attachmentRows: [],
contractFileRows: [],
transportCargoRows: [],
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,
cargoImportBox: false,
cargoImportLoading: false,
billingPlanRows: [],
settlementRuleForm: defaultSettlementRule(),
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: [],
};
},
created() {
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();
}
this.consumeTemplateCreatePayload();
},
computed: {
...mapGetters(['permission', 'userInfo']),
permissionList() {
return {
addBtn: this.canCreate,
};
},
detailSections() {
return this.config.detailSections || [];
},
transportPlanImportDuplicateCount() {
return this.transportPlanImportRows.filter(row => row._duplicate).length;
},
transportPlanImportVisibleRows() {
return this.transportPlanImportTab === 'duplicate'
? this.transportPlanImportRows.filter(row => row._duplicate)
: this.transportPlanImportRows;
},
actionButtonLikeSearch() {
return this.config.actionButtonLikeSearch === true;
},
contractSelectEnabled() {
return this.config.enableContractSelect || this.config.enableTransportPlanForm;
},
shippingPlanSelectEnabled() {
return this.config.enableShippingPlanSelect === true;
},
shippingInfoFormEnabled() {
return this.config.enableShippingInfoForm || this.config.enableTransportPlanForm;
},
taskInfoFormEnabled() {
return this.config.enableTaskInfoForm === true;
},
taskEntryModeOptions() {
return [
{ label: '精简录入', value: 'simple' },
{ label: '完整录入', value: 'full' },
];
},
isTaskFullMode() {
return this.form.taskEntryMode === 'full';
},
isCarrierMode() {
return this.form.carrierType === '承运商';
},
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.parseJsonArray(this.dispatchRow.goodsJson);
},
dispatchPlanCargoInfo() {
const cargoInfo = this.dispatchPlanGoodsRows
.map(row => this.formatDispatchCargoInfo({}, row))
.filter(Boolean)
.join('; ');
return cargoInfo || this.dispatchRow.goodsInfo || '';
},
dispatchSummaryTotalQuantity() {
const goodsQuantity = this.dispatchPlanGoodsRows.reduce(
(total, row) =>
total +
this.parseDispatchQuantity(row.quantity || row.cargoQuantity || row.goodsQuantity),
0
);
if (goodsQuantity > 0) return goodsQuantity;
return this.parseDispatchQuantity(
this.dispatchRow.totalQuantity ||
this.dispatchRow.totalCargoQuantity ||
this.dispatchRow.quantity
);
},
dispatchSummaryAssignedQuantity() {
return this.dispatchRows.reduce(
(total, row) => total + this.parseDispatchQuantity(row.quantity),
0
);
},
dispatchSummaryRemainingQuantity() {
const total = this.dispatchSummaryTotalQuantity;
const assigned = this.dispatchSummaryAssignedQuantity;
const remain = total - assigned;
return remain > 0 ? remain : 0;
},
dispatchSummaryUnit() {
const unitRow = this.dispatchPlanGoodsRows.find(
row => row.quantityUnit || row.goodsQuantityUnit || row.unit
);
return (
unitRow?.quantityUnit ||
unitRow?.goodsQuantityUnit ||
unitRow?.unit ||
this.dispatchRow.quantityUnit ||
this.dispatchRows.find(row => row.quantityUnit)?.quantityUnit ||
'吨'
);
},
dispatchSummaryText() {
const total = this.formatDispatchQuantity(this.dispatchSummaryTotalQuantity);
const assigned = this.formatDispatchQuantity(this.dispatchSummaryAssignedQuantity);
const remain = this.formatDispatchQuantity(this.dispatchSummaryRemainingQuantity);
return `共${total}${this.dispatchSummaryUnit} | 已调度 ${assigned}${this.dispatchSummaryUnit},剩余${remain}${this.dispatchSummaryUnit}`;
},
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);
},
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)));
},
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.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,
};
},
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);
},
transportAddressSelectDisabled() {
return this.dialogReadonly || this.isBillingFieldEmpty(this.form.transportType);
},
transportMode() {
return this.resolveTransportMode(this.form.transportType);
},
transportStationMode() {
return ['water', 'rail', 'air'].includes(this.transportMode);
},
transportStationTypeLabel() {
const 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: {
'form.transportType'(value, oldValue) {
if (!this.shippingInfoFormEnabled || this.suppressTransportTypeClear) return;
if (String(value ?? '') === String(oldValue ?? '')) return;
this.handleTransportTypeChange(value);
},
},
methods: {
cloneOption(option, menuButtonCount) {
const nextOption = {
...option,
column: (option.column || []).map(column => ({ ...column })),
};
return applyTableMenuWidth(nextOption, menuButtonCount);
},
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.$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 || {};
this.suppressTransportTypeClear = true;
Object.assign(this.form, row);
this.$nextTick(() => {
this.suppressTransportTypeClear = false;
});
this.form.planName = this.form.planName || this.form.templateName || '';
this.selectedProjectId = this.form.projectId || '';
this.attachmentRows = this.parseJsonArray(this.form.attachmentsJson);
if (this.contractSelectEnabled) {
this.syncCurrentContractOption();
this.loadContractOptionsForProject();
}
if (this.config.enableProjectSelect) this.syncCurrentProjectOption();
if (this.config.enableTransportPlanForm) this.initTransportPlanFormRows();
if (this.taskInfoFormEnabled) this.initTaskInfoForm();
});
});
},
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;
},
openDetail(row) {
this.detailBox = true;
this.detailLoading = true;
this.detailRow = { ...row };
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.applyFormDetail(detail);
})
.finally(() => {
this.detailLoading = false;
});
},
displayStatus(row, prop) {
const textProp = prop === this.config.statusProp ? this.config.statusTextProp : '';
if (textProp && row[textProp]) {
return row[textProp];
}
const column = this.findColumn(this.option.column, prop);
const item = column && (column.dicData || []).find(dic => dic.value === row[prop]);
return item ? item.label : row[prop] || '未知';
},
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) {
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) {
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;
},
validateSettlementRule() {
const rule = this.normalizeSettlementRule(this.settlementRuleForm);
if (this.isBillingFieldEmpty(rule.billStartDate)) {
this.$message.warning('请选择账单起始日期');
return false;
}
if (this.isBillingFieldEmpty(rule.settlementType)) {
this.$message.warning('请选择结算类型');
return false;
}
if (this.isMonthlySettlement(rule) && this.isBillingFieldEmpty(rule.billCycleType)) {
this.$message.warning('请选择账单周期类型');
return false;
}
if (this.isFixedBillCutoffSettlement(rule)) {
const billCutoffDay = Number(rule.billCutoffDay);
if (!Number.isInteger(billCutoffDay) || billCutoffDay < 1 || billCutoffDay > 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.$message.warning('周期天数必须为1~365的整数');
return false;
}
}
return true;
},
normalizeRow(row) {
const submitRow = { ...row };
if (this.config.enableTransportPlanForm) {
if (!this.validateTransportPlanForm(submitRow)) return false;
submitRow.goodsJson = JSON.stringify(this.transportCargoRows);
}
if (this.taskInfoFormEnabled) {
if (!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 (!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) {
submitRow.approvalStatus = '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;
},
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;
}
for (const [index, cargo] of this.transportCargoRows.entries()) {
if (!cargo.cargoType) {
this.$message.warning(`第${index + 1}行货物类型不能为空`);
return false;
}
}
return true;
},
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;
}
this.getSaveRequest()(submitRow).then(
() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
done();
},
error => {
window.console.log(error);
this.stopSubmitLoading(loading);
}
);
},
getSaveRequest() {
if (this.config.saveAsDraft && typeof this.api.saveDraft === 'function') {
return this.api.saveDraft;
}
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);
if (!submitRow) return;
this.draftSaveLoading = true;
request(submitRow)
.then(() => {
this.$message.success('保存成功');
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.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.contractFileRows = [];
this.shippingPlanOptions = [];
if (this.contractSelectEnabled) {
this.contractOptions = [];
this.form.contractId = '';
this.form.contractName = '';
}
if (this.shippingPlanSelectEnabled) {
this.clearShippingPlan();
}
this.transportCargoRows = [];
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 || {};
this.$nextTick(() => {
this.suppressTransportTypeClear = false;
});
this.selectedProjectId = this.form.projectId || '';
this.selectedOrganizationId = this.form.organizationId || '';
this.attachmentRows = this.parseJsonArray(this.form.attachmentsJson);
this.contractFileRows = this.parseJsonArray(this.form.contractFileJson);
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.usedFundLimit = 0;
this.form.remainingFundLimit = 0;
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;
},
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, {})
.then(res => {
this.projectOptions = extractRecords(res);
this.syncCurrentProjectOption();
})
.finally(() => {
this.projectLoading = false;
});
},
syncCurrentProjectOption() {
if (!this.form.projectId || !this.form.projectName) return;
const exists = this.projectOptions.some(
item => String(item.id) === String(this.form.projectId)
);
if (!exists) {
this.projectOptions.unshift({
id: this.form.projectId,
projectName: this.form.projectName,
projectCode: this.form.projectCode,
undertakeDeptName: this.form.undertakeDeptName,
fundLimit: this.form.projectFundLimit,
});
}
},
handleProjectChange(projectId) {
const project = this.projectOptions.find(item => String(item.id) === String(projectId));
if (!project) {
this.form.projectId = '';
this.form.projectName = '';
this.form.projectCode = '';
this.form.undertakeDeptName = '';
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 = [];
}
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 = 0;
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.$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(planName) {
const plan = this.shippingPlanOptions.find(
item =>
String(item.planName) === String(planName) || String(item.id) === String(this.form.planId)
);
if (!plan) {
this.clearShippingPlan();
return;
}
this.applyShippingPlan(plan);
},
applyShippingPlan(plan = {}) {
this.form.planId = plan.id || '';
this.form.planName = plan.planName || '';
this.$refs.crud?.validateField?.('planName');
},
clearShippingPlan() {
this.form.planId = '';
this.form.planName = '';
},
initTaskInfoForm() {
if (!this.taskInfoFormEnabled) return;
const taskInfo = this.parseJsonObject(this.form.taskInfoJson);
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';
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.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 || '';
this.form.mileage = this.form.mileage || taskInfo.mileage || goodsInfo.mileage || '';
this.form.unitPrice = this.form.unitPrice || taskInfo.unitPrice || goodsInfo.unitPrice || '';
this.form.priceUnit = this.form.priceUnit || taskInfo.priceUnit || goodsInfo.priceUnit || '';
this.form.otherFeeTotal =
this.form.otherFeeTotal || taskInfo.otherFeeTotal || goodsInfo.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.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) {
this.form.taskEntryMode = value || 'simple';
if (this.form.taskEntryMode === 'full') {
this.initTaskFullCargoRows(this.parseJsonArray(this.form.goodsJson));
}
},
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: '承运商' })
.then(res => {
this.taskCarrierOptions = extractRecords(res);
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;
});
},
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 = []) {
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);
if (!params.secondCargoTypeName && !params.secondCargoTypeCode) {
this.setTaskCargoOptionsByKey(key, []);
return [];
}
this.setTaskCargoLoadingByKey(key, true);
return getCommonCargoList(1, 500, {
...params,
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);
});
});
},
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 || '';
}
},
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 =>
this.normalizeTransportCargoRow(row)
);
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 || '',
}));
},
handleTaskNumberInput(prop, value) {
this.form[prop] = this.normalizeTaskNumber(value);
},
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;
}
}
return true;
},
validateTaskInfoForm(row = {}) {
if (this.isTaskFullMode && !this.validateTaskCargoRows()) {
return false;
}
const requiredFields = this.config.transportFormRequiredFields || [
['carrierType', '承运类型'],
['vehicleNo', '车牌号'],
];
if (row.carrierType === '承运商') {
requiredFields.push(['carrierName', '承运商']);
}
if (row.carrierType === '自运') {
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', '里程'],
['unitPrice', '单价'],
['otherFeeTotal', '其他运费合计'],
];
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 (
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';
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,
trailerVehicleNo: row.trailerVehicleNo,
escortName: row.escortName,
escortPhone: row.escortPhone,
mileage: row.mileage,
estimatedStartTime: row.estimatedStartTime,
estimatedEndTime: row.estimatedEndTime,
unitPrice: row.unitPrice,
priceUnit: row.priceUnit,
otherFeeTotal: row.otherFeeTotal,
taskRemark: row.taskRemark,
};
row.taskInfoJson = JSON.stringify(taskInfo);
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,
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,
},
];
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 || row.unitPrice || '';
row.priceUnit = firstGoodsRow.priceUnit || row.priceUnit || '';
row.taskRemark = firstGoodsRow.remark || row.taskRemark || '';
},
initTransportPlanFormRows() {
if (!this.config.enableTransportPlanForm) return;
this.transportCargoRows = this.parseJsonArray(this.form.goodsJson).map(row =>
this.normalizeTransportCargoRow(row)
);
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 = {}) {
return {
...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 || '',
};
},
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(row);
if (index > -1) {
this.transportCargoRows.splice(index + 1, 0, nextRow);
} else {
this.transportCargoRows.push(nextRow);
}
this.syncTransportCargoJson();
},
removeTransportCargoRow(index) {
this.transportCargoRows.splice(index, 1);
if (
!this.transportCargoRows.length &&
(this.isTaskFullMode || this.config.enableTransportPlanForm)
) {
this.transportCargoRows.push(this.normalizeTransportCargoRow());
}
this.syncTransportCargoJson();
},
handleTransportCargoQuantityInput(row, value) {
const text = String(value || '').replace(/[^\d.]/g, '');
const parts = text.split('.');
row.quantity =
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 3)}` : parts[0];
this.syncTransportCargoJson();
},
handleTransportCargoTypeChange(row, value) {
const path = this.normalizeBillingCargoTypePath(value);
const cargoType = this.findBillingCargoTypeByPath(path);
const labels = this.getBillingCargoTypePathLabels(path);
row.cargoTypePath = path;
row.cargoType = labels.length ? labels[labels.length - 1] : '';
row.cargoTypeCode = cargoType?.cargoCode || cargoType?.code || cargoType?.id || '';
row.cargoName = '';
row.specification = '';
row.model = '';
this.syncTransportCargoJson();
this.loadTaskCargoOptionsByPath(path);
},
handleTransportTypeChange(value) {
this.form.transportTypeName = this.getTransportTypeLabel(value);
this.clearTransportShippingInfo();
},
clearTransportShippingInfo() {
[
'departureName',
'departureAddress',
'departureAddressCode',
'departureSiteCode',
'departureLongitude',
'departureLatitude',
'departureContact',
'departurePhone',
'arrivalName',
'arrivalAddress',
'arrivalAddressCode',
'arrivalSiteCode',
'arrivalLongitude',
'arrivalLatitude',
'arrivalContact',
'arrivalPhone',
].forEach(prop => {
this.form[prop] = '';
});
this.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);
},
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.transportAddressQuery = {};
this.transportAddressPage.currentPage = 1;
this.transportAddressBox = true;
},
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.transportAddressQuery = {};
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();
this.addTransportCargoRow(-1, this.mapCommonCargoRow(row));
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;
this.cargoImportLoading = true;
try {
const ExcelJSModule = await import('exceljs');
const ExcelJS = ExcelJSModule.default || ExcelJSModule;
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.load(await rawFile.arrayBuffer());
const worksheet = workbook.worksheets[0];
const headerRow = worksheet.getRow(1);
const headerMap = {};
headerRow.eachCell((cell, colNumber) => {
headerMap[String(cell.value || '').trim()] = colNumber;
});
const fieldMap = {
货物名称: 'cargoName',
货物类型: 'cargoType',
数量: 'quantity',
数量单位: 'quantityUnit',
包装: 'packageType',
品牌: 'brand',
规格: 'specification',
型号: 'model',
物料编码: 'materialCode',
设备编码: 'deviceCode',
备注: 'remark',
};
const rows = [];
worksheet.eachRow((row, rowNumber) => {
if (rowNumber === 1) return;
const nextRow = defaultTransportCargo();
Object.entries(fieldMap).forEach(([label, prop]) => {
const columnIndex = headerMap[label];
if (!columnIndex) return;
nextRow[prop] = String(row.getCell(columnIndex).value || '').trim();
});
if (Object.values(nextRow).some(value => String(value || '').trim())) {
rows.push(nextRow);
}
});
if (!rows.length) {
this.$message.warning('未读取到货物数据');
return;
}
rows.forEach(row => this.addTransportCargoRow(-1, row));
this.$message.success(`成功导入${rows.length}条货物`);
this.cargoImportBox = false;
} catch (error) {
window.console.log(error);
this.$message.error('货物导入失败,请检查文件格式');
} finally {
this.cargoImportLoading = false;
}
},
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.form.attachmentsJson = JSON.stringify(this.attachmentRows);
},
removeAttachment(index) {
this.attachmentRows.splice(index, 1);
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`;
},
downloadAttachment(row) {
const url = row.url || row.link;
if (!url) {
this.$message.warning('附件地址为空');
return;
}
downloadFileByUrl(url, row.originalName || row.name || '附件');
},
handleBatchDownload() {
this.attachmentRows.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,
}));
},
removeDispatchItemAttachment(index) {
this.dispatchItemAttachmentRows.splice(index, 1);
},
handleDispatchItemBatchDownload() {
this.dispatchItemAttachmentRows.forEach(row => this.downloadAttachment(row));
},
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.form.contractFileJson = JSON.stringify(this.contractFileRows);
},
removeContractFile(index) {
this.contractFileRows.splice(index, 1);
this.form.contractFileJson = JSON.stringify(this.contractFileRows);
},
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.page.currentPage = 1;
this.onLoad(this.page);
},
searchChange(params, done) {
this.query = params;
this.page.currentPage = 1;
this.onLoad(this.page, params);
done();
},
selectionChange(list) {
this.selectionList = list;
},
selectionClear() {
this.selectionList = [];
if (this.$refs.crud) {
this.$refs.crud.toggleSelection();
}
},
currentChange(currentPage) {
this.page.currentPage = currentPage;
},
sizeChange(pageSize) {
this.page.pageSize = pageSize;
},
refreshChange() {
this.onLoad(this.page, this.query);
},
handleScopeChange() {
if (!this.canViewAllDept) {
this.allDept = 0;
}
this.page.currentPage = 1;
this.onLoad(this.page);
},
normalizeSearchRangeValue(value, suffix) {
if (!value) return undefined;
return suffix ? `${value} ${suffix}` : value;
},
normalizeSearchRangeParams(params = {}) {
const query = { ...params };
const rangeMap = this.config.searchRangeMap || {};
Object.entries(rangeMap).forEach(([rangeProp, mapping]) => {
const [startProp, endProp, startSuffix, endSuffix] = Array.isArray(mapping)
? mapping
: [mapping.startProp, mapping.endProp, mapping.startSuffix, mapping.endSuffix];
const range = query[rangeProp];
if (Array.isArray(range) && range.length === 2) {
query[startProp] = this.normalizeSearchRangeValue(range[0], startSuffix);
query[endProp] = this.normalizeSearchRangeValue(range[1], endSuffix);
}
delete query[rangeProp];
});
return query;
},
normalizeQueryParams(params = {}) {
let query = { ...params };
if (Object.keys(this.config.searchRangeMap || {}).length) {
query = this.normalizeSearchRangeParams(query);
}
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.api
.getList(page.currentPage, page.pageSize, query)
.then(res => {
const result = res.data.data;
this.page.total = result.total;
this.data = result.records;
this.selectionClear();
})
.finally(() => {
this.loading = false;
});
},
buildExportParams() {
const query = this.normalizeQueryParams({ ...this.query, allDept: this.allDept });
return {
...query,
ids: this.ids,
[this.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 => {
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
const date = new Date(`${value}T00:00:00`);
return !Number.isNaN(date.getTime()) && date.toISOString().slice(0, 10) === value;
};
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: '完成',
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 = [];
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;
});
},
resetDispatchDialog() {
this.dispatchRow = {};
this.dispatchRows = [];
this.dispatchLoading = false;
this.dispatchSaving = '';
this.dispatchItemBox = false;
this.dispatchItemIndex = -1;
this.dispatchItemForm = defaultDispatchRow();
this.dispatchItemAttachmentRows = [];
},
buildDispatchRows(plan = {}) {
const goodsRows = this.parseJsonArray(plan.goodsJson);
const sourceRows = goodsRows.length ? goodsRows : [plan];
return sourceRows.map((item, index) => this.normalizeDispatchRow(plan, item, index));
},
normalizeDispatchRow(plan = {}, item = {}, index = 0) {
const cargoInfo = this.formatDispatchCargoInfo(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 || plan.vehicleNo || '',
trailerVehicleNo: item.trailerVehicleNo || '',
escortName: item.escortName || '',
escortPhone: item.escortPhone || '',
mileage: item.mileage || '',
cargoType: item.cargoType || item.goodsType || plan.cargoType || '',
cargoName: item.cargoName || item.goodsName || plan.cargoName || '',
cargoInfo,
quantity: item.quantity || item.cargoQuantity || item.goodsQuantity || '',
quantityUnit: item.quantityUnit || item.goodsQuantityUnit || item.unit || '吨',
specification: item.specification || item.spec || '',
model: item.model || '',
packageType: item.packageType || item.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 || '',
dispatchUserName: item.dispatchUserName || plan.dispatchUserName || '',
dispatchTime: item.dispatchTime || plan.dispatchTime || '',
attachmentsJson: item.attachmentsJson || '',
remark: item.remark || item.taskRemark || plan.remark || '',
};
},
formatDispatchCargoInfo(plan = {}, item = {}) {
const cargoName = item.cargoName || item.goodsName || plan.cargoName || '';
const cargoType = item.cargoType || item.goodsType || plan.cargoType || '';
const quantity = item.quantity || item.cargoQuantity || item.goodsQuantity || '';
const unit = item.quantityUnit || item.goodsQuantityUnit || item.unit || '';
const quantityText = quantity ? `${quantity}${unit}` : '';
return (
[cargoName, cargoType, quantityText].filter(Boolean).join(' - ') || plan.goodsInfo || ''
);
},
openDispatchItemDialog(index = -1, row = defaultDispatchRow()) {
const defaultGoods = this.dispatchPlanGoodsRows[0] || {};
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.dispatchSummaryRemainingQuantity),
quantityUnit: defaultGoods.quantityUnit || '吨',
specification: defaultGoods.specification || '',
model: defaultGoods.model || '',
packageType: defaultGoods.packageType || '',
departureName: this.dispatchRow.departureName || '',
departureAddress: this.dispatchRow.departureAddress || '',
departureContact: this.dispatchRow.departureContact || '',
departurePhone: this.dispatchRow.departurePhone || '',
arrivalName: this.dispatchRow.arrivalName || '',
arrivalAddress: this.dispatchRow.arrivalAddress || '',
arrivalContact: this.dispatchRow.arrivalContact || '',
arrivalPhone: this.dispatchRow.arrivalPhone || '',
};
this.dispatchItemIndex = index;
this.dispatchItemForm = {
...baseRow,
...(index >= 0 ? row : {}),
};
this.dispatchItemForm.carrierType = this.dispatchItemForm.carrierType || '承运商';
this.dispatchItemAttachmentRows = this.parseJsonArray(
index >= 0 ? row.attachmentsJson : this.dispatchRow.attachmentsJson
);
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: '承运商' })
.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 ||
'';
}
},
saveDispatchItem() {
const nextRow = {
...this.dispatchItemForm,
attachmentsJson: JSON.stringify(this.dispatchItemAttachmentRows),
};
const otherDispatchedQuantity = this.dispatchRows.reduce((total, row, index) => {
if (index === this.dispatchItemIndex) return total;
return total + this.parseDispatchQuantity(row.quantity);
}, 0);
const currentQuantity = this.parseDispatchQuantity(nextRow.quantity);
const totalQuantity = this.dispatchSummaryTotalQuantity;
if (totalQuantity > 0 && otherDispatchedQuantity + currentQuantity > totalQuantity) {
this.$message.warning(
`已调度数量合计不能超过总数量${this.formatDispatchQuantity(totalQuantity)}${
this.dispatchSummaryUnit
}`
);
return;
}
if (this.dispatchItemIndex >= 0) {
this.dispatchRows.splice(this.dispatchItemIndex, 1, nextRow);
} else {
this.dispatchRows.push(nextRow);
}
this.dispatchItemBox = false;
this.dispatchItemIndex = -1;
this.dispatchItemForm = defaultDispatchRow();
this.dispatchItemAttachmentRows = [];
},
resetDispatchItemDialog() {
this.dispatchItemIndex = -1;
this.dispatchItemForm = defaultDispatchRow();
this.dispatchItemAttachmentRows = [];
},
removeDispatchRow(index) {
this.$confirm('确定删除该调度明细?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
this.dispatchRows.splice(index, 1);
});
},
handleDispatchAddRow() {
if (this.dispatchSummaryRemainingQuantity <= 0) {
this.$message.warning('剩余数量为0不能新增调度明细');
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: 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;
}
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.trailerVehicleNo ||
!row.escortName ||
!row.escortPhone ||
!row.mileage))
);
});
if (invalidRow) {
this.$message.warning('请补全调度明细中的必填信息');
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.dispatchSummaryRemainingQuantity <= 0;
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;
}
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>
.business-crud-page {
&__field {
width: 100%;
}
.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-toolbar {
display: flex;
justify-content: flex-end;
margin-bottom: 16px;
}
&__task-form {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
column-gap: 24px;
row-gap: 16px;
width: 100%;
:deep(.el-form-item) {
min-width: 0;
margin-bottom: 0;
}
:deep(.el-form-item__content) {
min-width: 0;
}
:deep(.el-select),
:deep(.el-cascader),
:deep(.el-date-editor.el-input) {
width: 100%;
}
:deep(.el-radio-group) {
width: 100%;
}
:deep(.el-radio-button) {
flex: 1;
}
:deep(.el-radio-button__inner) {
width: 100%;
}
}
&__task-form--full {
display: block;
}
&__task-row {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
column-gap: 24px;
row-gap: 16px;
margin-bottom: 16px;
width: 100%;
&:last-child {
margin-bottom: 0;
}
:deep(.el-form-item) {
min-width: 0;
margin-bottom: 0;
}
:deep(.el-form-item__content) {
min-width: 0;
}
:deep(.el-select),
:deep(.el-cascader),
:deep(.el-date-editor.el-input) {
width: 100%;
}
}
&__task-span-4 {
grid-column: 1 / -1;
}
&__task-span-1 {
grid-column: span 1;
}
&__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;
}
}
:deep(.business-crud-page__goods-info-cell .cell) {
overflow: visible;
line-height: 20px;
white-space: normal;
word-break: break-word;
text-overflow: clip;
}
&__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;
}
&__cargo-wrap {
display: block;
width: 100%;
min-width: 0;
overflow-x: auto;
}
&__cargo-table {
width: 100%;
min-width: 1870px;
: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-end;
padding-top: 12px;
color: #303133;
font-weight: 600;
}
&__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%;
}
}
&__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: none;
}
:deep(.el-dialog__body) {
padding: 0;
}
}
&__dispatch-shell {
display: flex;
flex-direction: column;
min-height: calc(100vh - 140px);
}
&__dispatch-top {
padding: 22px 32px 14px;
border-bottom: 1px solid #e9edf5;
}
&__dispatch-heading {
margin-bottom: 20px;
}
&__dispatch-heading-title {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
}
&__dispatch-heading-name {
font-size: 22px;
font-weight: 600;
color: #303133;
}
&__dispatch-heading-split {
font-size: 20px;
color: #606266;
}
&__dispatch-heading-no {
font-size: 15px;
color: #303133;
}
&__dispatch-summary {
display: grid;
grid-template-columns: minmax(0, 1.15fr) minmax(520px, 1fr);
gap: 32px;
align-items: start;
}
&__dispatch-summary-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
column-gap: 48px;
row-gap: 18px;
}
&__dispatch-summary-item {
min-width: 0;
}
&__dispatch-summary-label {
margin-bottom: 8px;
font-size: 14px;
color: #666;
font-weight: 500;
}
&__dispatch-summary-value {
min-height: 24px;
font-size: 15px;
line-height: 1.45;
color: #333;
word-break: break-word;
}
&__dispatch-summary-item--attachments {
grid-column: 1 / -1;
}
&__dispatch-attachment-link {
display: inline-block;
margin-right: 22px;
margin-bottom: 6px;
}
&__dispatch-route {
display: grid;
grid-template-columns: minmax(0, 1fr) 72px minmax(0, 1fr);
gap: 24px;
align-items: center;
justify-items: center;
min-height: 200px;
}
&__dispatch-route-item {
display: flex;
align-items: flex-start;
gap: 16px;
max-width: 100%;
}
&__dispatch-route-badge {
display: inline-flex;
flex: 0 0 auto;
align-items: center;
justify-content: center;
width: 46px;
height: 46px;
border-radius: 6px;
color: #fff;
font-size: 16px;
font-weight: 600;
&.is-start {
background: #2a92ff;
}
&.is-end {
background: #ffb11a;
}
}
&__dispatch-route-body {
max-width: 340px;
text-align: center;
}
&__dispatch-route-title {
font-size: 22px;
line-height: 1.25;
color: #303133;
font-weight: 600;
}
&__dispatch-route-address {
margin-top: 12px;
font-size: 15px;
line-height: 1.45;
color: #333;
}
&__dispatch-route-contact {
margin-top: 10px;
font-size: 15px;
color: #333;
}
&__dispatch-route-line {
width: 72px;
height: 4px;
background: #3b3b3b;
border-radius: 999px;
}
&__dispatch-list {
padding: 12px 18px 18px;
}
&__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 {
font-size: 13px;
:deep(.el-table__header .cell) {
font-size: 13px;
font-weight: 600;
}
:deep(.cell) {
white-space: normal;
line-height: 1.35;
}
}
&__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: 12px;
padding-right: 0;
}
&__dispatch-item-dialog {
:deep(.el-dialog__body) {
padding-top: 20px;
}
}
&__dispatch-item-form {
width: 100%;
}
&__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-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));
}
}
&__dispatch-item-span-2 {
grid-column: span 2;
}
&__dispatch-item-span-3 {
grid-column: span 3;
}
&__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-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-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
&__attachment-table {
width: 100%;
min-width: 1000px;
: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-section {
margin-bottom: 20px;
: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;
}
:deep(.el-table) {
border-color: #eff1f7;
}
}
&__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;
}
}
}
</style>