2867 lines
92 KiB
Vue
2867 lines
92 KiB
Vue
<!-- eslint-disable vue/no-v-model-argument -->
|
|
<template>
|
|
<component
|
|
:is="isPageVariant ? 'div' : ElDialog"
|
|
v-bind="formShellBind"
|
|
@closed="handleClosed"
|
|
@opened="handleOpened"
|
|
@update:model-value="onFormShellVisible"
|
|
>
|
|
<h2 v-if="isPageVariant" class="mp-plan-form-title">{{ dialogTitle }}</h2>
|
|
<div class="pl-plan-form-body">
|
|
<section class="pl-plan-form-section">
|
|
<h3 class="pl-plan-form-section-title">基础信息</h3>
|
|
<el-form
|
|
ref="basicFormRef"
|
|
:model="formData"
|
|
:rules="basicRules"
|
|
:label-position="formLabelPosition"
|
|
label-width="100px"
|
|
class="pl-dialog-form"
|
|
>
|
|
<div class="form-grid-5">
|
|
<el-form-item label="创建日期" prop="createDate">
|
|
<el-date-picker
|
|
v-model="formData.createDate"
|
|
type="date"
|
|
placeholder="选择日期"
|
|
value-format="YYYY-MM-DD"
|
|
:disabled="!isEdit"
|
|
style="width: 100%"
|
|
/>
|
|
</el-form-item>
|
|
<el-form-item label="计划日期" prop="planDate">
|
|
<el-date-picker
|
|
v-model="formData.planDate"
|
|
type="date"
|
|
placeholder="选择日期"
|
|
value-format="YYYY-MM-DD"
|
|
clearable
|
|
style="width: 100%"
|
|
@change="handlePlanDateChange"
|
|
/>
|
|
</el-form-item>
|
|
<el-form-item label="归属公司" prop="belongsCompany">
|
|
<el-select v-model="formData.belongsCompany" placeholder="请选择归属公司">
|
|
<el-option label="安庆山峡" value="shanxia" />
|
|
<el-option label="安徽云栋" value="yundong" />
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="单据类型" prop="planType">
|
|
<el-select
|
|
v-model="formData.planType"
|
|
placeholder="请选择单据类型"
|
|
clearable
|
|
style="width: 100%"
|
|
>
|
|
<el-option label="直销单" value="direct" />
|
|
<el-option label="入库单" value="inbound" />
|
|
<el-option label="出库单" value="outbound" />
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="跟进人">
|
|
<el-input v-model="formData.followUserName" readonly />
|
|
</el-form-item>
|
|
</div>
|
|
</el-form>
|
|
</section>
|
|
|
|
<section class="pl-plan-form-section">
|
|
<h3 class="pl-plan-form-section-title">运输信息</h3>
|
|
<el-form
|
|
:model="formData"
|
|
:label-position="formLabelPosition"
|
|
label-width="100px"
|
|
class="pl-dialog-form"
|
|
>
|
|
<div class="pl-plan-transport-row pl-plan-transport-row--four">
|
|
<el-form-item label="车牌号" class="pl-plan-transport-row__vehicle">
|
|
<div class="pl-plan-vehicle-with-library">
|
|
<el-select
|
|
v-model="formData.vehicleId"
|
|
class="pl-plan-vehicle-with-library__select"
|
|
placeholder="请选择车牌号"
|
|
filterable
|
|
remote
|
|
clearable
|
|
:remote-method="searchVehicles"
|
|
:loading="vehicleSelectLoading || vehicleLoading"
|
|
@focus="onVehicleSelectFocus"
|
|
@change="handleVehicleChange"
|
|
>
|
|
<el-option
|
|
v-for="item in vehicleList"
|
|
:key="item.id"
|
|
:label="formatPlVehiclePlateOptionLabel(item.licensePlate, item.trailerNo)"
|
|
:value="item.id"
|
|
/>
|
|
</el-select>
|
|
<el-button
|
|
class="pl-plan-vehicle-with-library__btn btn-ghost"
|
|
@click="vehicleLibraryVisible = true"
|
|
>
|
|
<el-icon><FolderOpened /></el-icon>从车辆库选择
|
|
</el-button>
|
|
<el-button
|
|
class="pl-plan-vehicle-with-library__btn btn-ghost"
|
|
@click="pasteRecognizeVisible = true"
|
|
>
|
|
<el-icon><DocumentCopy /></el-icon>三方人车快速创建
|
|
</el-button>
|
|
</div>
|
|
</el-form-item>
|
|
<el-form-item label="选择司机" class="pl-plan-transport-row__driver">
|
|
<el-select
|
|
v-model="selectedDriverId"
|
|
placeholder="请选择司机(可选)"
|
|
filterable
|
|
remote
|
|
clearable
|
|
:remote-method="searchDrivers"
|
|
:loading="driverSelectLoading"
|
|
:disabled="!formData.vehicleId"
|
|
@focus="onDriverSelectFocus"
|
|
>
|
|
<el-option
|
|
v-for="d in driverList"
|
|
:key="d.id"
|
|
:label="formatDriverOptionLabel(d)"
|
|
:value="d.id"
|
|
/>
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="选择押运员" class="pl-plan-transport-row__escort">
|
|
<el-select
|
|
v-model="selectedEscortId"
|
|
placeholder="请选择押运员(可选)"
|
|
filterable
|
|
remote
|
|
clearable
|
|
:remote-method="searchEscorts"
|
|
:loading="escortSelectLoading"
|
|
:disabled="!formData.vehicleId"
|
|
@focus="onEscortSelectFocus"
|
|
>
|
|
<el-option
|
|
v-for="d in escortList"
|
|
:key="d.id"
|
|
:label="formatDriverOptionLabel(d)"
|
|
:value="d.id"
|
|
/>
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="运输公司" class="pl-plan-transport-row__carrier">
|
|
<div class="pl-plan-carrier-with-library">
|
|
<el-select
|
|
v-model="formData.carrierId"
|
|
class="pl-plan-carrier-with-library__select"
|
|
placeholder="请选择运输公司"
|
|
filterable
|
|
remote
|
|
clearable
|
|
:remote-method="searchCarriers"
|
|
:loading="carrierSelectLoading"
|
|
@focus="onCarrierSelectFocus"
|
|
>
|
|
<el-option
|
|
v-for="c in carrierList"
|
|
:key="c.id"
|
|
:label="c.carrierName"
|
|
:value="c.id"
|
|
/>
|
|
</el-select>
|
|
<el-button
|
|
class="pl-plan-carrier-with-library__btn btn-ghost"
|
|
@click="carrierLibraryVisible = true"
|
|
>
|
|
<el-icon><FolderOpened /></el-icon>从承运商库选择
|
|
</el-button>
|
|
</div>
|
|
</el-form-item>
|
|
</div>
|
|
|
|
<PlVehicleSummaryStrip
|
|
v-if="vehicleBundle"
|
|
:detail="vehicleBundle.detail"
|
|
:personnel-list="vehicleBundle.personnelList"
|
|
:carrier-name="summaryCarrierName"
|
|
@view="vehicleViewVisible = true"
|
|
/>
|
|
</el-form>
|
|
</section>
|
|
|
|
<section class="pl-plan-form-section">
|
|
<h3 class="pl-plan-form-section-title">装送货信息</h3>
|
|
<el-form :label-position="formLabelPosition" label-width="100px" class="pl-dialog-form">
|
|
<PlPlanStopsSplitShell
|
|
:plan-total-qty="planTotalQty"
|
|
:load-total-qty="loadTotalQty"
|
|
:delivery-total-qty="deliveryTotalQty"
|
|
:unit-label="planQtyUnitLabel"
|
|
:delivery-breakdown="deliveryQtyBreakdown"
|
|
>
|
|
<template #load>
|
|
<PlPlanLoadSection
|
|
ref="loadSectionRef"
|
|
v-model="loadForm"
|
|
compact-products
|
|
:exclude-plan-id="planId || ''"
|
|
:vehicle-plate="currentVehiclePlate"
|
|
:contract-product-rows="loadContractProductRows"
|
|
@open-customer-picker="openCustomerSelector('load')"
|
|
@add-products="openProductSelector('load')"
|
|
/>
|
|
</template>
|
|
<template #delivery-actions>
|
|
<el-button class="btn-main" type="primary" @click="handleAddDelivery">
|
|
<el-icon><Plus /></el-icon>添加送货段
|
|
</el-button>
|
|
<el-button
|
|
v-if="deliveryActiveIndex > 0"
|
|
class="btn-ghost"
|
|
@click="handleMoveDelivery(-1)"
|
|
>
|
|
<el-icon><Top /></el-icon>上移
|
|
</el-button>
|
|
<el-button
|
|
v-if="deliveryActiveIndex < deliveryForms.length - 1"
|
|
class="btn-ghost"
|
|
@click="handleMoveDelivery(1)"
|
|
>
|
|
<el-icon><Bottom /></el-icon>下移
|
|
</el-button>
|
|
<el-button
|
|
v-if="deliveryForms.length > 1"
|
|
class="btn-ghost"
|
|
type="danger"
|
|
@click="handleRemoveDelivery"
|
|
>
|
|
<el-icon><Delete /></el-icon>删除
|
|
</el-button>
|
|
</template>
|
|
<template #delivery>
|
|
<PlPlanDeliverySection
|
|
ref="deliverySectionRef"
|
|
v-model="deliveryForms"
|
|
tab-mode
|
|
compact-products
|
|
:qty-unit-label="planQtyUnitLabel"
|
|
:show-split-hint="showDeliverySplitHint"
|
|
:exclude-plan-id="planId || ''"
|
|
:load-products="loadForm.products"
|
|
:vehicle-plate="currentVehiclePlate"
|
|
:contract-product-rows-by-contract="contractProductRowsCache"
|
|
@active-index-change="deliveryActiveIndex = $event"
|
|
@open-customer-picker="openCustomerSelector('delivery', $event)"
|
|
@sync-products="syncDeliveryProductsFromLoadManual"
|
|
@select-products="openProductSelector('delivery', $event)"
|
|
/>
|
|
</template>
|
|
</PlPlanStopsSplitShell>
|
|
</el-form>
|
|
</section>
|
|
|
|
<section class="pl-plan-form-section">
|
|
<h3 class="pl-plan-form-section-title">备注</h3>
|
|
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入备注" />
|
|
</section>
|
|
</div>
|
|
|
|
<template v-if="!isPageVariant" #footer>
|
|
<div class="dialog-footer">
|
|
<el-button @click="handleCancel">
|
|
<el-icon><Close /></el-icon>取消
|
|
</el-button>
|
|
<el-button v-if="showDraftButton" :loading="submitLoading" @click="handleSubmit('draft')">
|
|
<el-icon><Document /></el-icon>暂存
|
|
</el-button>
|
|
<el-button
|
|
v-if="showSaveButton"
|
|
:type="showSaveAndArrangeButton ? 'default' : 'primary'"
|
|
:class="showSaveAndArrangeButton ? 'btn-ghost' : undefined"
|
|
:loading="submitLoading"
|
|
@click="handleSubmit('save')"
|
|
>
|
|
<el-icon><Check /></el-icon>{{ saveButtonLabel }}
|
|
</el-button>
|
|
<el-button
|
|
v-if="showSaveAndArrangeButton"
|
|
type="primary"
|
|
:loading="submitLoading"
|
|
@click="handleSubmit('saveAndArrange')"
|
|
>
|
|
<el-icon><Van /></el-icon>{{ saveAndArrangeButtonLabel }}
|
|
</el-button>
|
|
<el-button
|
|
v-if="showRearrangeButton"
|
|
type="primary"
|
|
:loading="submitLoading"
|
|
@click="handleSubmit('rearrange')"
|
|
>
|
|
<el-icon><Van /></el-icon>调整运输
|
|
</el-button>
|
|
</div>
|
|
</template>
|
|
<div v-else class="mp-plan-form-footer dialog-footer">
|
|
<el-button @click="handleCancel">
|
|
<el-icon><Close /></el-icon>取消
|
|
</el-button>
|
|
<el-button v-if="showDraftButton" :loading="submitLoading" @click="handleSubmit('draft')">
|
|
<el-icon><Document /></el-icon>暂存
|
|
</el-button>
|
|
<el-button
|
|
v-if="showSaveButton"
|
|
:type="showSaveAndArrangeButton ? 'default' : 'primary'"
|
|
:class="showSaveAndArrangeButton ? 'btn-ghost' : undefined"
|
|
:loading="submitLoading"
|
|
@click="handleSubmit('save')"
|
|
>
|
|
<el-icon><Check /></el-icon>{{ saveButtonLabel }}
|
|
</el-button>
|
|
<el-button
|
|
v-if="showSaveAndArrangeButton"
|
|
type="primary"
|
|
:loading="submitLoading"
|
|
@click="handleSubmit('saveAndArrange')"
|
|
>
|
|
<el-icon><Van /></el-icon>{{ saveAndArrangeButtonLabel }}
|
|
</el-button>
|
|
<el-button
|
|
v-if="showRearrangeButton"
|
|
type="primary"
|
|
:loading="submitLoading"
|
|
@click="handleSubmit('rearrange')"
|
|
>
|
|
<el-icon><Van /></el-icon>调整运输
|
|
</el-button>
|
|
</div>
|
|
|
|
<CustomerSelector
|
|
:visible="customerSelectorVisible"
|
|
@update:visible="customerSelectorVisible = $event"
|
|
@confirm="handleCustomerConfirm"
|
|
/>
|
|
<ProductSelector
|
|
:visible="productSelectorVisible"
|
|
:allowed-product-ids="productSelectorAllowedIds"
|
|
@update:visible="productSelectorVisible = $event"
|
|
@confirm="handleProductConfirm"
|
|
/>
|
|
<PlVehicleViewDialog
|
|
v-model:visible="vehicleViewVisible"
|
|
:vehicle-id="formData.vehicleId"
|
|
:bundle="vehicleBundle"
|
|
/>
|
|
<PlVehicleLibrarySelectDialog
|
|
v-model="vehicleLibraryVisible"
|
|
@select="handleVehicleLibrarySelect"
|
|
/>
|
|
<PlCarrierLibrarySelectDialog
|
|
v-model="carrierLibraryVisible"
|
|
@select="handleCarrierLibrarySelect"
|
|
/>
|
|
<PlPasteRecognizeDialog
|
|
v-model:visible="pasteRecognizeVisible"
|
|
mode="vehicle-bundle"
|
|
@done="handlePasteRecognizeDone"
|
|
/>
|
|
</component>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ElDialog } from 'element-plus'
|
|
import {
|
|
Bottom,
|
|
Check,
|
|
Close,
|
|
Delete,
|
|
Document,
|
|
DocumentCopy,
|
|
FolderOpened,
|
|
Plus,
|
|
Top,
|
|
Van
|
|
} from '@element-plus/icons-vue'
|
|
import {
|
|
planApi,
|
|
transportApi,
|
|
showPlanSaveWarnings,
|
|
type Plan,
|
|
type PlanContractLink,
|
|
type PlanProduct,
|
|
type PlanUpdatePayload,
|
|
type Carrier,
|
|
type PasteCommitResult,
|
|
type Personnel,
|
|
type Vehicle,
|
|
type VehiclePersonnelRow
|
|
} from '@/api/pl'
|
|
import {
|
|
usePlRemoteCarrierOptions,
|
|
usePlRemotePersonnelOptions,
|
|
usePlRemoteVehicleOptions
|
|
} from '@/composables/usePlRemoteOptions'
|
|
import { fetchCustomerOptions } from '@/api/crm/customer'
|
|
import { getContract } from '@/api/crm/contract/index'
|
|
import CustomerSelector from '@/components/CustomerSelector.vue'
|
|
import ProductSelector from '@/components/business/product-selector/index.vue'
|
|
import PlPlanLoadSection from '@/components/business/pl-plan-load-section/index.vue'
|
|
import PlPlanDeliverySection from '@/components/business/pl-plan-delivery-section/index.vue'
|
|
import PlPlanStopsSplitShell from '@/components/business/pl-plan-stops-split-shell/index.vue'
|
|
import PlVehicleSummaryStrip from '@/components/business/pl-vehicle-summary-strip/index.vue'
|
|
import PlVehicleViewDialog from '@/components/business/pl-vehicle-view-dialog/index.vue'
|
|
import PlVehicleLibrarySelectDialog from '@/components/business/pl-vehicle-library-select-dialog/index.vue'
|
|
import PlCarrierLibrarySelectDialog from '@/components/business/pl-carrier-library-select-dialog/index.vue'
|
|
import PlPasteRecognizeDialog from '@/components/business/pl-paste-recognize-dialog/index.vue'
|
|
import {
|
|
buildDeliveriesPayload,
|
|
buildLoadPayload,
|
|
createEmptyDeliveryForm,
|
|
createEmptyLoadForm,
|
|
createStopProductsFromPlanProducts,
|
|
mapDeliveriesFromApi,
|
|
mapLegacyPlanToStops,
|
|
mapLoadFromApi,
|
|
enrichStopProductsDisplayFromPlan,
|
|
mapLoadProductsToPlanFormRows,
|
|
mergePlanProductsFromLoadMirror,
|
|
resolveStopDibangDefaults,
|
|
sumStopProductsPlanQty,
|
|
syncDeliveriesProductsFromLoad,
|
|
type PlanDeliveryForm,
|
|
type PlanLoadForm,
|
|
type PlanStopProductFormRow
|
|
} from '@/utils/pl-plan-stop-form'
|
|
import { findFirstStopProductArrangeGap } from '@/utils/pl-plan-stop-product-arrange-required'
|
|
import {
|
|
enrichPlanProductLinesAlias,
|
|
type PlanFormProductRow
|
|
} from '@/composables/usePlContractToPlan'
|
|
import {
|
|
initLineBrandMetaFromProduct,
|
|
prepareLineAliasAndBrandForSubmit
|
|
} from '@/utils/crm-product-brand'
|
|
import { formatPlVehiclePlateOptionLabel } from '@/utils/pl-display'
|
|
import {
|
|
buildMissingHazardCertReadyMessage,
|
|
enrichPlanProductsHazardFromCrm
|
|
} from '@/composables/usePlToxicProductCertCheck'
|
|
import { usePlFreightCalc, resolveFreightUnitLabel } from '@/composables/usePlFreightCalc'
|
|
import {
|
|
buildArrangePayloadFromPlanForm,
|
|
confirmBeforePlanArrange,
|
|
loadArrangeHistoricalFlags,
|
|
planHasAnyDibangSelected,
|
|
submitPlanArrange
|
|
} from '@/composables/usePlPlanArrangeSubmit'
|
|
import {
|
|
loadVehicleViewBundle,
|
|
type VehicleViewBundle
|
|
} from '@/composables/usePlVehicleViewDetail'
|
|
import { useDictStore } from '@/store/modules/dict'
|
|
import { useUserStore } from '@/store/modules/user'
|
|
import { formatDate } from '@/utils/format'
|
|
import { pickBxData } from '@/utils/http/bx'
|
|
import { isPlInternalCustomerName } from '@/utils/pl-internal-customer'
|
|
import {
|
|
collectContractProductIds,
|
|
collectContractProductRows,
|
|
filterProductsByAllowedProductIds,
|
|
isPackageAllowedForContractProduct,
|
|
normalizePackageSpec,
|
|
packageSpecsForProduct,
|
|
productPackageKey,
|
|
type ContractProductRowLite
|
|
} from '@/utils/pl-plan-contract-product'
|
|
import { getPersonnelAffiliationLabel } from '@/utils/pl-personnel-cert'
|
|
|
|
const props = defineProps<{
|
|
visible: boolean
|
|
planId?: string
|
|
/** 有值且无 planId 时:新增模式 + 从该计划预填(复制) */
|
|
copyFromPlanId?: string
|
|
/** 从列表/详情「安排运输」「调整运输」进入时展开安排区块 */
|
|
openForArrange?: boolean
|
|
/** 归档等场景:仅保存计划,不展示暂存/安排运输(已安排计划也显示「保存」) */
|
|
planEditOnly?: boolean
|
|
/** 小程序 WebView 全屏页:不使用桌面弹窗遮罩 */
|
|
variant?: 'dialog' | 'page'
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
'update:visible': [value: boolean]
|
|
success: [payload?: { planId: string; arranged?: boolean }]
|
|
close: []
|
|
}>()
|
|
|
|
const userStore = useUserStore()
|
|
const dictStore = useDictStore()
|
|
|
|
type CustomerSide = 'load' | 'delivery'
|
|
|
|
interface CustomerConfirmRow {
|
|
id: string | number
|
|
customerName?: string
|
|
contactName?: string
|
|
mobile?: string
|
|
}
|
|
|
|
/** 当前登录用户 ID(后端 current-user 返回 id,兼容历史 userId 字段) */
|
|
function resolveCurrentUserId() {
|
|
const info = userStore.info || ({} as Api.Auth.UserInfo)
|
|
const fromInfo = String(info.id || info.userId || '')
|
|
if (fromInfo) return fromInfo
|
|
const token = userStore.accessToken
|
|
if (!token || token.split('.').length < 2) return ''
|
|
try {
|
|
const payload = token.split('.')[1]
|
|
const b64 = payload.replace(/-/g, '+').replace(/_/g, '/')
|
|
const pad = '='.repeat((4 - (b64.length % 4)) % 4)
|
|
const obj = JSON.parse(atob(b64 + pad))
|
|
return String(obj?.sub || '')
|
|
} catch {
|
|
return ''
|
|
}
|
|
}
|
|
|
|
function resolveCurrentUserName() {
|
|
const info = userStore.info || ({} as Api.Auth.UserInfo & Record<string, string>)
|
|
return info.realName || info.userName || info.username || info.nickName || ''
|
|
}
|
|
|
|
function todayYmd(): string {
|
|
const d = new Date()
|
|
const y = d.getFullYear()
|
|
const m = String(d.getMonth() + 1).padStart(2, '0')
|
|
const day = String(d.getDate()).padStart(2, '0')
|
|
return `${y}-${m}-${day}`
|
|
}
|
|
|
|
const isEdit = computed(() => !!props.planId)
|
|
const isCopy = computed(() => !props.planId && !!props.copyFromPlanId)
|
|
const isPageVariant = computed(() => props.variant === 'page')
|
|
const formLabelPosition = computed(() => (isPageVariant.value ? 'top' : 'right'))
|
|
const dialogTitle = computed(() => (isEdit.value ? '编辑运输计划' : '新增运输计划'))
|
|
|
|
const innerVisible = computed({
|
|
get: () => props.visible,
|
|
set: (value) => emit('update:visible', value)
|
|
})
|
|
|
|
function onFormShellVisible(value: boolean) {
|
|
innerVisible.value = value
|
|
}
|
|
|
|
const formShellBind = computed(() => {
|
|
if (isPageVariant.value) {
|
|
return { class: 'mp-plan-form-page pl-plan-form-dialog' }
|
|
}
|
|
return {
|
|
modelValue: innerVisible.value,
|
|
title: dialogTitle.value,
|
|
width: '100%',
|
|
alignCenter: true,
|
|
destroyOnClose: true,
|
|
appendToBody: true,
|
|
class: 'dialog-form-layout pl-plan-form-dialog',
|
|
modalClass: 'order-module-dialog-mask',
|
|
closeOnClickModal: false,
|
|
style: 'backdrop-filter: none'
|
|
}
|
|
})
|
|
|
|
const submitLoading = ref(false)
|
|
const detailLoading = ref(false)
|
|
/** 加载详情 / 重置时禁止计划产品 → 段产品自动对齐 */
|
|
const suppressStopProductSync = ref(false)
|
|
const loadedTransportStatus = ref('pending')
|
|
const loadedFollowStatus = ref('')
|
|
const loadForm = ref<PlanLoadForm>(createEmptyLoadForm())
|
|
const deliveryForms = ref<PlanDeliveryForm[]>([createEmptyDeliveryForm(1)])
|
|
const loadedPlanSnapshot = ref<Plan | null>(null)
|
|
const selectedDriverId = ref('')
|
|
const selectedEscortId = ref('')
|
|
|
|
/** 已安排:禁止仅「保存」,须走保存并安排/调整运输,避免计划与地磅等联动字段脱节 */
|
|
const showSaveButton = computed(() => {
|
|
if (props.planEditOnly && isEdit.value) return true
|
|
return loadedTransportStatus.value !== 'arranged'
|
|
})
|
|
const showDraftButton = computed(() => {
|
|
if (props.planEditOnly) return false
|
|
if (!isEdit.value) return true
|
|
return loadedFollowStatus.value === 'drafting' || loadedFollowStatus.value === 'pending_arrange'
|
|
})
|
|
const showSaveAndArrangeButton = computed(() => {
|
|
if (props.planEditOnly) return false
|
|
if (!isEdit.value) return true
|
|
if (props.openForArrange && loadedTransportStatus.value === 'arranged') return false
|
|
return loadedTransportStatus.value === 'pending' || loadedTransportStatus.value === 'arranged'
|
|
})
|
|
const isRearrangeMode = computed(
|
|
() => loadedTransportStatus.value === 'arranged' && Boolean(props.openForArrange)
|
|
)
|
|
const showRearrangeButton = computed(() => !props.planEditOnly && isRearrangeMode.value)
|
|
const saveButtonLabel = computed(() => (isEdit.value ? '保存' : '创建'))
|
|
const saveAndArrangeButtonLabel = computed(() => {
|
|
if (!isEdit.value) return '创建并安排运输'
|
|
return '保存并安排'
|
|
})
|
|
|
|
const customerSelectorVisible = ref(false)
|
|
const customerSelectorSide = ref<CustomerSide>('load')
|
|
const customerSelectorDeliveryIndex = ref(0)
|
|
const loadSectionRef = ref<InstanceType<typeof PlPlanLoadSection>>()
|
|
const deliverySectionRef = ref<InstanceType<typeof PlPlanDeliverySection>>()
|
|
const deliveryActiveIndex = ref(0)
|
|
const stopFormsReady = ref(false)
|
|
interface CustomerOptionRow {
|
|
id: string
|
|
customerName: string
|
|
customerNo?: string
|
|
}
|
|
const customerOptions = ref<CustomerOptionRow[]>([])
|
|
const customerOptionsLoading = ref(false)
|
|
const productSelectorVisible = ref(false)
|
|
/** 产品选择器目标:装货 或 某一送货段 */
|
|
const productSelectorTarget = ref<{ side: 'load' | 'delivery'; deliveryIndex: number }>({
|
|
side: 'load',
|
|
deliveryIndex: 0
|
|
})
|
|
/** 当前装货合同明细中的 CRM productId(非内部客户限制选品) */
|
|
const loadContractProductIds = ref<string[]>([])
|
|
/** 装货合同明细拉取序号,避免快速换合同时旧响应覆盖新结果 */
|
|
let loadContractFetchSeq = 0
|
|
/** 合同 id → 明细 productId(装/送货共用缓存) */
|
|
const contractProductIdsCache = ref<Record<string, string[]>>({})
|
|
/** 合同 id → 明细行摘要(含包装,供段产品表下拉与保存校验) */
|
|
const contractProductRowsCache = ref<Record<string, ContractProductRowLite[]>>({})
|
|
/** 送货段合同明细拉取序号(按段) */
|
|
const deliveryContractFetchSeqByIndex: Record<number, number> = {}
|
|
/**
|
|
* 内部客户:undefined → ProductSelector 全量目录
|
|
* 非内部:始终传 string[](合同无明细时为 [])→ 受限模式,勿与「未传」混淆
|
|
*/
|
|
const productSelectorAllowedIds = computed((): string[] | undefined => {
|
|
if (productSelectorTarget.value.side === 'delivery') {
|
|
const delivery = deliveryForms.value[productSelectorTarget.value.deliveryIndex]
|
|
if (!delivery) return []
|
|
if (isPlInternalCustomerName(delivery.customerName)) return undefined
|
|
const cid = String(delivery.contracts?.[0]?.contractId || '').trim()
|
|
return cid ? contractProductIdsCache.value[cid] || [] : []
|
|
}
|
|
if (isPlInternalCustomerName(loadForm.value.customerName)) return undefined
|
|
return loadContractProductIds.value
|
|
})
|
|
|
|
const loadContractProductRows = computed((): ContractProductRowLite[] => {
|
|
const cid = String(loadForm.value.contracts?.[0]?.contractId || '').trim()
|
|
return cid ? contractProductRowsCache.value[cid] || [] : []
|
|
})
|
|
|
|
function openProductSelector(side: 'load' | 'delivery', deliveryIndex = 0) {
|
|
productSelectorTarget.value = { side, deliveryIndex }
|
|
productSelectorVisible.value = true
|
|
}
|
|
|
|
async function fetchContractProductIds(contractId: string): Promise<string[]> {
|
|
const id = String(contractId || '').trim()
|
|
if (!id) return []
|
|
if (Object.prototype.hasOwnProperty.call(contractProductIdsCache.value, id)) {
|
|
return contractProductIdsCache.value[id] || []
|
|
}
|
|
try {
|
|
const res = await getContract(id)
|
|
const detail = pickBxData<{ products?: ContractProductRowLite[] }>(res)
|
|
const productRows = collectContractProductRows(detail?.products || [])
|
|
const allowedIds = collectContractProductIds(detail?.products || [])
|
|
contractProductIdsCache.value = { ...contractProductIdsCache.value, [id]: allowedIds }
|
|
contractProductRowsCache.value = { ...contractProductRowsCache.value, [id]: productRows }
|
|
return allowedIds
|
|
} catch {
|
|
contractProductIdsCache.value = { ...contractProductIdsCache.value, [id]: [] }
|
|
contractProductRowsCache.value = { ...contractProductRowsCache.value, [id]: [] }
|
|
return []
|
|
}
|
|
}
|
|
|
|
function filterDeliveryProductsByContract(delivery: PlanDeliveryForm): PlanDeliveryForm {
|
|
if (isPlInternalCustomerName(delivery.customerName)) return delivery
|
|
const cid = String(delivery.contracts?.[0]?.contractId || '').trim()
|
|
if (!cid) return delivery
|
|
const allowed = contractProductIdsCache.value[cid]
|
|
// 明细尚未拉到时先不刷,避免误清空;拉完后 watch 会再滤
|
|
if (allowed === undefined) return delivery
|
|
return {
|
|
...delivery,
|
|
products: filterProductsByAllowedProductIds(delivery.products || [], allowed)
|
|
}
|
|
}
|
|
|
|
const vehicleViewVisible = ref(false)
|
|
const vehicleLibraryVisible = ref(false)
|
|
const carrierLibraryVisible = ref(false)
|
|
const pasteRecognizeVisible = ref(false)
|
|
const vehicleLoading = ref(false)
|
|
const basicFormRef = ref()
|
|
const {
|
|
list: vehicleList,
|
|
loading: vehicleSelectLoading,
|
|
search: searchVehicles,
|
|
onFocus: onVehicleSelectFocus,
|
|
ensureById: ensureSelectedVehicleOption
|
|
} = usePlRemoteVehicleOptions({
|
|
getSelectedId: () => formData.vehicleId,
|
|
listParams: { vehicleStatus: 'available' }
|
|
})
|
|
const {
|
|
list: carrierList,
|
|
loading: carrierSelectLoading,
|
|
search: searchCarriers,
|
|
onFocus: onCarrierSelectFocus,
|
|
ensureById: ensureSelectedCarrierOption
|
|
} = usePlRemoteCarrierOptions({
|
|
getSelectedId: () => formData.carrierId,
|
|
listParams: { status: '1' }
|
|
})
|
|
const {
|
|
list: driverList,
|
|
loading: driverSelectLoading,
|
|
search: searchDrivers,
|
|
onFocus: onDriverSelectFocus,
|
|
ensureById: ensureSelectedDriverOption
|
|
} = usePlRemotePersonnelOptions({
|
|
getSelectedId: () => selectedDriverId.value,
|
|
listParams: { status: '1', personnelType: 'driver' }
|
|
})
|
|
const {
|
|
list: escortList,
|
|
loading: escortSelectLoading,
|
|
search: searchEscorts,
|
|
onFocus: onEscortSelectFocus,
|
|
ensureById: ensureSelectedEscortOption
|
|
} = usePlRemotePersonnelOptions({
|
|
getSelectedId: () => selectedEscortId.value,
|
|
listParams: { status: '1', personnelTypeIn: 'escort,both' }
|
|
})
|
|
const vehicleBundle = ref<VehicleViewBundle | null>(null)
|
|
|
|
const formData = reactive({
|
|
createDate: todayYmd(),
|
|
planDate: '',
|
|
belongsCompany: 'shanxia',
|
|
planType: '',
|
|
followUserId: '',
|
|
followUserName: '',
|
|
supplierId: '',
|
|
supplierName: '',
|
|
supplierContracts: [] as PlanContractLink[],
|
|
destinationId: '',
|
|
destinationName: '',
|
|
customerId: '',
|
|
customerName: '',
|
|
demanderContracts: [] as PlanContractLink[],
|
|
contactName: '',
|
|
contactPhone: '',
|
|
hasFreight: 0,
|
|
pickupAddress: '',
|
|
pickupLng: null as number | null,
|
|
pickupLat: null as number | null,
|
|
pickupTime: '',
|
|
pickupContact: '',
|
|
pickupNotes: '',
|
|
destinationAddress: '',
|
|
destinationLng: null as number | null,
|
|
destinationLat: null as number | null,
|
|
deliveryTime: '',
|
|
deliveryContact: '',
|
|
destinationNotes: '',
|
|
vehicleId: '',
|
|
carrierId: '',
|
|
distanceKm: null as number | null,
|
|
freightType: 'by_weight',
|
|
remark: '',
|
|
products: [] as PlanFormProductRow[]
|
|
})
|
|
|
|
const basicRules = {
|
|
createDate: [{ required: true, message: '请选择创建日期', trigger: 'change' }],
|
|
belongsCompany: [{ required: true, message: '请选择归属公司', trigger: 'change' }]
|
|
}
|
|
|
|
const productsRef = computed(() => formData.products)
|
|
|
|
const planTotalQty = computed(() => sumStopProductsPlanQty(loadForm.value.products || []))
|
|
const loadTotalQty = computed(() => sumStopProductsPlanQty(loadForm.value.products || []))
|
|
const deliveryTotalQty = computed(() =>
|
|
deliveryForms.value.reduce((sum, row) => sum + sumStopProductsPlanQty(row.products || []), 0)
|
|
)
|
|
const planQtyUnitLabel = computed(() => resolveFreightUnitLabel(formData.products))
|
|
const showDeliverySplitHint = computed(() => deliveryForms.value.length >= 2)
|
|
const deliveryQtyBreakdown = computed(() => {
|
|
const parts = deliveryForms.value
|
|
.map((row) => parseFloat(sumStopProductsPlanQty(row.products || []).toFixed(3)))
|
|
.filter((qty) => qty > 0)
|
|
if (parts.length < 2) return ''
|
|
const total = parseFloat(parts.reduce((a, b) => a + b, 0).toFixed(3))
|
|
return `${parts.join(' + ')} = ${total}`
|
|
})
|
|
|
|
function mirrorPlanProductsFromLoad() {
|
|
formData.products = mergePlanProductsFromLoadMirror(
|
|
mapLoadProductsToPlanFormRows(loadForm.value.products || []),
|
|
formData.products
|
|
)
|
|
}
|
|
|
|
async function ensureDeliveryContractProductCaches() {
|
|
const tasks = deliveryForms.value.map(async (row) => {
|
|
if (isPlInternalCustomerName(row.customerName)) return
|
|
const cid = String(row.contracts?.[0]?.contractId || '').trim()
|
|
if (cid) await fetchContractProductIds(cid)
|
|
})
|
|
await Promise.all(tasks)
|
|
}
|
|
|
|
async function applyLoadProductsToDeliveries(options?: {
|
|
showHint?: boolean
|
|
forceQtyFromLoad?: boolean
|
|
}) {
|
|
await ensureDeliveryContractProductCaches()
|
|
const result = syncDeliveriesProductsFromLoad(
|
|
deliveryForms.value,
|
|
loadForm.value.products || [],
|
|
{ forceQtyFromLoad: options?.forceQtyFromLoad }
|
|
)
|
|
deliveryForms.value = result.deliveries.map((row) => filterDeliveryProductsByContract(row))
|
|
if (options?.showHint && result.needSplitHint) {
|
|
ElMessage.info('多个送货段,请拆分各段数量')
|
|
}
|
|
}
|
|
|
|
/** 装货产品变更后:镜像计划层产品 + 同步送货段(C1) */
|
|
async function syncFromLoadProducts(options?: { showHint?: boolean }) {
|
|
if (suppressStopProductSync.value || detailLoading.value) return
|
|
mirrorPlanProductsFromLoad()
|
|
await applyLoadProductsToDeliveries(options)
|
|
}
|
|
|
|
function handleAddDelivery() {
|
|
deliverySectionRef.value?.addDelivery()
|
|
nextTick(() => {
|
|
void applyLoadProductsToDeliveries({ showHint: true })
|
|
})
|
|
}
|
|
|
|
function handleMoveDelivery(delta: number) {
|
|
deliverySectionRef.value?.moveDelivery(deliveryActiveIndex.value, delta)
|
|
}
|
|
|
|
function handleRemoveDelivery() {
|
|
deliverySectionRef.value?.removeDelivery(deliveryActiveIndex.value)
|
|
nextTick(() => {
|
|
void applyLoadProductsToDeliveries()
|
|
})
|
|
}
|
|
const hasFreightRef = computed(() => formData.hasFreight)
|
|
const freightTypeRef = computed(() => formData.freightType)
|
|
const freightCalc = usePlFreightCalc({
|
|
products: productsRef,
|
|
hasFreight: hasFreightRef,
|
|
freightType: freightTypeRef
|
|
})
|
|
|
|
const selectedVehicle = computed(() =>
|
|
vehicleList.value.find((item) => item.id === formData.vehicleId)
|
|
)
|
|
|
|
const currentVehiclePlate = computed(() => {
|
|
const fromList = (selectedVehicle.value?.licensePlate || '').trim()
|
|
if (fromList) return fromList
|
|
return (vehicleBundle.value?.detail?.licensePlate || '').trim()
|
|
})
|
|
|
|
/** 按当前车牌重算各段地磅默认勾选(换车时调用;编辑回填勿调用以免覆盖已存值) */
|
|
function reapplyStopDibangByVehiclePlate(plate?: string | null) {
|
|
const vehiclePlate = (plate ?? currentVehiclePlate.value) || ''
|
|
const loadDefaults = resolveStopDibangDefaults(
|
|
loadForm.value.customerName,
|
|
'load',
|
|
vehiclePlate
|
|
)
|
|
loadForm.value = {
|
|
...loadForm.value,
|
|
generateDibang: loadDefaults.generateDibang,
|
|
dibangDirection: loadDefaults.dibangDirection
|
|
}
|
|
deliveryForms.value = deliveryForms.value.map((row) => {
|
|
const defaults = resolveStopDibangDefaults(row.customerName, 'delivery', vehiclePlate)
|
|
return {
|
|
...row,
|
|
generateDibang: defaults.generateDibang,
|
|
dibangDirection: defaults.dibangDirection
|
|
}
|
|
})
|
|
}
|
|
|
|
const summaryCarrierName = computed(() => {
|
|
const carrier = carrierList.value.find((item) => item.id === formData.carrierId)
|
|
if (carrier?.carrierName) return carrier.carrierName
|
|
return vehicleBundle.value?.detail?.carrierName || ''
|
|
})
|
|
|
|
function mapPlanContractsFromApi(links?: PlanContractLink[]): PlanContractLink[] {
|
|
return (links || [])
|
|
.map((item) => ({
|
|
contractId: String(item.contractId || ''),
|
|
contractNo: item.contractNo || undefined
|
|
}))
|
|
.filter((item) => item.contractId)
|
|
}
|
|
|
|
function extractCustomerOptions(res: unknown): CustomerOptionRow[] {
|
|
const unwrapped = (res as { data?: unknown })?.data ?? res
|
|
if (Array.isArray(unwrapped)) return unwrapped as CustomerOptionRow[]
|
|
const payload = (unwrapped as { list?: CustomerOptionRow[]; rows?: CustomerOptionRow[] }) || {}
|
|
return payload.list || payload.rows || []
|
|
}
|
|
|
|
function ensureCustomerOption(customer?: Partial<CustomerOptionRow> | null) {
|
|
if (!customer?.id) return
|
|
const id = String(customer.id)
|
|
const exists = customerOptions.value.some((item) => String(item.id) === id)
|
|
if (exists) {
|
|
customerOptions.value = customerOptions.value.map((item) =>
|
|
String(item.id) === id
|
|
? {
|
|
...item,
|
|
customerName: customer.customerName || item.customerName,
|
|
customerNo: customer.customerNo || item.customerNo
|
|
}
|
|
: item
|
|
)
|
|
return
|
|
}
|
|
customerOptions.value = [
|
|
{
|
|
id,
|
|
customerName: customer.customerName || '',
|
|
customerNo: customer.customerNo
|
|
},
|
|
...customerOptions.value
|
|
]
|
|
}
|
|
|
|
function syncSelectedCustomerOptions() {
|
|
if (formData.supplierId) {
|
|
ensureCustomerOption({
|
|
id: formData.supplierId,
|
|
customerName: formData.supplierName
|
|
})
|
|
}
|
|
if (formData.customerId) {
|
|
ensureCustomerOption({
|
|
id: formData.customerId,
|
|
customerName: formData.customerName
|
|
})
|
|
}
|
|
}
|
|
|
|
async function loadDefaultCustomerOptions() {
|
|
customerOptionsLoading.value = true
|
|
try {
|
|
const res = await fetchCustomerOptions({ limit: 20 })
|
|
customerOptions.value = extractCustomerOptions(res)
|
|
syncSelectedCustomerOptions()
|
|
} catch (error) {
|
|
console.error('加载客户选项失败:', error)
|
|
customerOptions.value = []
|
|
syncSelectedCustomerOptions()
|
|
} finally {
|
|
customerOptionsLoading.value = false
|
|
}
|
|
}
|
|
|
|
async function searchCustomerOptions(query: string) {
|
|
const keyword = query?.trim()
|
|
if (!keyword) {
|
|
await loadDefaultCustomerOptions()
|
|
return
|
|
}
|
|
customerOptionsLoading.value = true
|
|
try {
|
|
const res = await fetchCustomerOptions({
|
|
keyword,
|
|
limit: 50
|
|
})
|
|
customerOptions.value = extractCustomerOptions(res)
|
|
syncSelectedCustomerOptions()
|
|
} catch (error) {
|
|
console.error('搜索客户失败:', error)
|
|
customerOptions.value = []
|
|
syncSelectedCustomerOptions()
|
|
} finally {
|
|
customerOptionsLoading.value = false
|
|
}
|
|
}
|
|
|
|
function handleCustomerSelectFocus() {
|
|
if (customerOptions.value.length === 0) {
|
|
loadDefaultCustomerOptions()
|
|
}
|
|
}
|
|
|
|
const MSG_SAME_CUSTOMER = '装货客户与送货客户不能为同一客户'
|
|
const MSG_SAME_ADDRESS = '取货地址与送货地址不能相同'
|
|
|
|
/** 已确认的客户 id,用于冲突时回滚 el-select */
|
|
const committedSupplierId = ref('')
|
|
const committedCustomerId = ref('')
|
|
/** 加载/重置表单时跳过地址互斥回滚 */
|
|
const suppressAddressMutex = ref(false)
|
|
|
|
function isSamePartyCustomer(supplierId?: string, customerId?: string) {
|
|
const s = String(supplierId || '').trim()
|
|
const c = String(customerId || '').trim()
|
|
return Boolean(s && c && s === c)
|
|
}
|
|
|
|
function normalizeAddressText(value?: string | null) {
|
|
return String(value || '').trim()
|
|
}
|
|
|
|
function isSameCoordPair(
|
|
lng1: number | null | undefined,
|
|
lat1: number | null | undefined,
|
|
lng2: number | null | undefined,
|
|
lat2: number | null | undefined
|
|
) {
|
|
if (lng1 == null || lat1 == null || lng2 == null || lat2 == null) return false
|
|
const aLng = Number(lng1)
|
|
const aLat = Number(lat1)
|
|
const bLng = Number(lng2)
|
|
const bLat = Number(lat2)
|
|
if ([aLng, aLat, bLng, bLat].some((n) => Number.isNaN(n))) return false
|
|
return aLng === bLng && aLat === bLat
|
|
}
|
|
|
|
function isSameLogisticsAddress(
|
|
addressA?: string | null,
|
|
lngA?: number | null,
|
|
latA?: number | null,
|
|
addressB?: string | null,
|
|
lngB?: number | null,
|
|
latB?: number | null
|
|
) {
|
|
const ta = normalizeAddressText(addressA)
|
|
const tb = normalizeAddressText(addressB)
|
|
if (ta && tb && ta === tb) return true
|
|
if (isSameCoordPair(lngA, latA, lngB, latB)) return true
|
|
return false
|
|
}
|
|
|
|
function syncCommittedPartyIds() {
|
|
committedSupplierId.value = String(formData.supplierId || '')
|
|
committedCustomerId.value = String(formData.customerId || '')
|
|
}
|
|
|
|
/** 供需方互换:仅交换本区字段,勿走 select change(会清空合同) */
|
|
function handleSwapParties() {
|
|
const temp = {
|
|
supplierId: formData.supplierId,
|
|
supplierName: formData.supplierName,
|
|
supplierContracts: [...formData.supplierContracts],
|
|
customerId: formData.customerId,
|
|
customerName: formData.customerName,
|
|
demanderContracts: [...formData.demanderContracts]
|
|
}
|
|
|
|
formData.supplierId = temp.customerId
|
|
formData.supplierName = temp.customerName
|
|
formData.supplierContracts = temp.demanderContracts
|
|
|
|
formData.customerId = temp.supplierId
|
|
formData.customerName = temp.supplierName
|
|
formData.demanderContracts = temp.supplierContracts
|
|
formData.destinationId = temp.supplierId
|
|
formData.destinationName = temp.supplierName
|
|
// 联系人仅绑定需方;互换后无供方联系人可对调,清空以免挂在错误客户上
|
|
formData.contactName = ''
|
|
formData.contactPhone = ''
|
|
|
|
syncCommittedPartyIds()
|
|
ElMessage.success('供需方已互换')
|
|
}
|
|
|
|
/** 取货信息 ↔ 送货信息 */
|
|
function handleSwapPickupDelivery() {
|
|
suppressAddressMutex.value = true
|
|
const temp = {
|
|
pickupAddress: formData.pickupAddress,
|
|
pickupLng: formData.pickupLng,
|
|
pickupLat: formData.pickupLat,
|
|
pickupTime: formData.pickupTime,
|
|
pickupContact: formData.pickupContact,
|
|
pickupNotes: formData.pickupNotes
|
|
}
|
|
|
|
formData.pickupAddress = formData.destinationAddress
|
|
formData.pickupLng = formData.destinationLng
|
|
formData.pickupLat = formData.destinationLat
|
|
formData.pickupTime = formData.deliveryTime
|
|
formData.pickupContact = formData.deliveryContact
|
|
formData.pickupNotes = formData.destinationNotes
|
|
|
|
formData.destinationAddress = temp.pickupAddress
|
|
formData.destinationLng = temp.pickupLng
|
|
formData.destinationLat = temp.pickupLat
|
|
formData.deliveryTime = temp.pickupTime
|
|
formData.deliveryContact = temp.pickupContact
|
|
formData.destinationNotes = temp.pickupNotes
|
|
|
|
nextTick(() => {
|
|
suppressAddressMutex.value = false
|
|
})
|
|
ElMessage.success('取送货信息已互换')
|
|
}
|
|
|
|
function handleSupplierSelectChange(customerId?: string) {
|
|
if (!customerId) {
|
|
formData.supplierId = ''
|
|
formData.supplierName = ''
|
|
formData.supplierContracts = []
|
|
committedSupplierId.value = ''
|
|
return
|
|
}
|
|
const nextId = String(customerId)
|
|
if (isSamePartyCustomer(nextId, formData.customerId)) {
|
|
ElMessage.warning(MSG_SAME_CUSTOMER)
|
|
nextTick(() => {
|
|
formData.supplierId = committedSupplierId.value
|
|
})
|
|
return
|
|
}
|
|
const customer = customerOptions.value.find((item) => String(item.id) === nextId)
|
|
formData.supplierId = nextId
|
|
formData.supplierName = customer?.customerName || formData.supplierName || ''
|
|
formData.supplierContracts = []
|
|
committedSupplierId.value = nextId
|
|
}
|
|
|
|
function handleDemanderSelectChange(customerId?: string) {
|
|
if (!customerId) {
|
|
formData.customerId = ''
|
|
formData.customerName = ''
|
|
formData.destinationId = ''
|
|
formData.destinationName = ''
|
|
formData.demanderContracts = []
|
|
formData.contactName = ''
|
|
formData.contactPhone = ''
|
|
committedCustomerId.value = ''
|
|
return
|
|
}
|
|
const nextId = String(customerId)
|
|
if (isSamePartyCustomer(formData.supplierId, nextId)) {
|
|
ElMessage.warning(MSG_SAME_CUSTOMER)
|
|
nextTick(() => {
|
|
formData.customerId = committedCustomerId.value
|
|
})
|
|
return
|
|
}
|
|
const customer = customerOptions.value.find((item) => String(item.id) === nextId)
|
|
const name = customer?.customerName || formData.customerName || ''
|
|
formData.customerId = nextId
|
|
formData.customerName = name
|
|
formData.destinationId = nextId
|
|
formData.destinationName = name
|
|
formData.demanderContracts = []
|
|
committedCustomerId.value = nextId
|
|
}
|
|
|
|
function resetStopForms() {
|
|
stopFormsReady.value = false
|
|
loadForm.value = createEmptyLoadForm()
|
|
deliveryForms.value = [createEmptyDeliveryForm(1)]
|
|
deliveryActiveIndex.value = 0
|
|
stopFormsReady.value = true
|
|
}
|
|
|
|
function applyStopsFromPlan(data: Plan) {
|
|
stopFormsReady.value = false
|
|
const plate = data.licensePlate || ''
|
|
if (data.load || data.deliveries?.length) {
|
|
loadForm.value = mapLoadFromApi(data.load, plate)
|
|
deliveryForms.value = mapDeliveriesFromApi(data.deliveries, plate)
|
|
} else {
|
|
const legacy = mapLegacyPlanToStops(data)
|
|
loadForm.value = legacy.load
|
|
deliveryForms.value = legacy.deliveries
|
|
}
|
|
stopFormsReady.value = true
|
|
}
|
|
|
|
function enrichStopFormsDisplayFromPlanProducts() {
|
|
loadForm.value = {
|
|
...loadForm.value,
|
|
products: enrichStopProductsDisplayFromPlan(loadForm.value.products, formData.products)
|
|
}
|
|
deliveryForms.value = deliveryForms.value.map((row) => ({
|
|
...row,
|
|
products: enrichStopProductsDisplayFromPlan(row.products, formData.products)
|
|
}))
|
|
}
|
|
|
|
/** 详情加载后:无装货产品时用计划产品回填;再按 C1 同步送货 */
|
|
async function bootstrapStopsFromPlanProducts() {
|
|
if (!loadForm.value.products.length && formData.products.length) {
|
|
loadForm.value = {
|
|
...loadForm.value,
|
|
products: createStopProductsFromPlanProducts(formData.products)
|
|
}
|
|
} else {
|
|
enrichStopFormsDisplayFromPlanProducts()
|
|
}
|
|
mirrorPlanProductsFromLoad()
|
|
await applyLoadProductsToDeliveries()
|
|
}
|
|
|
|
async function syncDeliveryProductsFromLoadManual() {
|
|
await applyLoadProductsToDeliveries({ forceQtyFromLoad: deliveryForms.value.length < 2 })
|
|
ElMessage.success('已从装货产品同步')
|
|
}
|
|
|
|
watch(
|
|
() => loadForm.value.products,
|
|
() => {
|
|
syncFromLoadProducts()
|
|
},
|
|
{ deep: true }
|
|
)
|
|
|
|
watch(
|
|
() => loadForm.value.contracts,
|
|
async (contracts, prev) => {
|
|
const id = String(contracts?.[0]?.contractId || '').trim()
|
|
const prevId = String(prev?.[0]?.contractId || '').trim()
|
|
if (!id) {
|
|
loadContractFetchSeq += 1
|
|
loadContractProductIds.value = []
|
|
return
|
|
}
|
|
const seq = ++loadContractFetchSeq
|
|
const allowedIds = await fetchContractProductIds(id)
|
|
if (seq !== loadContractFetchSeq) return
|
|
loadContractProductIds.value = allowedIds
|
|
// 合同 id 变化时过滤装货产品(详情首次灌入同 id / 加载中不刷)
|
|
if (prev && id !== prevId && !detailLoading.value && !suppressStopProductSync.value) {
|
|
const nextProducts = filterProductsByAllowedProductIds(
|
|
loadForm.value.products || [],
|
|
allowedIds
|
|
)
|
|
loadForm.value = { ...loadForm.value, products: nextProducts }
|
|
syncFromLoadProducts()
|
|
}
|
|
},
|
|
{ deep: true }
|
|
)
|
|
|
|
watch(
|
|
() =>
|
|
deliveryForms.value.map((row) => ({
|
|
customerName: row.customerName,
|
|
contractId: String(row.contracts?.[0]?.contractId || '').trim()
|
|
})),
|
|
async (curr, prev) => {
|
|
if (detailLoading.value || suppressStopProductSync.value) return
|
|
for (let i = 0; i < curr.length; i += 1) {
|
|
const id = curr[i]?.contractId || ''
|
|
const prevId = prev?.[i]?.contractId || ''
|
|
if (!id) continue
|
|
const seq = (deliveryContractFetchSeqByIndex[i] || 0) + 1
|
|
deliveryContractFetchSeqByIndex[i] = seq
|
|
const allowedIds = await fetchContractProductIds(id)
|
|
if (deliveryContractFetchSeqByIndex[i] !== seq) continue
|
|
const delivery = deliveryForms.value[i]
|
|
if (!delivery || isPlInternalCustomerName(delivery.customerName)) continue
|
|
const contractChanged = !prev || id !== prevId
|
|
const hasOrphan = (delivery.products || []).some((row) => {
|
|
const pid = String(row.productId || '').trim()
|
|
return !!pid && !allowedIds.includes(pid)
|
|
})
|
|
// 换合同必滤;同合同仅在有合同外产品时滤(如同步后明细刚拉到)
|
|
if (!contractChanged && !hasOrphan) continue
|
|
const nextProducts = filterProductsByAllowedProductIds(delivery.products || [], allowedIds)
|
|
deliveryForms.value = deliveryForms.value.map((row, idx) =>
|
|
idx === i ? { ...row, products: nextProducts } : row
|
|
)
|
|
}
|
|
},
|
|
{ deep: true }
|
|
)
|
|
|
|
function openCustomerSelector(side: CustomerSide, deliveryIndex = 0) {
|
|
customerSelectorSide.value = side
|
|
customerSelectorDeliveryIndex.value = deliveryIndex
|
|
customerSelectorVisible.value = true
|
|
}
|
|
|
|
function handleCustomerConfirm(customer: CustomerConfirmRow) {
|
|
if (!customer?.id) return
|
|
if (customerSelectorSide.value === 'load') {
|
|
loadSectionRef.value?.applyCustomerFromPicker({
|
|
id: customer.id,
|
|
customerName: customer.customerName
|
|
})
|
|
return
|
|
}
|
|
deliverySectionRef.value?.applyCustomerFromPicker(customerSelectorDeliveryIndex.value, {
|
|
id: customer.id,
|
|
customerName: customer.customerName
|
|
})
|
|
}
|
|
|
|
watch(
|
|
() =>
|
|
[formData.pickupAddress, formData.pickupLng, formData.pickupLat] as [
|
|
string,
|
|
number | null,
|
|
number | null
|
|
],
|
|
(curr, prev) => {
|
|
if (suppressAddressMutex.value || !prev) return
|
|
if (
|
|
!isSameLogisticsAddress(
|
|
curr[0],
|
|
curr[1],
|
|
curr[2],
|
|
formData.destinationAddress,
|
|
formData.destinationLng,
|
|
formData.destinationLat
|
|
)
|
|
) {
|
|
return
|
|
}
|
|
// 仅当「取货侧」变化导致冲突时回滚取货
|
|
if (
|
|
isSameLogisticsAddress(
|
|
prev[0],
|
|
prev[1],
|
|
prev[2],
|
|
formData.destinationAddress,
|
|
formData.destinationLng,
|
|
formData.destinationLat
|
|
)
|
|
) {
|
|
return
|
|
}
|
|
ElMessage.warning(MSG_SAME_ADDRESS)
|
|
suppressAddressMutex.value = true
|
|
formData.pickupAddress = prev[0] || ''
|
|
formData.pickupLng = prev[1]
|
|
formData.pickupLat = prev[2]
|
|
nextTick(() => {
|
|
suppressAddressMutex.value = false
|
|
})
|
|
}
|
|
)
|
|
|
|
watch(
|
|
() =>
|
|
[formData.destinationAddress, formData.destinationLng, formData.destinationLat] as [
|
|
string,
|
|
number | null,
|
|
number | null
|
|
],
|
|
(curr, prev) => {
|
|
if (suppressAddressMutex.value || !prev) return
|
|
if (
|
|
!isSameLogisticsAddress(
|
|
formData.pickupAddress,
|
|
formData.pickupLng,
|
|
formData.pickupLat,
|
|
curr[0],
|
|
curr[1],
|
|
curr[2]
|
|
)
|
|
) {
|
|
return
|
|
}
|
|
if (
|
|
isSameLogisticsAddress(
|
|
formData.pickupAddress,
|
|
formData.pickupLng,
|
|
formData.pickupLat,
|
|
prev[0],
|
|
prev[1],
|
|
prev[2]
|
|
)
|
|
) {
|
|
return
|
|
}
|
|
ElMessage.warning(MSG_SAME_ADDRESS)
|
|
suppressAddressMutex.value = true
|
|
formData.destinationAddress = prev[0] || ''
|
|
formData.destinationLng = prev[1]
|
|
formData.destinationLat = prev[2]
|
|
nextTick(() => {
|
|
suppressAddressMutex.value = false
|
|
})
|
|
}
|
|
)
|
|
|
|
async function handleProductConfirm(products: any[]) {
|
|
const target = productSelectorTarget.value
|
|
const resolveDefaultPackageSpec = (productId: string) => {
|
|
let rows: ContractProductRowLite[] = []
|
|
if (target.side === 'delivery') {
|
|
const delivery = deliveryForms.value[target.deliveryIndex]
|
|
const cid = String(delivery?.contracts?.[0]?.contractId || '').trim()
|
|
rows = cid ? contractProductRowsCache.value[cid] || [] : []
|
|
} else {
|
|
rows = loadContractProductRows.value
|
|
}
|
|
const specs = packageSpecsForProduct(rows, productId)
|
|
return specs.length === 1 ? specs[0] : ''
|
|
}
|
|
const buildRows = async () => {
|
|
const rows: ReturnType<typeof createStopProductsFromPlanProducts> = []
|
|
for (const product of products) {
|
|
const brandMeta = initLineBrandMetaFromProduct(product as Record<string, unknown>)
|
|
const productId = String(product.id || '')
|
|
const row: PlanFormProductRow = {
|
|
productId,
|
|
productNo: String(product.productNo || product.id || ''),
|
|
productName: String(product.productName || ''),
|
|
model: product.model as string | undefined,
|
|
categoryName: product.categoryName as string | undefined,
|
|
unit: String(product.unit || '吨'),
|
|
quantity: 1,
|
|
actualQty: 0,
|
|
packageSpec: resolveDefaultPackageSpec(productId),
|
|
deliveryMethod: '',
|
|
isToxic: Number(product.isToxic || 0),
|
|
isExplosive: Number(product.isExplosive || 0),
|
|
...brandMeta
|
|
}
|
|
const [enriched] = await enrichPlanProductLinesAlias(
|
|
await enrichPlanProductsHazardFromCrm([row])
|
|
)
|
|
rows.push(...createStopProductsFromPlanProducts([enriched]))
|
|
}
|
|
return rows
|
|
}
|
|
|
|
if (target.side === 'delivery') {
|
|
const index = target.deliveryIndex
|
|
const delivery = deliveryForms.value[index]
|
|
if (!delivery) return
|
|
const cid = String(delivery?.contracts?.[0]?.contractId || '').trim()
|
|
const contractRows = cid ? contractProductRowsCache.value[cid] || [] : []
|
|
const existing = delivery.products || []
|
|
const existingKeys = new Set(
|
|
existing.map((row) => productPackageKey(row.productId, row.packageSpec)).filter(Boolean)
|
|
)
|
|
const countByPid = new Map<string, number>()
|
|
for (const row of existing) {
|
|
const pid = String(row.productId || '').trim()
|
|
if (!pid) continue
|
|
countByPid.set(pid, (countByPid.get(pid) || 0) + 1)
|
|
}
|
|
const added = (await buildRows()).filter((row) => {
|
|
const pid = String(row.productId || '').trim()
|
|
if (!pid) return false
|
|
const key = productPackageKey(row.productId, row.packageSpec)
|
|
const specs = packageSpecsForProduct(contractRows, pid)
|
|
// 同品多包装:可按包装份数追加空包装行,便于分别选包装
|
|
if (!normalizePackageSpec(row.packageSpec) && specs.length > 1) {
|
|
const count = countByPid.get(pid) || 0
|
|
if (count >= specs.length) return false
|
|
countByPid.set(pid, count + 1)
|
|
return true
|
|
}
|
|
if (key && existingKeys.has(key)) return false
|
|
if (key) existingKeys.add(key)
|
|
countByPid.set(pid, (countByPid.get(pid) || 0) + 1)
|
|
return true
|
|
})
|
|
if (!added.length) {
|
|
ElMessage.info('所选产品(含包装)已在送货段中')
|
|
return
|
|
}
|
|
deliveryForms.value = deliveryForms.value.map((row, idx) =>
|
|
idx === index ? { ...row, products: [...(row.products || []), ...added] } : row
|
|
)
|
|
return
|
|
}
|
|
|
|
const nextRows = [...(loadForm.value.products || []), ...(await buildRows())]
|
|
loadForm.value = { ...loadForm.value, products: nextRows }
|
|
freightCalc.resetFreightCalc()
|
|
}
|
|
|
|
function mapPlanProductToFormRow(item: PlanProduct, index: number): PlanFormProductRow {
|
|
return {
|
|
id: item.id,
|
|
productId: String(item.productId || index),
|
|
productNo: item.productNo || String(item.productId || index),
|
|
productName: item.productName || '',
|
|
brandMode: item.brandMode || undefined,
|
|
brandId: item.brandId || undefined,
|
|
brandName: item.brandName || undefined,
|
|
model: item.model,
|
|
categoryName: item.categoryName,
|
|
unit: item.unit || '吨',
|
|
quantity: Number(item.quantity || item.inboundEstimateQty || item.outboundEstimateQty || 0),
|
|
actualQty: 0,
|
|
packageSpec: item.packageSpec,
|
|
deliveryMethod: item.deliveryMethod,
|
|
taxRate: undefined,
|
|
taxPrice: item.taxPrice ?? item.inboundPrice ?? item.outboundPrice,
|
|
taxSubtotal: item.taxSubtotal ?? item.inboundAmount ?? item.outboundAmount,
|
|
taxAmount: item.taxAmount,
|
|
remark: item.remark,
|
|
isExplosive: Number(
|
|
item.isExplosive ?? (item as { is_explosive?: number }).is_explosive ?? 0
|
|
),
|
|
isToxic: Number(item.isToxic ?? (item as { is_toxic?: number }).is_toxic ?? 0),
|
|
purchaseCertUrl: item.purchaseCertUrl,
|
|
transportCertUrl: item.transportCertUrl,
|
|
legalUseCertUrl: item.legalUseCertUrl
|
|
}
|
|
}
|
|
|
|
function mapPlanProductToCopyFormRow(item: PlanProduct, index: number): PlanFormProductRow {
|
|
const row = mapPlanProductToFormRow(item, index)
|
|
return {
|
|
...row,
|
|
id: undefined,
|
|
actualQty: 0,
|
|
purchaseCertUrl: undefined,
|
|
transportCertUrl: undefined,
|
|
legalUseCertUrl: undefined
|
|
}
|
|
}
|
|
|
|
function mapProductsToSubmitPayload() {
|
|
return formData.products.map((item) => {
|
|
prepareLineAliasAndBrandForSubmit(item)
|
|
return {
|
|
...(item.id ? { id: item.id } : {}),
|
|
productId: item.productId,
|
|
productNo: item.productNo,
|
|
productName: item.productName,
|
|
brandMode: item.brandMode || undefined,
|
|
brandId: item.brandMode === 'required' ? item.brandId || undefined : undefined,
|
|
brandName: item.brandMode === 'required' ? item.brandName || undefined : undefined,
|
|
model: item.model,
|
|
unit: item.unit,
|
|
categoryName: item.categoryName,
|
|
packageSpec: item.packageSpec,
|
|
deliveryMethod: item.deliveryMethod,
|
|
quantity: item.quantity,
|
|
actualQty: 0,
|
|
taxPrice: item.taxPrice,
|
|
taxAmount: item.taxAmount,
|
|
taxSubtotal: item.taxSubtotal,
|
|
remark: item.remark,
|
|
isExplosive: Number(item.isExplosive || 0),
|
|
isToxic: Number(item.isToxic || 0),
|
|
purchaseCertUrl: item.purchaseCertUrl || undefined,
|
|
transportCertUrl: item.transportCertUrl || undefined,
|
|
legalUseCertUrl: item.legalUseCertUrl || undefined
|
|
}
|
|
})
|
|
}
|
|
|
|
function buildTransportBlock(options?: { allowClear?: boolean }) {
|
|
if (!formData.vehicleId) {
|
|
if (!options?.allowClear) return undefined
|
|
return {
|
|
vehicleId: '',
|
|
freightType: formData.freightType
|
|
}
|
|
}
|
|
return {
|
|
vehicleId: formData.vehicleId,
|
|
driverId: selectedDriverId.value || undefined,
|
|
escortId: selectedEscortId.value || undefined,
|
|
carrierId: formData.carrierId || undefined,
|
|
distanceKm: formData.distanceKm ?? undefined,
|
|
freightType: formData.freightType,
|
|
transportQty:
|
|
formData.freightType === 'by_weight'
|
|
? (freightCalc.transportQty.value ?? undefined)
|
|
: undefined,
|
|
freightPrice:
|
|
formData.freightType === 'by_weight'
|
|
? (freightCalc.freightPrice.value ?? undefined)
|
|
: undefined,
|
|
freightAmount: freightCalc.freightAmount.value ?? undefined
|
|
}
|
|
}
|
|
|
|
function buildPlanCommonPayload(options?: { allowClearTransport?: boolean }): PlanUpdatePayload {
|
|
const firstDelivery = deliveryForms.value[0]
|
|
return {
|
|
createDate: formData.createDate,
|
|
planDate: formData.planDate || null,
|
|
belongsCompany: formData.belongsCompany,
|
|
planType: formData.planType || '',
|
|
supplierId: loadForm.value.customerId || undefined,
|
|
supplierName: loadForm.value.customerName || undefined,
|
|
destinationId: firstDelivery?.customerId || undefined,
|
|
destinationName: firstDelivery?.customerName || undefined,
|
|
customerId: firstDelivery?.customerId || undefined,
|
|
customerName: firstDelivery?.customerName || undefined,
|
|
contactName: formData.contactName || undefined,
|
|
contactPhone: formData.contactPhone || undefined,
|
|
hasFreight: formData.hasFreight,
|
|
pickupAddress: loadForm.value.address || undefined,
|
|
pickupLng: loadForm.value.lng,
|
|
pickupLat: loadForm.value.lat,
|
|
pickupTime: loadForm.value.loadTime || undefined,
|
|
pickupContact: loadForm.value.contact || undefined,
|
|
pickupNotes: loadForm.value.notes || undefined,
|
|
destinationAddress: firstDelivery?.address || undefined,
|
|
destinationLng: firstDelivery?.lng,
|
|
destinationLat: firstDelivery?.lat,
|
|
deliveryTime: firstDelivery?.deliveryTime || undefined,
|
|
deliveryContact: firstDelivery?.contact || undefined,
|
|
destinationNotes: firstDelivery?.notes || undefined,
|
|
followUserId: formData.followUserId || undefined,
|
|
followUserName: formData.followUserName || undefined,
|
|
remark: formData.remark || undefined,
|
|
load: buildLoadPayload(loadForm.value),
|
|
deliveries: buildDeliveriesPayload(deliveryForms.value),
|
|
products: mapProductsToSubmitPayload(),
|
|
transport: buildTransportBlock({ allowClear: options?.allowClearTransport })
|
|
}
|
|
}
|
|
|
|
async function restoreFreightFromTransport(plan: Plan) {
|
|
if (!plan.transportId) {
|
|
if (plan.freightAmount) {
|
|
freightCalc.freightAmount.value = Number(plan.freightAmount)
|
|
}
|
|
return
|
|
}
|
|
|
|
try {
|
|
const transportRes = await transportApi.get(plan.transportId)
|
|
const transport = (transportRes.data ?? transportRes) as {
|
|
distanceKm?: number
|
|
freightType?: string
|
|
freightPrice?: number
|
|
transportQty?: number
|
|
freightAmount?: number
|
|
carrierId?: string
|
|
driverId?: string
|
|
escortId?: string
|
|
}
|
|
formData.distanceKm = transport.distanceKm != null ? Number(transport.distanceKm) : null
|
|
formData.freightType = transport.freightType || 'by_weight'
|
|
if (transport.carrierId) {
|
|
formData.carrierId = transport.carrierId
|
|
}
|
|
if (transport.driverId) {
|
|
selectedDriverId.value = transport.driverId
|
|
}
|
|
if (transport.escortId) {
|
|
selectedEscortId.value = transport.escortId
|
|
}
|
|
freightCalc.freightPrice.value =
|
|
transport.freightPrice != null ? Number(transport.freightPrice) : null
|
|
freightCalc.freightAmount.value =
|
|
transport.freightAmount != null ? Number(transport.freightAmount) : null
|
|
freightCalc.freightAmountTouched.value = false
|
|
} catch {
|
|
if (plan.freightAmount) {
|
|
freightCalc.freightAmount.value = Number(plan.freightAmount)
|
|
}
|
|
}
|
|
}
|
|
|
|
function getUnitLabel(unit: string) {
|
|
return dictStore.getLabel('crm_product_unit', unit) || unit
|
|
}
|
|
|
|
function formatDriverOptionLabel(driver: Personnel) {
|
|
const phone = driver.phoneNo ? ` (${driver.phoneNo})` : ''
|
|
const affiliation = getPersonnelAffiliationLabel(driver.personnelAffiliation)
|
|
const suffix = affiliation ? ` · ${affiliation}` : ''
|
|
return `${driver.personnelName}${phone}${suffix}`
|
|
}
|
|
|
|
async function performArrangeAfterSave(planId: string): Promise<boolean> {
|
|
const detailRes = await planApi.get(planId)
|
|
const plan = pickBxData<Plan>(detailRes)
|
|
if (!plan?.id) {
|
|
ElMessage.error('获取运输计划失败,无法安排运输')
|
|
return false
|
|
}
|
|
loadedPlanSnapshot.value = plan
|
|
|
|
if (!formData.vehicleId) {
|
|
ElMessage.warning('安排运输前请选择车辆')
|
|
return false
|
|
}
|
|
|
|
const rawProducts = Array.isArray(plan.products) ? plan.products : []
|
|
const enrichedProducts = await enrichPlanProductsHazardFromCrm(rawProducts)
|
|
const certMessage = buildMissingHazardCertReadyMessage(plan, enrichedProducts)
|
|
if (certMessage) {
|
|
await ElMessageBox.alert(`${certMessage},请至【危化品证上传】页面操作。`, '无法安排运输', {
|
|
type: 'warning'
|
|
})
|
|
return false
|
|
}
|
|
|
|
const isRearrange = plan.transportStatus === 'arranged'
|
|
const dibangSelected = planHasAnyDibangSelected(plan, loadForm.value, deliveryForms.value)
|
|
let historical = { hasHistoricalDibang: false, hasHistoricalSiteLinks: false }
|
|
if (isRearrange) {
|
|
historical = await loadArrangeHistoricalFlags(planId)
|
|
}
|
|
|
|
try {
|
|
await confirmBeforePlanArrange({
|
|
isRearrange,
|
|
plan,
|
|
hasHistoricalDibang: historical.hasHistoricalDibang,
|
|
hasHistoricalSiteLinks: historical.hasHistoricalSiteLinks,
|
|
hasAnyDibangSelected: dibangSelected
|
|
})
|
|
} catch (error) {
|
|
if (error === 'cancel') return false
|
|
throw error
|
|
}
|
|
|
|
const payload = buildArrangePayloadFromPlanForm({
|
|
plan,
|
|
vehicleId: formData.vehicleId,
|
|
driverId: selectedDriverId.value,
|
|
escortId: selectedEscortId.value,
|
|
carrierId: formData.carrierId,
|
|
loadForm: loadForm.value,
|
|
deliveryForms: deliveryForms.value,
|
|
products: rawProducts,
|
|
remark: formData.remark,
|
|
distanceKm: formData.distanceKm,
|
|
freightType: formData.freightType,
|
|
hasFreight: formData.hasFreight,
|
|
freightCalc,
|
|
getUnitLabel
|
|
})
|
|
|
|
try {
|
|
await submitPlanArrange(planId, payload)
|
|
} catch {
|
|
return false
|
|
}
|
|
ElMessage.success(isRearrange ? '运输调整成功' : '运输安排成功')
|
|
innerVisible.value = false
|
|
emit('success', { planId, arranged: true })
|
|
return true
|
|
}
|
|
|
|
function resetForm() {
|
|
loadedTransportStatus.value = 'pending'
|
|
loadedFollowStatus.value = ''
|
|
loadedPlanSnapshot.value = null
|
|
vehicleBundle.value = null
|
|
selectedDriverId.value = ''
|
|
selectedEscortId.value = ''
|
|
const d = todayYmd()
|
|
Object.assign(formData, {
|
|
createDate: d,
|
|
planDate: '',
|
|
belongsCompany: 'shanxia',
|
|
planType: '',
|
|
followUserId: resolveCurrentUserId(),
|
|
followUserName: resolveCurrentUserName(),
|
|
supplierId: '',
|
|
supplierName: '',
|
|
supplierContracts: [],
|
|
destinationId: '',
|
|
destinationName: '',
|
|
customerId: '',
|
|
customerName: '',
|
|
demanderContracts: [],
|
|
contactName: '',
|
|
contactPhone: '',
|
|
hasFreight: 0,
|
|
pickupAddress: '',
|
|
pickupLng: null as number | null,
|
|
pickupLat: null as number | null,
|
|
pickupTime: '',
|
|
pickupContact: '',
|
|
pickupNotes: '',
|
|
destinationAddress: '',
|
|
destinationLng: null as number | null,
|
|
destinationLat: null as number | null,
|
|
deliveryTime: '',
|
|
deliveryContact: '',
|
|
destinationNotes: '',
|
|
vehicleId: '',
|
|
carrierId: '',
|
|
distanceKm: null,
|
|
freightType: 'by_weight',
|
|
remark: '',
|
|
products: []
|
|
})
|
|
syncCommittedPartyIds()
|
|
resetStopForms()
|
|
freightCalc.resetFreightCalc()
|
|
basicFormRef.value?.clearValidate()
|
|
}
|
|
|
|
/** 计划日期变更时,装货/各送货时间跟随;仍可单独调整 */
|
|
function handlePlanDateChange(val: string | null) {
|
|
const nextVal = val || ''
|
|
loadForm.value.loadTime = nextVal
|
|
deliveryForms.value = deliveryForms.value.map((row) => ({
|
|
...row,
|
|
deliveryTime: nextVal
|
|
}))
|
|
}
|
|
|
|
function handleOpened() {
|
|
loadMasterData().then(async () => {
|
|
if (props.planId) {
|
|
await loadPlanForEdit(props.planId)
|
|
} else if (props.copyFromPlanId) {
|
|
await loadPlanForCopy(props.copyFromPlanId)
|
|
} else {
|
|
resetForm()
|
|
}
|
|
})
|
|
}
|
|
|
|
function handleCancel() {
|
|
if (isPageVariant.value) {
|
|
emit('close')
|
|
return
|
|
}
|
|
innerVisible.value = false
|
|
}
|
|
|
|
onMounted(() => {
|
|
if (isPageVariant.value) handleOpened()
|
|
})
|
|
|
|
async function loadPlanForEdit(planId: string) {
|
|
detailLoading.value = true
|
|
suppressAddressMutex.value = true
|
|
suppressStopProductSync.value = true
|
|
try {
|
|
resetForm()
|
|
const res = await planApi.get(planId)
|
|
const data = (res.data ?? res) as Plan
|
|
Object.assign(formData, {
|
|
createDate: data.createDate || formatDate(data.createTime) || todayYmd(),
|
|
planDate: data.planDate || '',
|
|
belongsCompany: data.belongsCompany,
|
|
planType: data.planType || '',
|
|
followUserId: data.followUserId || resolveCurrentUserId(),
|
|
followUserName: data.followUserName || resolveCurrentUserName(),
|
|
contactName: data.contactName || '',
|
|
contactPhone: data.contactPhone || '',
|
|
hasFreight: data.hasFreight ?? 0,
|
|
vehicleId: data.vehicleId || '',
|
|
remark: data.remark || ''
|
|
})
|
|
applyStopsFromPlan(data)
|
|
loadedTransportStatus.value = data.transportStatus || 'pending'
|
|
loadedFollowStatus.value = data.followStatus || ''
|
|
loadedPlanSnapshot.value = data
|
|
syncCommittedPartyIds()
|
|
|
|
formData.products = await enrichPlanProductLinesAlias(
|
|
await enrichPlanProductsHazardFromCrm((data.products || []).map(mapPlanProductToFormRow))
|
|
)
|
|
bootstrapStopsFromPlanProducts()
|
|
|
|
if (data.vehicleId) {
|
|
await ensureSelectedVehicleOption(data.vehicleId)
|
|
await handleVehicleChange(data.vehicleId, { syncStopDibang: false })
|
|
}
|
|
if (data.driverId) {
|
|
selectedDriverId.value = data.driverId
|
|
}
|
|
if (data.escortId) {
|
|
selectedEscortId.value = data.escortId
|
|
}
|
|
freightCalc.resetFreightCalc()
|
|
await restoreFreightFromTransport(data)
|
|
if (selectedDriverId.value) {
|
|
await ensureSelectedDriverOption(selectedDriverId.value)
|
|
}
|
|
if (selectedEscortId.value) {
|
|
await ensureSelectedEscortOption(selectedEscortId.value)
|
|
}
|
|
if (formData.carrierId) {
|
|
await ensureSelectedCarrierOption(formData.carrierId)
|
|
}
|
|
syncSelectedCustomerOptions()
|
|
} finally {
|
|
await nextTick()
|
|
suppressAddressMutex.value = false
|
|
suppressStopProductSync.value = false
|
|
detailLoading.value = false
|
|
}
|
|
}
|
|
|
|
async function loadPlanForCopy(planId: string) {
|
|
detailLoading.value = true
|
|
suppressAddressMutex.value = true
|
|
suppressStopProductSync.value = true
|
|
try {
|
|
resetForm()
|
|
const res = await planApi.get(planId)
|
|
const data = (res.data ?? res) as Plan
|
|
Object.assign(formData, {
|
|
createDate: todayYmd(),
|
|
planDate: data.planDate || '',
|
|
belongsCompany: data.belongsCompany,
|
|
planType: data.planType || '',
|
|
followUserId: resolveCurrentUserId(),
|
|
followUserName: resolveCurrentUserName(),
|
|
contactName: data.contactName || '',
|
|
contactPhone: data.contactPhone || '',
|
|
hasFreight: data.hasFreight ?? 0,
|
|
remark: data.remark || ''
|
|
})
|
|
applyStopsFromPlan(data)
|
|
// 复制为新计划:不回传源计划段/段产品 id,避免误 upsert
|
|
loadForm.value = {
|
|
...loadForm.value,
|
|
id: undefined,
|
|
products: loadForm.value.products.map((row) => ({
|
|
...row,
|
|
id: undefined,
|
|
planProductId: undefined
|
|
}))
|
|
}
|
|
deliveryForms.value = deliveryForms.value.map((row) => ({
|
|
...row,
|
|
id: undefined,
|
|
products: row.products.map((product) => ({
|
|
...product,
|
|
id: undefined,
|
|
planProductId: undefined
|
|
}))
|
|
}))
|
|
loadedTransportStatus.value = 'pending'
|
|
loadedFollowStatus.value = ''
|
|
syncCommittedPartyIds()
|
|
|
|
formData.products = await enrichPlanProductLinesAlias(
|
|
await enrichPlanProductsHazardFromCrm(
|
|
(data.products || []).map(mapPlanProductToCopyFormRow)
|
|
)
|
|
)
|
|
bootstrapStopsFromPlanProducts()
|
|
|
|
formData.vehicleId = ''
|
|
formData.carrierId = ''
|
|
selectedDriverId.value = ''
|
|
selectedEscortId.value = ''
|
|
vehicleBundle.value = null
|
|
reapplyStopDibangByVehiclePlate('')
|
|
freightCalc.resetFreightCalc()
|
|
syncSelectedCustomerOptions()
|
|
} catch (e) {
|
|
const message = e instanceof Error ? e.message : '加载源计划失败'
|
|
ElMessage.error(message.includes('加载') ? message : `加载源计划失败:${message}`)
|
|
emit('update:visible', false)
|
|
} finally {
|
|
await nextTick()
|
|
suppressAddressMutex.value = false
|
|
suppressStopProductSync.value = false
|
|
detailLoading.value = false
|
|
}
|
|
}
|
|
|
|
function handleClosed() {
|
|
resetForm()
|
|
}
|
|
|
|
async function loadMasterData() {
|
|
await Promise.all([
|
|
searchVehicles(''),
|
|
searchCarriers(''),
|
|
searchDrivers(''),
|
|
searchEscorts(''),
|
|
loadDefaultCustomerOptions()
|
|
])
|
|
}
|
|
|
|
function resolveDefaultDriver(personnelList: VehiclePersonnelRow[]) {
|
|
const drivers = personnelList.filter((item) => item.role === 'driver' && item.personnelId)
|
|
const defaultDriver = drivers.find((item) => item.isDefault === 1) || drivers[0]
|
|
selectedDriverId.value = defaultDriver?.personnelId || ''
|
|
}
|
|
|
|
function resolveDefaultEscort(personnelList: VehiclePersonnelRow[]) {
|
|
const escorts = personnelList.filter((item) => item.role === 'escort' && item.personnelId)
|
|
const defaultEscort = escorts.find((item) => item.isDefault === 1) || escorts[0]
|
|
selectedEscortId.value = defaultEscort?.personnelId || ''
|
|
}
|
|
|
|
async function handleVehicleChange(vehicleId: string, opts?: { syncStopDibang?: boolean }) {
|
|
const syncStopDibang = opts?.syncStopDibang !== false
|
|
vehicleBundle.value = null
|
|
selectedDriverId.value = ''
|
|
selectedEscortId.value = ''
|
|
if (!vehicleId) {
|
|
formData.carrierId = ''
|
|
if (syncStopDibang) reapplyStopDibangByVehiclePlate('')
|
|
return
|
|
}
|
|
|
|
const vehicle = vehicleList.value.find((item) => item.id === vehicleId)
|
|
if (vehicle?.carrierId) {
|
|
formData.carrierId = vehicle.carrierId
|
|
} else {
|
|
formData.carrierId = ''
|
|
}
|
|
|
|
vehicleLoading.value = true
|
|
try {
|
|
vehicleBundle.value = await loadVehicleViewBundle(vehicleId)
|
|
if (vehicleBundle.value.detail?.carrierId) {
|
|
formData.carrierId = vehicleBundle.value.detail.carrierId
|
|
}
|
|
resolveDefaultDriver(vehicleBundle.value.personnelList || [])
|
|
resolveDefaultEscort(vehicleBundle.value.personnelList || [])
|
|
if (selectedDriverId.value) {
|
|
await ensureSelectedDriverOption(selectedDriverId.value)
|
|
}
|
|
if (selectedEscortId.value) {
|
|
await ensureSelectedEscortOption(selectedEscortId.value)
|
|
}
|
|
} finally {
|
|
vehicleLoading.value = false
|
|
}
|
|
if (syncStopDibang) {
|
|
const plate =
|
|
(vehicle?.licensePlate || '').trim() ||
|
|
(vehicleBundle.value?.detail?.licensePlate || '').trim()
|
|
reapplyStopDibangByVehiclePlate(plate)
|
|
}
|
|
}
|
|
|
|
async function handleVehicleLibrarySelect(vehicle: Vehicle) {
|
|
if (!vehicle?.id) return
|
|
await ensureSelectedVehicleOption(vehicle.id)
|
|
formData.vehicleId = vehicle.id
|
|
await handleVehicleChange(vehicle.id)
|
|
if (vehicle.carrierId) {
|
|
await ensureSelectedCarrierOption(vehicle.carrierId)
|
|
}
|
|
}
|
|
|
|
async function handlePasteRecognizeDone(result?: PasteCommitResult) {
|
|
if (!result?.vehicleId) return
|
|
await handleVehicleLibrarySelect({ id: result.vehicleId } as Vehicle)
|
|
}
|
|
|
|
async function handleCarrierLibrarySelect(carrier: Carrier) {
|
|
if (!carrier?.id) return
|
|
await ensureSelectedCarrierOption(carrier.id)
|
|
formData.carrierId = carrier.id
|
|
}
|
|
|
|
function validateBusinessRules() {
|
|
if (!loadForm.value.customerId || !String(loadForm.value.customerName || '').trim()) {
|
|
ElMessage.warning('请选择装货客户')
|
|
return false
|
|
}
|
|
if (!String(loadForm.value.loadTime || '').trim()) {
|
|
ElMessage.warning('请选择装货时间')
|
|
return false
|
|
}
|
|
if (!deliveryForms.value.length) {
|
|
ElMessage.warning('请至少添加一段送货信息')
|
|
return false
|
|
}
|
|
for (let i = 0; i < deliveryForms.value.length; i += 1) {
|
|
const row = deliveryForms.value[i]
|
|
if (!row.customerId || !String(row.customerName || '').trim()) {
|
|
ElMessage.warning(`送货段#${i + 1}请选择客户`)
|
|
return false
|
|
}
|
|
if (!String(row.deliveryTime || '').trim()) {
|
|
ElMessage.warning(`送货段#${i + 1}请选择送货时间`)
|
|
return false
|
|
}
|
|
if (
|
|
loadForm.value.customerId &&
|
|
row.customerId &&
|
|
String(loadForm.value.customerId) === String(row.customerId)
|
|
) {
|
|
ElMessage.warning(MSG_SAME_CUSTOMER)
|
|
return false
|
|
}
|
|
}
|
|
if (!loadForm.value.products.length) {
|
|
ElMessage.warning('请至少添加一条装货产品')
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
function stopHasSelectedContract(contracts?: { contractId?: string }[] | null) {
|
|
return (contracts || []).some((item) => !!String(item.contractId || '').trim())
|
|
}
|
|
|
|
/** 非内部客户:装/送货段合同至多 1 份;已选 1 份时校验装/送货产品归属合同 */
|
|
function validateSingleContractPerStop(): boolean {
|
|
if (
|
|
!isPlInternalCustomerName(loadForm.value.customerName) &&
|
|
(loadForm.value.contracts || []).length > 1
|
|
) {
|
|
ElMessage.warning('装货合同只能保留 1 份,请移除多余合同后再保存')
|
|
return false
|
|
}
|
|
for (let i = 0; i < deliveryForms.value.length; i += 1) {
|
|
const row = deliveryForms.value[i]
|
|
if (isPlInternalCustomerName(row.customerName)) continue
|
|
if ((row.contracts || []).length > 1) {
|
|
ElMessage.warning(`送货段#${i + 1}合同只能保留 1 份,请移除多余合同后再保存`)
|
|
return false
|
|
}
|
|
}
|
|
if (
|
|
!isPlInternalCustomerName(loadForm.value.customerName) &&
|
|
(loadForm.value.contracts || []).length === 1
|
|
) {
|
|
const products = loadForm.value.products || []
|
|
if (products.length) {
|
|
const allowed = new Set(
|
|
loadContractProductIds.value.map((id) => String(id).trim()).filter(Boolean)
|
|
)
|
|
if (!allowed.size) {
|
|
ElMessage.warning('所选合同暂无产品明细,或合同明细加载失败,请重选合同后再保存')
|
|
return false
|
|
}
|
|
const orphan = products.some((row) => !allowed.has(String(row.productId || '').trim()))
|
|
if (orphan) {
|
|
ElMessage.warning('装货产品须属于所选合同')
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
for (let i = 0; i < deliveryForms.value.length; i += 1) {
|
|
const row = deliveryForms.value[i]
|
|
if (isPlInternalCustomerName(row.customerName)) continue
|
|
if ((row.contracts || []).length !== 1) continue
|
|
const products = row.products || []
|
|
if (!products.length) continue
|
|
const cid = String(row.contracts?.[0]?.contractId || '').trim()
|
|
const allowedList = cid ? contractProductIdsCache.value[cid] : undefined
|
|
const allowed = new Set((allowedList || []).map((id) => String(id).trim()).filter(Boolean))
|
|
if (!allowed.size) {
|
|
ElMessage.warning(
|
|
`送货段#${i + 1}所选合同暂无产品明细,或合同明细加载失败,请重选合同后再保存`
|
|
)
|
|
return false
|
|
}
|
|
const orphan = products.some((p) => !allowed.has(String(p.productId || '').trim()))
|
|
if (orphan) {
|
|
ElMessage.warning(`送货段#${i + 1}产品须属于所选合同`)
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
/** 绑合同时:合同产品有包装选项则包装必填且须在选项内 */
|
|
function validateContractPackageSpecs(): boolean {
|
|
const checkProducts = (
|
|
products: PlanStopProductFormRow[] | undefined,
|
|
contractId: string,
|
|
stopLabel: string
|
|
): boolean => {
|
|
const cid = String(contractId || '').trim()
|
|
if (!cid) return true
|
|
const contractRows = contractProductRowsCache.value[cid] || []
|
|
for (const product of products || []) {
|
|
const pid = String(product.productId || '').trim()
|
|
if (!pid) continue
|
|
const allowed = packageSpecsForProduct(contractRows, pid)
|
|
if (!allowed.length) continue
|
|
const pkg = normalizePackageSpec(product.packageSpec)
|
|
const name = String(product.productName || product.standardProductName || pid).trim() || pid
|
|
if (!pkg) {
|
|
ElMessage.error(`请选择包装规格:${stopLabel}${name}`)
|
|
return false
|
|
}
|
|
if (!isPackageAllowedForContractProduct(contractRows, pid, pkg)) {
|
|
ElMessage.error(`包装规格不在合同范围内:${stopLabel}${name}`)
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
if (
|
|
!checkProducts(
|
|
loadForm.value.products,
|
|
String(loadForm.value.contracts?.[0]?.contractId || ''),
|
|
'装货 '
|
|
)
|
|
) {
|
|
return false
|
|
}
|
|
for (let i = 0; i < deliveryForms.value.length; i += 1) {
|
|
const row = deliveryForms.value[i]
|
|
if (
|
|
!checkProducts(
|
|
row.products,
|
|
String(row.contracts?.[0]?.contractId || ''),
|
|
`送货段#${i + 1} `
|
|
)
|
|
) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
/** 非内部客户(山峡/云栋)的装/送货段必须至少选一份合同(保存与安排均校验) */
|
|
function validateStopContractsRequired() {
|
|
if (
|
|
!isPlInternalCustomerName(loadForm.value.customerName) &&
|
|
!stopHasSelectedContract(loadForm.value.contracts)
|
|
) {
|
|
ElMessage.warning('请为装货客户选择合同')
|
|
return false
|
|
}
|
|
for (let i = 0; i < deliveryForms.value.length; i += 1) {
|
|
const row = deliveryForms.value[i]
|
|
if (isPlInternalCustomerName(row.customerName)) continue
|
|
if (!stopHasSelectedContract(row.contracts)) {
|
|
ElMessage.warning(`请为送货段#${i + 1}选择合同`)
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
/** 安排运输前:装/送货产品单位、计划数量、包装规格、送货方式必填 */
|
|
function validateStopProductsForArrange() {
|
|
const gap = findFirstStopProductArrangeGap({
|
|
loadProducts: loadForm.value.products,
|
|
deliveries: deliveryForms.value
|
|
})
|
|
if (gap) {
|
|
ElMessage.warning(gap)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
/** 安排运输前必填:创建日期、计划日期、归属公司、单据类型、跟进人 */
|
|
function getArrangePrerequisiteGaps(): string[] {
|
|
const missing: string[] = []
|
|
if (!String(formData.createDate || '').trim()) missing.push('创建日期')
|
|
if (!String(formData.planDate || '').trim()) missing.push('计划日期')
|
|
if (!String(formData.belongsCompany || '').trim()) missing.push('归属公司')
|
|
if (!String(formData.planType || '').trim()) missing.push('单据类型')
|
|
if (
|
|
!String(formData.followUserId || '').trim() &&
|
|
!String(formData.followUserName || '').trim()
|
|
) {
|
|
missing.push('跟进人')
|
|
}
|
|
return missing
|
|
}
|
|
|
|
async function handleSubmit(mode: 'draft' | 'save' | 'saveAndArrange' | 'rearrange' = 'save') {
|
|
// 必须在 await validate 之前置位,避免连点并发多次 createFull
|
|
if (submitLoading.value) return
|
|
submitLoading.value = true
|
|
try {
|
|
if (mode === 'draft') {
|
|
const companyOk = !!(formData.belongsCompany || '').trim()
|
|
const dateOk = !!(formData.createDate || '').trim()
|
|
if (!companyOk || !dateOk) {
|
|
ElMessage.warning('暂存请至少填写归属公司和创建日期')
|
|
await basicFormRef.value
|
|
?.validateField?.(['belongsCompany', 'createDate'])
|
|
.catch(() => undefined)
|
|
return
|
|
}
|
|
mirrorPlanProductsFromLoad()
|
|
const draftPayload = {
|
|
...buildPlanCommonPayload({ allowClearTransport: isEdit.value }),
|
|
saveMode: 'draft' as const,
|
|
hasFreight: formData.hasFreight
|
|
}
|
|
let planId = props.planId || ''
|
|
if (isEdit.value) {
|
|
if (!props.planId) return
|
|
await planApi.update(props.planId, draftPayload)
|
|
planId = props.planId
|
|
} else {
|
|
const res = await planApi.createFull(
|
|
draftPayload as Parameters<typeof planApi.createFull>[0]
|
|
)
|
|
const created = pickBxData<Plan>(res)
|
|
planId = String(created?.id || '')
|
|
if (!planId) {
|
|
ElMessage.error('暂存成功但未返回计划 ID')
|
|
return
|
|
}
|
|
}
|
|
ElMessage.success('暂存成功')
|
|
innerVisible.value = false
|
|
emit('success', { planId, arranged: false })
|
|
return
|
|
}
|
|
|
|
const valid = await basicFormRef.value?.validate().catch(() => false)
|
|
if (!valid || !validateBusinessRules()) return
|
|
if (!validateSingleContractPerStop()) return
|
|
if (!validateContractPackageSpecs()) return
|
|
if (!validateStopContractsRequired()) return
|
|
|
|
if (mode === 'saveAndArrange' || mode === 'rearrange') {
|
|
const missing = getArrangePrerequisiteGaps()
|
|
if (missing.length) {
|
|
ElMessage.warning(`安排运输前请先填写:${missing.join('、')}`)
|
|
return
|
|
}
|
|
if (!formData.vehicleId) {
|
|
ElMessage.warning('安排运输前请选择车辆')
|
|
return
|
|
}
|
|
if (!validateStopProductsForArrange()) return
|
|
}
|
|
|
|
mirrorPlanProductsFromLoad()
|
|
const commonPayload = {
|
|
...buildPlanCommonPayload({ allowClearTransport: isEdit.value }),
|
|
saveMode: 'formal' as const
|
|
}
|
|
let planId = props.planId || ''
|
|
const wasCreate = !isEdit.value
|
|
|
|
if (isEdit.value) {
|
|
if (!props.planId) return
|
|
const res = await planApi.update(props.planId, commonPayload)
|
|
planId = props.planId
|
|
showPlanSaveWarnings(pickBxData<Plan>(res))
|
|
if (mode === 'save') {
|
|
ElMessage.success('保存成功')
|
|
}
|
|
} else {
|
|
const res = await planApi.createFull({
|
|
...commonPayload,
|
|
hasFreight: formData.hasFreight
|
|
} as Parameters<typeof planApi.createFull>[0])
|
|
const created = pickBxData<Plan>(res)
|
|
planId = String(created?.id || '')
|
|
showPlanSaveWarnings(created)
|
|
if (!planId) {
|
|
ElMessage.error('创建成功但未返回计划 ID')
|
|
return
|
|
}
|
|
if (mode === 'save') {
|
|
ElMessage.success('创建成功')
|
|
}
|
|
}
|
|
|
|
if ((mode === 'saveAndArrange' || mode === 'rearrange') && planId) {
|
|
const arranged = await performArrangeAfterSave(planId)
|
|
if (!arranged) {
|
|
// 计划已落库;关闭新建窗,避免停留在创建态再次点击又生成一张
|
|
if (wasCreate) {
|
|
ElMessage.warning(
|
|
'计划已创建成功,但未能安排运输。请至列表查看;若因危化证未就绪,请先完成【危化品证上传】后再安排。'
|
|
)
|
|
} else {
|
|
ElMessage.warning('计划已保存,但未能安排运输。请稍后在列表中重试安排。')
|
|
}
|
|
innerVisible.value = false
|
|
emit('success', { planId, arranged: false })
|
|
}
|
|
return
|
|
}
|
|
|
|
innerVisible.value = false
|
|
emit('success', { planId, arranged: false })
|
|
} finally {
|
|
submitLoading.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.mp-plan-form-title {
|
|
margin: 0 0 12px;
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
color: #087f57;
|
|
}
|
|
|
|
.mp-plan-form-page {
|
|
:deep(.form-grid-5),
|
|
:deep(.form-grid-4),
|
|
:deep(.form-grid-3),
|
|
:deep(.form-grid-2) {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.pl-plan-transport-row--two,
|
|
.pl-plan-transport-row--three,
|
|
.pl-plan-transport-row--four,
|
|
.pl-plan-kv-grid-party {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.pl-plan-transport-row__vehicle,
|
|
.pl-plan-transport-row__driver,
|
|
.pl-plan-transport-row__escort {
|
|
width: auto;
|
|
max-width: none;
|
|
}
|
|
|
|
.pl-plan-freight-form {
|
|
min-height: 0;
|
|
}
|
|
}
|
|
|
|
.mp-plan-form-footer {
|
|
position: sticky;
|
|
bottom: 0;
|
|
z-index: 2;
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
padding: 12px 0 calc(8px + env(safe-area-inset-bottom));
|
|
background: #f5f8f7;
|
|
}
|
|
|
|
.pl-plan-transport-row {
|
|
align-items: start;
|
|
|
|
:deep(.el-select) {
|
|
width: 100%;
|
|
}
|
|
}
|
|
|
|
.pl-plan-transport-row--two {
|
|
display: grid;
|
|
grid-template-columns: max-content minmax(0, 1fr);
|
|
gap: 12px 20px;
|
|
}
|
|
|
|
.pl-plan-transport-row--three {
|
|
display: grid;
|
|
grid-template-columns: max-content minmax(240px, 340px) minmax(280px, 1fr);
|
|
gap: 12px 16px;
|
|
}
|
|
|
|
.pl-plan-transport-row--four {
|
|
display: grid;
|
|
grid-template-columns: max-content minmax(180px, 240px) minmax(180px, 240px) minmax(240px, 1fr);
|
|
gap: 12px 16px;
|
|
}
|
|
|
|
.pl-plan-transport-row__vehicle {
|
|
width: max-content;
|
|
max-width: 100%;
|
|
}
|
|
|
|
.pl-plan-transport-row__driver,
|
|
.pl-plan-transport-row__escort {
|
|
min-width: 0;
|
|
max-width: 280px;
|
|
}
|
|
|
|
.pl-plan-transport-row__carrier {
|
|
min-width: 0;
|
|
}
|
|
|
|
.pl-plan-vehicle-with-library,
|
|
.pl-plan-carrier-with-library {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
width: 100%;
|
|
}
|
|
|
|
.pl-plan-vehicle-with-library__select {
|
|
flex: 0 0 220px;
|
|
width: 220px;
|
|
}
|
|
|
|
.pl-plan-carrier-with-library__select {
|
|
flex: 1 1 auto;
|
|
min-width: 0;
|
|
width: auto;
|
|
max-width: none;
|
|
}
|
|
|
|
.pl-plan-vehicle-with-library__btn,
|
|
.pl-plan-carrier-with-library__btn {
|
|
flex: 0 0 auto;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.pl-plan-form-body {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
}
|
|
|
|
.pl-plan-form-section {
|
|
padding: 0;
|
|
border: none;
|
|
border-radius: 0;
|
|
background: transparent;
|
|
}
|
|
|
|
.pl-plan-form-section-head {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.pl-plan-form-section-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.pl-plan-form-section-title {
|
|
margin: 0 0 12px;
|
|
padding-left: 10px;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: var(--section-bar-title-color, var(--primary-dark, #059669));
|
|
border-left: 3px solid var(--section-bar-border-color, var(--primary, #10b981));
|
|
line-height: 1;
|
|
}
|
|
|
|
.pl-plan-form-section-head .pl-plan-form-section-title {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.customer-select-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
width: 100%;
|
|
}
|
|
|
|
.customer-select-row__select {
|
|
flex: 1 1 auto;
|
|
min-width: 0;
|
|
}
|
|
|
|
.customer-select-row__search {
|
|
flex: 0 0 auto;
|
|
padding: 0 6px;
|
|
}
|
|
|
|
.customer-option-row {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
}
|
|
|
|
.customer-option-row__meta {
|
|
font-size: 12px;
|
|
color: #94a3b8;
|
|
}
|
|
|
|
.pl-plan-subtitle {
|
|
margin: 16px 0 10px;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: #1e293b;
|
|
}
|
|
|
|
.pl-plan-party-block .pl-plan-subtitle {
|
|
margin: 0 0 10px;
|
|
}
|
|
|
|
.pl-plan-party-select-row {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 12px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.pl-plan-party-select-row__customer {
|
|
flex: 1.4 1 0;
|
|
min-width: 0;
|
|
margin-bottom: 0 !important;
|
|
}
|
|
|
|
.pl-plan-party-select-row__contract {
|
|
flex: 1 1 0;
|
|
min-width: 160px;
|
|
margin-bottom: 0 !important;
|
|
}
|
|
|
|
.pl-plan-contract-trigger {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
width: 100%;
|
|
min-height: 32px;
|
|
}
|
|
|
|
.pl-plan-contract-trigger__hint {
|
|
font-size: 12px;
|
|
color: #64748b;
|
|
white-space: nowrap;
|
|
|
|
&.is-warn {
|
|
color: #d97706;
|
|
}
|
|
}
|
|
|
|
.pl-plan-form-empty-hint,
|
|
.text-muted {
|
|
margin: 0 0 8px;
|
|
font-size: 13px;
|
|
color: #64748b;
|
|
}
|
|
|
|
.pl-plan-kv-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
gap: 10px 16px;
|
|
}
|
|
|
|
.pl-plan-kv-grid-2 {
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
}
|
|
|
|
.pl-plan-kv {
|
|
min-width: 0;
|
|
}
|
|
|
|
.pl-plan-kv-span {
|
|
grid-column: 1 / -1;
|
|
}
|
|
|
|
.pl-plan-party-row,
|
|
.pl-plan-logistics-row {
|
|
display: flex;
|
|
align-items: stretch;
|
|
gap: 0;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.pl-plan-logistics-row {
|
|
margin-top: 34px;
|
|
}
|
|
|
|
.pl-plan-party-row .pl-plan-party-block,
|
|
.pl-plan-logistics-row .pl-plan-logistics-block,
|
|
.pl-plan-logistics-row .pl-plan-logistics-block-wrap {
|
|
flex: 1 1 0;
|
|
min-width: 0;
|
|
}
|
|
|
|
.pl-plan-logistics-block-wrap {
|
|
position: relative;
|
|
}
|
|
|
|
.pl-plan-logistics-swap-btn {
|
|
position: absolute;
|
|
top: -32px;
|
|
right: 0;
|
|
z-index: 1;
|
|
height: 26px;
|
|
padding: 0 4px;
|
|
}
|
|
|
|
.pl-plan-flow-bridge {
|
|
flex: 0 0 88px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 10px;
|
|
padding: 0 8px;
|
|
}
|
|
|
|
.pl-plan-flow-bridge__label {
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: var(--primary-dark, #168f45);
|
|
letter-spacing: 0.06em;
|
|
padding: 4px 10px;
|
|
border-radius: 999px;
|
|
background: #e6f8ed;
|
|
border: 1px solid #a8e0b8;
|
|
line-height: 1.2;
|
|
}
|
|
|
|
.pl-plan-flow-bridge__arrow {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 44px;
|
|
height: 44px;
|
|
border-radius: 50%;
|
|
color: var(--primary-dark, #168f45);
|
|
background: linear-gradient(145deg, #e6f8ed 0%, #d4f0de 100%);
|
|
border: 1px solid #a8e0b8;
|
|
box-shadow: 0 2px 8px rgba(28, 168, 81, 0.14);
|
|
|
|
svg {
|
|
width: 22px;
|
|
height: 22px;
|
|
display: block;
|
|
}
|
|
}
|
|
|
|
.pl-plan-kv-grid-party {
|
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
}
|
|
|
|
.pl-plan-logistics-block {
|
|
padding: 10px 12px;
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: 8px;
|
|
background: #f8fafc;
|
|
|
|
> .pl-plan-subtitle {
|
|
margin: 0 0 8px;
|
|
}
|
|
|
|
:deep(.el-form-item) {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.form-grid-2 {
|
|
gap: 8px 12px;
|
|
}
|
|
}
|
|
|
|
.pl-plan-party-block {
|
|
min-width: 0;
|
|
padding: 12px;
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: 8px;
|
|
background: #f8fafc;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.pl-plan-party-title {
|
|
margin: 0 0 10px;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: #1e293b;
|
|
}
|
|
|
|
@media (max-width: 1200px) {
|
|
.pl-plan-kv-grid,
|
|
.pl-plan-kv-grid-party {
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
}
|
|
}
|
|
|
|
@media (max-width: 1100px) {
|
|
.pl-plan-party-row,
|
|
.pl-plan-logistics-row {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.pl-plan-flow-bridge {
|
|
flex: 0 0 auto;
|
|
flex-direction: row;
|
|
gap: 12px;
|
|
padding: 8px 0;
|
|
}
|
|
|
|
.pl-plan-flow-bridge__arrow {
|
|
transform: rotate(90deg);
|
|
}
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.pl-plan-kv-grid,
|
|
.pl-plan-kv-grid-party {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
.pl-plan-freight-section-head {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: flex-start;
|
|
gap: 16px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.pl-plan-freight-section-subtitle {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: #334155;
|
|
}
|
|
|
|
.pl-plan-freight-switch {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex-shrink: 0;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.pl-plan-freight-switch-label {
|
|
font-size: 13px;
|
|
color: var(--mint-text-muted, #64748b);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.pl-plan-freight-form {
|
|
:deep(.el-form-item) {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
:deep(.el-select),
|
|
:deep(.el-input-number) {
|
|
width: 100%;
|
|
}
|
|
}
|
|
|
|
.pl-plan-freight-form {
|
|
min-height: min(720px, calc(100vh - 180px));
|
|
}
|
|
</style>
|
|
|
|
<style lang="scss">
|
|
.el-dialog.pl-plan-form-dialog.dialog-form-layout {
|
|
width: min(2000px, calc(100vw - 32px)) !important;
|
|
max-width: min(2000px, calc(100vw - 32px));
|
|
height: var(--dialog-max-height, calc(100vh - 40px));
|
|
max-height: var(--dialog-max-height, calc(100vh - 40px));
|
|
}
|
|
|
|
.el-dialog.pl-plan-form-dialog.dialog-form-layout .el-dialog__body {
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex: 1;
|
|
min-height: 0;
|
|
max-height: none;
|
|
}
|
|
|
|
.el-dialog.pl-plan-form-dialog.dialog-form-layout .pl-plan-form-body {
|
|
flex: 1;
|
|
min-height: 0;
|
|
}
|
|
</style>
|