Files
cattleTransportation/pc-cattle-transportation/src/views/shipping/createDeliveryDialog.vue
2025-11-28 17:12:36 +08:00

2603 lines
116 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>
<el-dialog
v-model="dialogVisible"
:title="formData.editId ? '编辑运送清单' : '新增运送清单'"
width="800px"
:close-on-click-modal="false"
@close="handleClose"
>
<el-form
ref="formRef"
:model="formData"
:rules="rules"
label-width="120px"
>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="关联订单" prop="orderIds">
<el-select
v-model="formData.orderIds"
placeholder="请选择订单(可多选关联订单)"
multiple
clearable
filterable
style="width: 100%"
:key="`order-select-${orderList.length}`"
@change="handleOrderChange"
>
<el-option
v-for="item in orderList"
:key="`order-${item.id}`"
:label="`订单${item.id} - 买方: ${item.buyerName || '--'} | 卖方: ${item.sellerName || '--'} | 单价: ${item.firmPrice ? (item.firmPrice + '元/斤') : '--'}`"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<!-- 已关联订单管理区域 -->
<el-row :gutter="20" v-if="formData.editId && associatedOrders.length > 0">
<el-col :span="24">
<el-form-item label="已关联订单">
<el-table :data="associatedOrders" border size="small" style="width: 100%">
<el-table-column label="订单ID" prop="id" width="100">
<template #default="scope">
{{ scope.row.id || '--' }}
</template>
</el-table-column>
<el-table-column label="买方" prop="buyerName" width="150">
<template #default="scope">
{{ scope.row.buyerName || '--' }}
</template>
</el-table-column>
<el-table-column label="卖方" prop="sellerName" width="150">
<template #default="scope">
{{ scope.row.sellerName || '--' }}
</template>
</el-table-column>
<el-table-column label="单价" prop="firmPrice" width="120">
<template #default="scope">
{{ scope.row.firmPrice ? (scope.row.firmPrice + ' 元/斤') : '--' }}
</template>
</el-table-column>
<el-table-column label="结算方式" prop="settlementTypeDesc" width="120">
<template #default="scope">
{{ scope.row.settlementTypeDesc || '--' }}
</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right">
<template #default="scope">
<el-button
link
type="danger"
size="small"
@click="unbindOrder(scope.row.id)"
:loading="unbindingOrderId === scope.row.id"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="车牌号" prop="plateNumber">
<el-select v-model="formData.plateNumber" placeholder="请选择车牌号" clearable filterable style="width: 100%">
<el-option
v-for="item in vehicleOptions"
:key="item.id"
:label="item.licensePlate"
:value="item.licensePlate"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="司机姓名" prop="driverId">
<el-select v-model="formData.driverId" placeholder="请选择司机" clearable filterable style="width: 100%" @change="handleDriverChange">
<el-option
v-for="item in driverOptions"
:key="item.id"
:label="item.username + (item.mobile ? ' - ' + item.mobile : '')"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="司机电话" prop="driverPhone">
<el-input v-model="formData.driverPhone" placeholder="请输入司机电话" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="主机设备" prop="serverId">
<el-select
v-model="formData.serverId"
placeholder="请选择主机设备"
clearable
filterable
style="width: 100%"
@change="handleServerChange"
>
<el-option
v-for="item in serverList"
:key="item.deviceId"
:label="item.deviceId + (item.name ? ' - ' + item.name : '')"
:value="item.id || item.deviceId"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="耳标设备" prop="eartagIds">
<el-select
v-model="formData.eartagIds"
placeholder="请选择耳标设备"
multiple
clearable
filterable
style="width: 100%"
@change="handleEartagChange"
>
<el-option
v-for="item in eartagList"
:key="item.deviceId"
:label="item.deviceId + (item.name ? ' - ' + item.name : '')"
:value="item.id || item.deviceId"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="项圈设备" prop="collarIds">
<el-select
v-model="formData.collarIds"
placeholder="请选择项圈设备"
multiple
clearable
filterable
style="width: 100%"
@change="handleCollarChange"
>
<el-option
v-for="item in collarList"
:key="item.deviceId"
:label="item.deviceId + (item.name ? ' - ' + item.name : '')"
:value="item.id || item.deviceId"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="预计出发时间" prop="estimatedDepartureTime">
<el-date-picker
v-model="formData.estimatedDepartureTime"
type="datetime"
placeholder="请选择预计出发时间"
style="width: 100%"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="预计到达时间" prop="estimatedArrivalTime">
<el-date-picker
v-model="formData.estimatedArrivalTime"
type="datetime"
placeholder="请选择预计到达时间"
style="width: 100%"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="起点地址" prop="startLocation">
<el-input
v-model="formData.startLocation"
placeholder="请输入或在地图上选择起点地址"
>
<template #append>
<el-button @click="openStartLocationMap">在地图上选择</el-button>
</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="目的地地址" prop="endLocation">
<el-input
v-model="formData.endLocation"
placeholder="请输入或在地图上选择目的地地址"
>
<template #append>
<el-button @click="openEndLocationMap">在地图上选择</el-button>
</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="牛只数量" prop="cattleCount">
<el-input-number
v-model="formData.cattleCount"
:min="1"
:max="9999"
placeholder="请输入牛只数量"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="司机运费" prop="freight">
<el-input-number
v-model="formData.freight"
:min="0"
:precision="2"
placeholder="请输入司机运费"
style="width: 100%"
>
<template #append></template>
</el-input-number>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="检疫证号" prop="quarantineCertNo">
<el-input v-model="formData.quarantineCertNo" placeholder="请输入检疫证号(可选)" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="空车过磅重量" prop="emptyWeight">
<el-input-number
v-model="formData.emptyWeight"
placeholder="请输入空车过磅重量kg"
:precision="2"
:min="0"
:step="0.01"
:controls-position="'right'"
style="width: 100%"
class="weight-input-number"
>
<template #append>kg</template>
</el-input-number>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="装车过磅重量" prop="entruckWeight">
<el-input-number
v-model="formData.entruckWeight"
placeholder="请输入装车过磅重量kg"
:precision="2"
:min="0"
:step="0.01"
:controls-position="'right'"
style="width: 100%"
class="weight-input-number"
>
<template #append>kg</template>
</el-input-number>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="落地过磅重量" prop="landingEntruckWeight">
<el-input-number
v-model="formData.landingEntruckWeight"
placeholder="请输入落地过磅重量kg"
:precision="2"
:min="0"
:step="0.01"
:controls-position="'right'"
style="width: 100%"
class="weight-input-number"
>
<template #append>kg</template>
</el-input-number>
</el-form-item>
</el-col>
</el-row>
<!-- 耳标设备编号及体重 -->
<el-form-item label="智能耳标编号及体重" v-if="earTagDevices.length > 0">
<el-table :data="earTagDevices" border style="width: 100%" max-height="200">
<el-table-column prop="deviceId" label="设备编号" width="200"></el-table-column>
<el-table-column prop="weight" label="体重" width="200">
<template #default="scope">
<el-input-number v-model="scope.row.weight" placeholder="请输入体重" :precision="2" style="width: 100%">
<template #append>kg</template>
</el-input-number>
</template>
</el-table-column>
</el-table>
</el-form-item>
<!-- 项圈设备编号及体重 -->
<el-form-item label="智能项圈编号及体重" v-if="collarDevices.length > 0">
<el-table :data="collarDevices" border style="width: 100%" max-height="200">
<el-table-column prop="deviceId" label="设备编号" width="200"></el-table-column>
<el-table-column prop="weight" label="体重" width="200">
<template #default="scope">
<el-input-number v-model="scope.row.weight" placeholder="请输入体重" :precision="2" style="width: 100%">
<template #append>kg</template>
</el-input-number>
</template>
</el-table-column>
</el-table>
</el-form-item>
<!-- 照片上传区域 -->
<el-divider content-position="left">
<span style="font-weight: bold; color: #409EFF;">照片上传</span>
</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="检疫票" prop="quarantineTickeyUrl" label-width="180px">
<div class="photo-upload-wrapper">
<el-upload
class="avatar-uploader"
action="/api/common/upload"
:show-file-list="false"
:on-success="handleQuarantinePhotoSuccess"
:headers="uploadHeaders"
>
<el-image
v-if="formData.quarantineTickeyUrl"
ref="quarantineImageRef"
:src="formData.quarantineTickeyUrl"
class="avatar"
fit="cover"
:preview-src-list="[formData.quarantineTickeyUrl]"
preview-teleported
preview-disabled
/>
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
<el-button
v-if="formData.quarantineTickeyUrl"
type="primary"
:icon="ZoomIn"
circle
size="small"
class="preview-btn"
@click.stop="handlePreviewPhoto('quarantineTickeyUrl')"
/>
<el-button
v-if="formData.quarantineTickeyUrl"
type="danger"
:icon="Delete"
circle
size="small"
class="delete-btn"
@click.stop="handleDeletePhoto('quarantineTickeyUrl')"
/>
</div>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="发车纸质磅单(双章)" prop="poundListImg" label-width="180px">
<div class="photo-upload-wrapper">
<el-upload
class="avatar-uploader"
action="/api/common/upload"
:show-file-list="false"
:on-success="handlePoundListPhotoSuccess"
:headers="uploadHeaders"
>
<el-image
v-if="formData.poundListImg"
ref="poundListImageRef"
:src="formData.poundListImg"
class="avatar"
fit="cover"
:preview-src-list="[formData.poundListImg]"
preview-teleported
preview-disabled
/>
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
<el-button
v-if="formData.poundListImg"
type="primary"
:icon="ZoomIn"
circle
size="small"
class="preview-btn"
@click.stop="handlePreviewPhoto('poundListImg')"
/>
<el-button
v-if="formData.poundListImg"
type="danger"
:icon="Delete"
circle
size="small"
class="delete-btn"
@click.stop="handleDeletePhoto('poundListImg')"
/>
</div>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="车辆空磅上磅车头照片" prop="emptyVehicleFrontPhoto" label-width="180px">
<div class="photo-upload-wrapper">
<el-upload
class="avatar-uploader"
action="/api/common/upload"
:show-file-list="false"
:on-success="handleEmptyVehicleFrontPhotoSuccess"
:headers="uploadHeaders"
>
<el-image
v-if="formData.emptyVehicleFrontPhoto"
ref="emptyVehicleFrontImageRef"
:src="formData.emptyVehicleFrontPhoto"
class="avatar"
fit="cover"
:preview-src-list="[formData.emptyVehicleFrontPhoto]"
preview-teleported
preview-disabled
/>
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
<el-button
v-if="formData.emptyVehicleFrontPhoto"
type="primary"
:icon="ZoomIn"
circle
size="small"
class="preview-btn"
@click.stop="handlePreviewPhoto('emptyVehicleFrontPhoto')"
/>
<el-button
v-if="formData.emptyVehicleFrontPhoto"
type="danger"
:icon="Delete"
circle
size="small"
class="delete-btn"
@click.stop="handleDeletePhoto('emptyVehicleFrontPhoto')"
/>
</div>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="车辆过重磅车头照片" prop="loadedVehicleFrontPhoto" label-width="180px">
<div class="photo-upload-wrapper">
<el-upload
class="avatar-uploader"
action="/api/common/upload"
:show-file-list="false"
:on-success="handleLoadedVehicleFrontPhotoSuccess"
:headers="uploadHeaders"
>
<el-image
v-if="formData.loadedVehicleFrontPhoto"
ref="loadedVehicleFrontImageRef"
:src="formData.loadedVehicleFrontPhoto"
class="avatar"
fit="cover"
:preview-src-list="[formData.loadedVehicleFrontPhoto]"
preview-teleported
preview-disabled
/>
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
<el-button
v-if="formData.loadedVehicleFrontPhoto"
type="primary"
:icon="ZoomIn"
circle
size="small"
class="preview-btn"
@click.stop="handlePreviewPhoto('loadedVehicleFrontPhoto')"
/>
<el-button
v-if="formData.loadedVehicleFrontPhoto"
type="danger"
:icon="Delete"
circle
size="small"
class="delete-btn"
@click.stop="handleDeletePhoto('loadedVehicleFrontPhoto')"
/>
</div>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="车辆重磅侧方照片" prop="loadedVehicleWeightPhoto" label-width="180px">
<div class="photo-upload-wrapper">
<el-upload
class="avatar-uploader"
action="/api/common/upload"
:show-file-list="false"
:on-success="handleLoadedVehicleWeightPhotoSuccess"
:headers="uploadHeaders"
>
<el-image
v-if="formData.loadedVehicleWeightPhoto"
ref="loadedVehicleWeightImageRef"
:src="formData.loadedVehicleWeightPhoto"
class="avatar"
fit="cover"
:preview-src-list="[formData.loadedVehicleWeightPhoto]"
preview-teleported
preview-disabled
/>
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
<el-button
v-if="formData.loadedVehicleWeightPhoto"
type="primary"
:icon="ZoomIn"
circle
size="small"
class="preview-btn"
@click.stop="handlePreviewPhoto('loadedVehicleWeightPhoto')"
/>
<el-button
v-if="formData.loadedVehicleWeightPhoto"
type="danger"
:icon="Delete"
circle
size="small"
class="delete-btn"
@click.stop="handleDeletePhoto('loadedVehicleWeightPhoto')"
/>
</div>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="驾驶员手持身份证站车头照片" prop="driverIdCardPhoto" label-width="180px">
<div class="photo-upload-wrapper">
<el-upload
class="avatar-uploader"
action="/api/common/upload"
:show-file-list="false"
:on-success="handleDriverIdCardPhotoSuccess"
:headers="uploadHeaders"
>
<el-image
v-if="formData.driverIdCardPhoto"
ref="driverIdCardImageRef"
:src="formData.driverIdCardPhoto"
class="avatar"
fit="cover"
:preview-src-list="[formData.driverIdCardPhoto]"
preview-teleported
preview-disabled
/>
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
<el-button
v-if="formData.driverIdCardPhoto"
type="primary"
:icon="ZoomIn"
circle
size="small"
class="preview-btn"
@click.stop="handlePreviewPhoto('driverIdCardPhoto')"
/>
<el-button
v-if="formData.driverIdCardPhoto"
type="danger"
:icon="Delete"
circle
size="small"
class="delete-btn"
@click.stop="handleDeletePhoto('driverIdCardPhoto')"
/>
</div>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="落地纸质磅单(双章)" prop="destinationPoundListImg" label-width="180px">
<div class="photo-upload-wrapper">
<el-upload
class="avatar-uploader"
action="/api/common/upload"
:show-file-list="false"
:on-success="handleDestinationPoundListPhotoSuccess"
:headers="uploadHeaders"
>
<el-image
v-if="formData.destinationPoundListImg"
ref="destinationPoundListImageRef"
:src="formData.destinationPoundListImg"
class="avatar"
fit="cover"
:preview-src-list="[formData.destinationPoundListImg]"
preview-teleported
preview-disabled
/>
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
<el-button
v-if="formData.destinationPoundListImg"
type="primary"
:icon="ZoomIn"
circle
size="small"
class="preview-btn"
@click.stop="handlePreviewPhoto('destinationPoundListImg')"
/>
<el-button
v-if="formData.destinationPoundListImg"
type="danger"
:icon="Delete"
circle
size="small"
class="delete-btn"
@click.stop="handleDeletePhoto('destinationPoundListImg')"
/>
</div>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="落地车辆过重磅车头照片" prop="destinationVehicleFrontPhoto" label-width="180px">
<div class="photo-upload-wrapper">
<el-upload
class="avatar-uploader"
action="/api/common/upload"
:show-file-list="false"
:on-success="handleDestinationVehicleFrontPhotoSuccess"
:headers="uploadHeaders"
>
<el-image
v-if="formData.destinationVehicleFrontPhoto"
ref="destinationVehicleFrontImageRef"
:src="formData.destinationVehicleFrontPhoto"
class="avatar"
fit="cover"
:preview-src-list="[formData.destinationVehicleFrontPhoto]"
preview-teleported
preview-disabled
/>
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
<el-button
v-if="formData.destinationVehicleFrontPhoto"
type="primary"
:icon="ZoomIn"
circle
size="small"
class="preview-btn"
@click.stop="handlePreviewPhoto('destinationVehicleFrontPhoto')"
/>
<el-button
v-if="formData.destinationVehicleFrontPhoto"
type="danger"
:icon="Delete"
circle
size="small"
class="delete-btn"
@click.stop="handleDeletePhoto('destinationVehicleFrontPhoto')"
/>
</div>
</el-form-item>
</el-col>
</el-row>
<!-- 视频上传区域 -->
<el-divider content-position="left">
<span style="font-weight: bold; color: #67C23A;">视频上传</span>
</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="装车过磅视频" prop="entruckWeightVideo" label-width="180px">
<el-upload
accept=".mp4,.avi,.rmvb,.mkv,.MP4,.AVI,.RMVB,.MKV"
class="upload-demo"
action="/api/common/upload"
style="width: 100%"
:headers="uploadHeaders"
:limit="1"
:on-success="handleEntruckWeightVideoSuccess"
:on-remove="() => handleRemoveVideo('entruckWeightVideo')"
>
<el-button type="primary">上传</el-button>
</el-upload>
<div v-if="formData.entruckWeightVideo" class="video-preview-wrapper">
<video :src="formData.entruckWeightVideo" style="width: 100%; max-height: 300px; margin-top: 10px" controls></video>
<el-button
type="danger"
:icon="Delete"
circle
size="small"
class="delete-btn"
@click="handleRemoveVideo('entruckWeightVideo')"
/>
</div>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="空车过磅视频" prop="emptyWeightVideo" label-width="180px">
<el-upload
accept=".mp4,.avi,.rmvb,.mkv,.MP4,.AVI,.RMVB,.MKV"
class="upload-demo"
action="/api/common/upload"
style="width: 100%"
:headers="uploadHeaders"
:limit="1"
:on-success="handleEmptyWeightVideoSuccess"
:on-remove="() => handleRemoveVideo('emptyWeightVideo')"
>
<el-button type="primary">上传</el-button>
</el-upload>
<div v-if="formData.emptyWeightVideo" class="video-preview-wrapper">
<video :src="formData.emptyWeightVideo" style="width: 100%; max-height: 300px; margin-top: 10px" controls></video>
<el-button
type="danger"
:icon="Delete"
circle
size="small"
class="delete-btn"
@click="handleRemoveVideo('emptyWeightVideo')"
/>
</div>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="装牛视频" prop="cattleLoadingVideo" label-width="180px">
<el-upload
accept=".mp4,.avi,.rmvb,.mkv,.MP4,.AVI,.RMVB,.MKV"
class="upload-demo"
action="/api/common/upload"
style="width: 100%"
:headers="uploadHeaders"
:limit="1"
:on-success="handleCattleLoadingVideoSuccess"
:on-remove="() => handleRemoveVideo('cattleLoadingVideo')"
>
<el-button type="primary">上传</el-button>
</el-upload>
<div v-if="formData.cattleLoadingVideo" class="video-preview-wrapper">
<video :src="formData.cattleLoadingVideo" style="width: 100%; max-height: 300px; margin-top: 10px" controls></video>
<el-button
type="danger"
:icon="Delete"
circle
size="small"
class="delete-btn"
@click="handleRemoveVideo('cattleLoadingVideo')"
/>
</div>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="控槽视频" prop="controlSlotVideo" label-width="180px">
<el-upload
accept=".mp4,.avi,.rmvb,.mkv,.MP4,.AVI,.RMVB,.MKV"
class="upload-demo"
action="/api/common/upload"
style="width: 100%"
:headers="uploadHeaders"
:limit="1"
:on-success="handleControlSlotVideoSuccess"
:on-remove="() => handleRemoveVideo('controlSlotVideo')"
>
<el-button type="primary">上传</el-button>
</el-upload>
<div v-if="formData.controlSlotVideo" class="video-preview-wrapper">
<video :src="formData.controlSlotVideo" style="width: 100%; max-height: 300px; margin-top: 10px" controls></video>
<el-button
type="danger"
:icon="Delete"
circle
size="small"
class="delete-btn"
@click="handleRemoveVideo('controlSlotVideo')"
/>
</div>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="装完牛绕车一圈视频" prop="cattleLoadingCircleVideo" label-width="180px">
<el-upload
accept=".mp4,.avi,.rmvb,.mkv,.MP4,.AVI,.RMVB,.MKV"
class="upload-demo"
action="/api/common/upload"
style="width: 100%"
:headers="uploadHeaders"
:limit="1"
:on-success="handleCattleLoadingCircleVideoSuccess"
:on-remove="() => handleRemoveVideo('cattleLoadingCircleVideo')"
>
<el-button type="primary">上传</el-button>
</el-upload>
<div v-if="formData.cattleLoadingCircleVideo" class="video-preview-wrapper">
<video :src="formData.cattleLoadingCircleVideo" style="width: 100%; max-height: 300px; margin-top: 10px" controls></video>
<el-button
type="danger"
:icon="Delete"
circle
size="small"
class="delete-btn"
@click="handleRemoveVideo('cattleLoadingCircleVideo')"
/>
</div>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="卸牛视频" prop="unloadCattleVideo" label-width="180px">
<el-upload
accept=".mp4,.avi,.rmvb,.mkv,.MP4,.AVI,.RMVB,.MKV"
class="upload-demo"
action="/api/common/upload"
style="width: 100%"
:headers="uploadHeaders"
:limit="1"
:on-success="handleUnloadCattleVideoSuccess"
:on-remove="() => handleRemoveVideo('unloadCattleVideo')"
>
<el-button type="primary">上传</el-button>
</el-upload>
<div v-if="formData.unloadCattleVideo" class="video-preview-wrapper">
<video :src="formData.unloadCattleVideo" style="width: 100%; max-height: 300px; margin-top: 10px" controls></video>
<el-button
type="danger"
:icon="Delete"
circle
size="small"
class="delete-btn"
@click="handleRemoveVideo('unloadCattleVideo')"
/>
</div>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="落地过磅视频" prop="destinationWeightVideo" label-width="180px">
<el-upload
accept=".mp4,.avi,.rmvb,.mkv,.MP4,.AVI,.RMVB,.MKV"
class="upload-demo"
action="/api/common/upload"
style="width: 100%"
:headers="uploadHeaders"
:limit="1"
:on-success="handleDestinationWeightVideoSuccess"
:on-remove="() => handleRemoveVideo('destinationWeightVideo')"
>
<el-button type="primary">上传</el-button>
</el-upload>
<div v-if="formData.destinationWeightVideo" class="video-preview-wrapper">
<video :src="formData.destinationWeightVideo" style="width: 100%; max-height: 300px; margin-top: 10px" controls></video>
<el-button
type="danger"
:icon="Delete"
circle
size="small"
class="delete-btn"
@click="handleRemoveVideo('destinationWeightVideo')"
/>
</div>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="备注" prop="remark" label-width="180px">
<el-input
v-model="formData.remark"
type="textarea"
:rows="3"
maxlength="500"
show-word-limit
placeholder="请输入备注信息可选"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">
提交
</el-button>
</span>
</template>
</el-dialog>
<!-- 起点地址选择地图 -->
<el-dialog v-model="showStartLocationMap" title="选择起点地址" width="900px">
<baidu-map
class="map"
:center="formData.startLon && formData.startLat ? {lng: parseFloat(formData.startLon), lat: parseFloat(formData.startLat)} : {lng: 116.404, lat: 39.915}"
:zoom="15"
:scroll-wheel-zoom="true"
@click="handleStartLocationClick"
style="height: 500px"
>
<bm-marker v-if="formData.startLon && formData.startLat" :position="{lng: parseFloat(formData.startLon), lat: parseFloat(formData.startLat)}" :dragging="true" @dragging="handleStartMarkerDrag" />
<bm-map-type :map-types="['BMAP_NORMAL_MAP', 'BMAP_HYBRID_MAP']"></bm-map-type>
</baidu-map>
<template #footer>
<span class="dialog-footer">
<el-button @click="showStartLocationMap = false">取消</el-button>
<el-button type="primary" @click="showStartLocationMap = false">确定</el-button>
</span>
</template>
</el-dialog>
<!-- 目的地地址选择地图 -->
<el-dialog v-model="showEndLocationMap" title="选择目的地地址" width="900px">
<baidu-map
class="map"
:center="formData.endLon && formData.endLat ? {lng: parseFloat(formData.endLon), lat: parseFloat(formData.endLat)} : {lng: 116.404, lat: 39.915}"
:zoom="15"
:scroll-wheel-zoom="true"
@click="handleEndLocationClick"
style="height: 500px"
>
<bm-marker v-if="formData.endLon && formData.endLat" :position="{lng: parseFloat(formData.endLon), lat: parseFloat(formData.endLat)}" :dragging="true" @dragging="handleEndMarkerDrag" />
<bm-map-type :map-types="['BMAP_NORMAL_MAP', 'BMAP_HYBRID_MAP']"></bm-map-type>
</baidu-map>
<template #footer>
<span class="dialog-footer">
<el-button @click="showEndLocationMap = false">取消</el-button>
<el-button type="primary" @click="showEndLocationMap = false">确定</el-button>
</span>
</template>
</el-dialog>
<!-- 图片预览 -->
<el-image-viewer
v-if="showImageViewer"
:url-list="imageViewerUrlList"
:initial-index="imageViewerInitialIndex"
@close="closeImageViewer"
teleported
/>
</template>
<script setup>
import { ref, reactive, computed, watch, nextTick } from 'vue';
import { ElMessage } from 'element-plus';
import { ElImageViewer } from 'element-plus';
import { Plus, UploadFilled, Delete, ZoomIn } from '@element-plus/icons-vue';
import { createDelivery, updateDeviceDeliveryId, orderPageQuery, getDeliveryDetail, updateOrderDeliveryId, getOrdersByDeliveryId, unbindOrderFromDelivery } from '@/api/shipping.js';
import * as shippingApi from '@/api/shipping.js';
import { driverList as fetchDriverList, vehicleList as fetchVehicleList } from '@/api/userManage.js';
import { iotDeviceQueryList } from '@/api/hardware.js';
import { BaiduMap, BmMapType, BmMarker } from 'vue-baidu-map-3x';
import { useUserStore } from '../../store/user';
import { useDeliveryFormStore } from '../../store/deliveryForm';
const userStore = useUserStore();
const deliveryFormStore = useDeliveryFormStore();
const uploadHeaders = reactive({ Authorization: userStore.$state.token });
const dialogVisible = ref(false);
const formRef = ref(null);
const submitLoading = ref(false);
const isRestoringFromCache = ref(false); // 标记是否正在从缓存恢复数据
const isSubmitSuccess = ref(false); // 标记是否刚刚提交成功
const serverList = ref([]);
const eartagList = ref([]);
const collarList = ref([]);
const driverOptions = ref([]);
const vehicleOptions = ref([]);
const orderList = ref([]);
const associatedOrders = ref([]); // 已关联的订单列表
const unbindingOrderId = ref(null); // 正在解除关联的订单ID
const showStartLocationMap = ref(false);
const showEndLocationMap = ref(false);
const formData = reactive({
editId: null, // 编辑时的运单ID
orderIds: [], // 关联订单ID数组多选
plateNumber: null,
driverId: null, // 司机ID后端用
driverName: null,
driverPhone: '',
serverId: null, // Integer设备表主键ID
serverDeviceId: '', // String设备编号用于设备绑定
eartagIds: [], // Integer数组设备表主键ID
collarIds: [], // Integer数组不低于主键ID
eartagDeviceIds: [], // String数组耳标设备编号用于设备绑定
collarDeviceIds: [], // String数组项圈设备编号用于设备绑定
estimatedDepartureTime: '',
estimatedArrivalTime: '',
startLocation: '',
endLocation: '',
startLat: '',
startLon: '',
endLat: '',
endLon: '',
cattleCount: 1,
freight: null, // 司机运费
quarantineCertNo: '',
remark: '',
// 装车相关字段
estimatedDeliveryTime: '',
emptyWeight: null,
entruckWeight: null,
landingEntruckWeight: null,
quarantineTickeyUrl: '',
poundListImg: '',
emptyVehicleFrontPhoto: '',
loadedVehicleFrontPhoto: '',
loadedVehicleWeightPhoto: '',
driverIdCardPhoto: '',
// 落地相关照片
destinationPoundListImg: '',
destinationVehicleFrontPhoto: '',
// 视频
entruckWeightVideo: '',
emptyWeightVideo: '',
// 新增视频
cattleLoadingVideo: '',
controlSlotVideo: '',
cattleLoadingCircleVideo: '',
unloadCattleVideo: '',
destinationWeightVideo: '',
});
// 车牌号校验(已移除正则验证,直接传递字符串)
// const validatePlateNumber = (rule, value, callback) => {
// const plateReg = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z][A-Z0-9]{5}[A-Z0-9挂学警港澳]$/;
// if (!value) {
// callback(new Error('请输入车牌号'));
// } else if (!plateReg.test(value)) {
// callback(new Error('车牌号格式不正确'));
// } else {
// callback();
// }
// };
// 手机号校验
const validatePhone = (rule, value, callback) => {
const phoneReg = /^1[3-9]\d{9}$/;
if (!value) {
callback(new Error('请输入司机电话'));
} else if (!phoneReg.test(value)) {
callback(new Error('手机号格式不正确'));
} else {
callback();
}
};
// 时间校验
const validateArrivalTime = (rule, value, callback) => {
if (!value) {
callback(new Error('请选择预计到达时间'));
} else if (formData.estimatedDepartureTime && value <= formData.estimatedDepartureTime) {
callback(new Error('预计到达时间必须晚于出发时间'));
} else {
callback();
}
};
const rules = {
orderIds: [{ required: false, message: '请选择订单', trigger: 'change' }],
plateNumber: [{ required: true, message: '请选择车牌号', trigger: 'change' }],
driverId: [{ required: true, message: '请选择司机', trigger: 'change' }],
driverPhone: [{ required: true, validator: validatePhone, trigger: 'blur' }],
estimatedDepartureTime: [{ required: true, message: '请选择预计出发时间', trigger: 'change' }],
estimatedArrivalTime: [{ required: true, validator: validateArrivalTime, trigger: 'change' }],
startLocation: [{ required: true, message: '请输入起点地址', trigger: 'blur' }],
endLocation: [{ required: true, message: '请输入目的地地址', trigger: 'blur' }],
cattleCount: [{ required: true, message: '请输入牛只数量', trigger: 'blur' }],
};
// 计算耳标设备列表(包含体重输入)
const earTagDevices = computed(() => {
return formData.eartagDeviceIds.map(deviceId => {
const device = eartagList.value.find(item => item.deviceId === deviceId);
return {
deviceId: deviceId,
weight: device?.weight || null
};
});
});
// 计算项圈设备列表(包含体重输入)
const collarDevices = computed(() => {
return formData.collarDeviceIds.map(deviceId => {
const device = collarList.value.find(item => item.deviceId === deviceId);
return {
deviceId: deviceId,
weight: device?.weight || null
};
});
});
// 完善提交数据 - 只提交DeliveryCreateDto需要的字段
const buildSubmitData = () => {
// 将司机ID转换为姓名
let driverNameStr = '';
if (formData.driverId) {
const driver = driverOptions.value.find(item => item.id === formData.driverId);
// 使用username作为司机姓名不要使用mobile
driverNameStr = driver ? (driver.username || '') : '';
}
const data = {
// 基本信息
plateNumber: formData.plateNumber,
driverId: formData.driverId, // 传递司机ID给后端
driverName: driverNameStr,
driverPhone: formData.driverPhone,
// 关联信息将订单ID数组转换为逗号分隔的字符串
orderId: formData.orderIds && formData.orderIds.length > 0 ? formData.orderIds.join(',') : null,
// 设备ID
serverId: formData.serverId,
eartagIds: formData.eartagIds,
collarIds: formData.collarIds,
// 时间
estimatedDepartureTime: formData.estimatedDepartureTime,
estimatedArrivalTime: formData.estimatedArrivalTime,
// 地址和坐标
startLocation: formData.startLocation,
startLon: formData.startLon,
startLat: formData.startLat,
endLocation: formData.endLocation,
endLon: formData.endLon,
endLat: formData.endLat,
// 重量
emptyWeight: formData.emptyWeight,
entruckWeight: formData.entruckWeight,
landingEntruckWeight: formData.landingEntruckWeight,
// 其他信息
cattleCount: formData.cattleCount,
freight: formData.freight,
quarantineCertNo: formData.quarantineCertNo,
remark: formData.remark,
// 照片字段
quarantineTickeyUrl: formData.quarantineTickeyUrl,
poundListImg: formData.poundListImg,
emptyVehicleFrontPhoto: formData.emptyVehicleFrontPhoto,
loadedVehicleFrontPhoto: formData.loadedVehicleFrontPhoto,
loadedVehicleWeightPhoto: formData.loadedVehicleWeightPhoto,
driverIdCardPhoto: formData.driverIdCardPhoto,
destinationPoundListImg: formData.destinationPoundListImg,
destinationVehicleFrontPhoto: formData.destinationVehicleFrontPhoto,
// 视频字段
entruckWeightVideo: formData.entruckWeightVideo,
emptyWeightVideo: formData.emptyWeightVideo,
cattleLoadingVideo: formData.cattleLoadingVideo,
controlSlotVideo: formData.controlSlotVideo,
cattleLoadingCircleVideo: formData.cattleLoadingCircleVideo,
unloadCattleVideo: formData.unloadCattleVideo,
destinationWeightVideo: formData.destinationWeightVideo,
};
// 检查并清空包含字符串的设备ID数组避免Integer溢出
if (data.eartagIds && data.eartagIds.length > 0 && typeof data.eartagIds[0] === 'string') {
data.eartagIds = [];
}
if (data.collarIds && data.collarIds.length > 0 && typeof data.collarIds[0] === 'string') {
data.collarIds = [];
}
// 将所有undefined值转换为空字符串确保字段被提交
// 但是对于数值类型的字段如cattleCount保留null或原值不要转换为空字符串
Object.keys(data).forEach(key => {
if (data[key] === undefined) {
// 数值类型字段保留null其他字段转换为空字符串
if (key === 'cattleCount' ||
key === 'orderIds' ||
key === 'driverId' || key === 'serverId') {
data[key] = null;
} else {
data[key] = '';
}
}
});
// 确保cattleCount不为空字符串如果是空字符串则转换为null
if (data.cattleCount === '') {
data.cattleCount = null;
}
return data;
};
// 打开弹窗(支持编辑模式)
const open = async (editData = null) => {
// 重置提交成功标志
isSubmitSuccess.value = false;
dialogVisible.value = true;
// 并行加载所有下拉列表数据
await Promise.all([
loadDeviceOptions(),
loadDriverList(),
loadVehicleList(),
loadOrderList()
]);
// 如果是编辑模式,加载已关联的订单
if (editData && editData.id) {
await loadAssociatedOrders(editData.id);
// 将已关联的订单补充到订单列表中(确保下拉框能正确显示)
// 必须在填充表单之前完成这样formData.orderIds填充时orderList中已有完整信息
if (associatedOrders.value.length > 0) {
associatedOrders.value.forEach(associatedOrder => {
// 检查订单列表中是否已存在该订单
const exists = orderList.value.find(order => order.id === associatedOrder.id);
if (!exists) {
// 如果不存在,添加到订单列表中
orderList.value.push(associatedOrder);
console.log('[ORDER-LIST] 补充订单到列表:', associatedOrder);
} else {
// 如果存在但信息不完整,更新订单信息
let updated = false;
if (!exists.buyerName && associatedOrder.buyerName) {
exists.buyerName = associatedOrder.buyerName;
updated = true;
}
if (!exists.sellerName && associatedOrder.sellerName) {
exists.sellerName = associatedOrder.sellerName;
updated = true;
}
if (!exists.firmPrice && associatedOrder.firmPrice) {
exists.firmPrice = associatedOrder.firmPrice;
updated = true;
}
if (updated) {
console.log('[ORDER-LIST] 更新订单信息:', exists);
}
}
});
}
} else {
associatedOrders.value = [];
}
// 标记开始恢复缓存避免watch触发
isRestoringFromCache.value = true;
// 如果传入了编辑数据,则填充表单(编辑模式)
if (editData) {
fillFormWithEditData(editData);
// 填充表单后再次确保orderList中有完整的订单信息使用nextTick确保DOM已更新
await nextTick();
if (formData.orderIds && formData.orderIds.length > 0) {
formData.orderIds.forEach(orderId => {
const existsInOrderList = orderList.value.find(order => order.id === orderId);
if (!existsInOrderList) {
// 从associatedOrders中查找并补充
const associatedOrder = associatedOrders.value.find(order => order.id === orderId);
if (associatedOrder) {
orderList.value.push(associatedOrder);
console.log('[AFTER-FILL] 补充订单到orderList:', associatedOrder);
}
} else {
// 如果存在但信息不完整从associatedOrders中更新
const associatedOrder = associatedOrders.value.find(order => order.id === orderId);
if (associatedOrder) {
if (!existsInOrderList.buyerName && associatedOrder.buyerName) {
existsInOrderList.buyerName = associatedOrder.buyerName;
}
if (!existsInOrderList.sellerName && associatedOrder.sellerName) {
existsInOrderList.sellerName = associatedOrder.sellerName;
}
if (!existsInOrderList.firmPrice && associatedOrder.firmPrice) {
existsInOrderList.firmPrice = associatedOrder.firmPrice;
}
console.log('[AFTER-FILL] 更新订单信息:', existsInOrderList);
}
}
});
}
// 编辑模式:尝试从缓存恢复数据(如果缓存存在,用于补充未填充的字段)
const delivery = editData.delivery || editData;
if (delivery.id) {
const cachedData = deliveryFormStore.getEditFormData(delivery.id);
if (cachedData) {
// 合并缓存数据到表单(缓存数据优先级较低,不覆盖已填充的数据)
Object.keys(cachedData).forEach(key => {
if (key !== 'editId') {
const currentValue = formData[key];
const cachedValue = cachedData[key];
// 如果当前值为空,且缓存值不为空,则使用缓存值
if ((currentValue === null || currentValue === '' ||
(Array.isArray(currentValue) && currentValue.length === 0)) &&
(cachedValue !== null && cachedValue !== '' &&
!(Array.isArray(cachedValue) && cachedValue.length === 0))) {
formData[key] = cachedValue;
}
}
});
}
}
} else {
// 新增模式:从缓存恢复数据
const cachedData = deliveryFormStore.getNewFormData();
if (cachedData) {
console.log('[CACHE] 从缓存恢复新增模式数据:', cachedData);
Object.keys(cachedData).forEach(key => {
const cachedValue = cachedData[key];
// 恢复所有非空值包括0、false等有效值
if (cachedValue !== null && cachedValue !== '' &&
!(Array.isArray(cachedValue) && cachedValue.length === 0)) {
// 确保数组类型正确
if (Array.isArray(cachedValue) && Array.isArray(formData[key])) {
formData[key] = [...cachedValue];
} else {
formData[key] = cachedValue;
}
console.log(`[CACHE] 恢复字段 ${key}:`, cachedValue);
}
});
} else {
console.log('[CACHE] 没有找到新增模式的缓存数据');
}
}
// 恢复完成允许watch触发
setTimeout(() => {
isRestoringFromCache.value = false;
console.log('[CACHE] 缓存恢复完成watch 已启用');
}, 100);
};
// 填充编辑数据到表单
const fillFormWithEditData = (editData) => {
// editData 包含两个部分:
// 1. editData.delivery - 运单基本信息
// 2. editData 的根级字段 - supplierId, buyerId, eartagIds, collarIds, serverIds
const delivery = editData.delivery || editData; // 兼容两种数据结构
// 基本信息将逗号分隔的订单ID字符串转换为数组
if (delivery.orderId) {
let orderIdArray = [];
if (typeof delivery.orderId === 'string') {
// 如果是字符串,按逗号分割并转换为整数数组
orderIdArray = delivery.orderId.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id));
} else if (Array.isArray(delivery.orderId)) {
// 如果已经是数组,确保转换为整数数组
orderIdArray = delivery.orderId.map(id => {
if (typeof id === 'number') {
return id;
} else if (typeof id === 'string') {
const parsed = parseInt(id);
return isNaN(parsed) ? null : parsed;
}
return null;
}).filter(id => id !== null);
} else {
// 如果是单个数字,转换为数组
const id = typeof delivery.orderId === 'number' ? delivery.orderId : parseInt(delivery.orderId);
orderIdArray = isNaN(id) ? [] : [id];
}
formData.orderIds = orderIdArray;
// 确保orderList中包含这些订单的完整信息
// 如果orderList中没有对应的订单从associatedOrders中查找并补充
orderIdArray.forEach(orderId => {
const existsInOrderList = orderList.value.find(order => order.id === orderId);
if (!existsInOrderList) {
// 从associatedOrders中查找
const associatedOrder = associatedOrders.value.find(order => order.id === orderId);
if (associatedOrder) {
orderList.value.push(associatedOrder);
console.log('[FILL-FORM] 从associatedOrders补充订单到orderList:', associatedOrder);
} else {
console.warn('[FILL-FORM] 订单ID', orderId, '在orderList和associatedOrders中都不存在');
}
} else {
// 如果存在但信息不完整尝试从associatedOrders中更新
const associatedOrder = associatedOrders.value.find(order => order.id === orderId);
if (associatedOrder) {
if (!existsInOrderList.buyerName && associatedOrder.buyerName) {
existsInOrderList.buyerName = associatedOrder.buyerName;
}
if (!existsInOrderList.sellerName && associatedOrder.sellerName) {
existsInOrderList.sellerName = associatedOrder.sellerName;
}
if (!existsInOrderList.firmPrice && associatedOrder.firmPrice) {
existsInOrderList.firmPrice = associatedOrder.firmPrice;
}
}
}
});
} else {
formData.orderIds = [];
}
// 车牌号
formData.plateNumber = delivery.licensePlate || '';
// 检查车牌号是否在车辆列表中
const vehicleExists = vehicleOptions.value.find(v => v.licensePlate === formData.plateNumber);
if (!vehicleExists && formData.plateNumber) {
console.warn('[EDIT-FILL] ⚠️ 车牌号在车辆列表中不存在:', formData.plateNumber);
}
formData.driverId = delivery.driverId || null;
formData.driverPhone = delivery.driverMobile || '';
// 设备信息:从根级读取
if (editData.serverIds && editData.serverIds !== '') {
formData.serverId = editData.serverIds;
}
if (editData.eartagIds && Array.isArray(editData.eartagIds) && editData.eartagIds.length > 0) {
formData.eartagIds = editData.eartagIds;
}
if (editData.collarIds && Array.isArray(editData.collarIds) && editData.collarIds.length > 0) {
formData.collarIds = editData.collarIds;
}
// 地址和坐标
formData.startLocation = delivery.startLocation || '';
formData.startLat = delivery.startLat || '';
formData.startLon = delivery.startLon || '';
formData.endLocation = delivery.endLocation || '';
formData.endLat = delivery.endLat || '';
formData.endLon = delivery.endLon || '';
// 时间字段映射
// estimatedDepartureTime预计出发时间
if (delivery.estimatedDepartureTime) {
formData.estimatedDepartureTime = delivery.estimatedDepartureTime;
} else if (delivery.createTime) {
// 如果没有预计出发时间,使用创建时间作为默认值
formData.estimatedDepartureTime = delivery.createTime;
} else {
formData.estimatedDepartureTime = '';
}
// estimatedDeliveryTime 对应 estimatedArrivalTime预计送达时间
if (delivery.estimatedDeliveryTime) {
formData.estimatedArrivalTime = delivery.estimatedDeliveryTime;
} else {
formData.estimatedArrivalTime = '';
}
// 重量信息
formData.emptyWeight = delivery.emptyWeight || null;
formData.entruckWeight = delivery.entruckWeight || null;
formData.landingEntruckWeight = delivery.landingEntruckWeight || null;
// 牛只数量从ratedQuantity字段映射
formData.cattleCount = delivery.ratedQuantity || 1;
// 司机运费
formData.freight = delivery.freight || null;
// 检疫证号从quarantineTickey字段读取
// 支持多种可能的数据结构和字段名
// 1. 从 delivery 对象中读取
// 2. 从 editData 根级读取(如果 delivery 中没有)
// 3. 支持多种字段名quarantineTickey、quarantine_tickey、quarantineCertNo
const quarantineValue = delivery.quarantineTickey
|| delivery.quarantine_tickey
|| delivery.quarantineCertNo
|| (editData && editData.quarantineTickey)
|| (editData && editData.quarantine_tickey)
|| (editData && editData.quarantineCertNo)
|| '';
formData.quarantineCertNo = quarantineValue;
console.log('[FILL-FORM] 检疫证号填充:', {
'delivery.quarantineTickey': delivery.quarantineTickey,
'delivery.quarantine_tickey': delivery.quarantine_tickey,
'delivery.quarantineCertNo': delivery.quarantineCertNo,
'editData.quarantineTickey': editData?.quarantineTickey,
'editData.quarantine_tickey': editData?.quarantine_tickey,
'editData.quarantineCertNo': editData?.quarantineCertNo,
'finalValue': formData.quarantineCertNo,
'delivery对象': delivery
});
// 照片
formData.quarantineTickeyUrl = delivery.quarantineTickeyUrl || '';
formData.poundListImg = delivery.poundListImg || '';
formData.emptyVehicleFrontPhoto = delivery.emptyVehicleFrontPhoto || '';
formData.loadedVehicleFrontPhoto = delivery.loadedVehicleFrontPhoto || '';
formData.loadedVehicleWeightPhoto = delivery.loadedVehicleWeightPhoto || '';
formData.driverIdCardPhoto = delivery.driverIdCardPhoto || '';
formData.destinationPoundListImg = delivery.destinationPoundListImg || '';
formData.destinationVehicleFrontPhoto = delivery.destinationVehicleFrontPhoto || '';
// 视频
formData.entruckWeightVideo = delivery.entruckWeightVideo || '';
formData.emptyWeightVideo = delivery.emptyWeightVideo || '';
formData.cattleLoadingVideo = delivery.cattleLoadingVideo || '';
formData.controlSlotVideo = delivery.controlSlotVideo || '';
formData.cattleLoadingCircleVideo = delivery.cattleLoadingCircleVideo || '';
formData.unloadCattleVideo = delivery.unloadCattleVideo || '';
formData.destinationWeightVideo = delivery.destinationWeightVideo || '';
// 保存编辑的ID用于区分是新增还是编辑
formData.editId = delivery.id;
ElMessage.success('已加载运单数据');
};
// 加载设备选项
const loadDeviceOptions = async () => {
try {
// 统一使用 iotDeviceQueryList 接口,通过 type 参数区分设备类型
// type: 1-主机, 2-耳标, 4-项圈
// allotType: "unassigned" 表示只查询未绑定的设备delivery_id为空
const [serverRes, eartagRes, collarRes] = await Promise.all([
iotDeviceQueryList({ pageNum: 1, pageSize: 9999, type: 1, allotType: 'unassigned' }), // 主机
iotDeviceQueryList({ pageNum: 1, pageSize: 9999, type: 2, allotType: 'unassigned' }), // 耳标
iotDeviceQueryList({ pageNum: 1, pageSize: 9999, type: 4, allotType: 'unassigned' }) // 项圈
]);
if (serverRes.code === 200) {
// 过滤出主机类型设备 (type === 1)
const allServers = serverRes.data?.rows || serverRes.data || [];
serverList.value = allServers
.filter(item => item.type === 1 || item.type === '1')
.map((item, idx) => ({
// 确保有数值型 id若后端未提供 id则使用索引占位
id: item.id ?? idx + 1,
deviceId: item.deviceId,
name: item.name,
type: item.type,
}));
}
if (eartagRes.code === 200) {
// 过滤出耳标类型设备 (type === 2)
const allEartags = eartagRes.data?.rows || eartagRes.data || [];
eartagList.value = allEartags
.filter(item => item.type === 2 || item.type === '2')
.map((item, idx) => ({
id: item.id ?? idx + 1000,
deviceId: item.deviceId,
name: item.name,
type: item.type,
}));
}
if (collarRes.code === 200) {
// 过滤出项圈类型设备 (type === 4)
const allCollars = collarRes.data?.rows || collarRes.data || [];
collarList.value = allCollars
.filter(item => item.type === 4 || item.type === '4')
.map((item, idx) => ({
id: item.id ?? idx + 2000,
deviceId: item.deviceId,
name: item.name,
type: item.type,
}));
}
} catch (error) {
console.error('加载设备列表失败', error);
}
};
// 加载司机列表
const loadDriverList = async () => {
try {
const res = await fetchDriverList({ pageNum: 1, pageSize: 9999 });
if (res.code === 200) {
driverOptions.value = res.data?.rows || res.data || [];
}
} catch (error) {
console.error('加载司机列表失败', error);
}
};
// 加载车辆列表
const loadVehicleList = async () => {
try {
const res = await fetchVehicleList({ pageNum: 1, pageSize: 9999 });
if (res.code === 200) {
vehicleOptions.value = res.data?.data?.rows || res.data?.rows || res.data || [];
}
} catch (error) {
console.error('加载车辆列表失败', error);
}
};
// 加载订单列表
const loadOrderList = async () => {
try {
// 兼容运行时找不到函数引用的问题,优先从命名空间调用
const res = await (orderPageQuery ? orderPageQuery({ pageNum: 1, pageSize: 1000 }) : shippingApi.orderPageQuery({ pageNum: 1, pageSize: 1000 }));
if (res.code === 200) {
const responseData = res.data || res;
if (responseData && typeof responseData === 'object' && 'rows' in responseData) {
orderList.value = responseData.rows || [];
} else if (responseData && responseData.data && 'rows' in responseData.data) {
orderList.value = responseData.data.rows || [];
} else {
orderList.value = [];
}
console.log('[ORDER-LIST] 加载订单列表成功,数量:', orderList.value.length);
if (orderList.value.length > 0) {
console.log('[ORDER-LIST] 第一个订单示例:', orderList.value[0]);
}
}
} catch (error) {
console.error('加载订单列表失败', error);
}
};
// 订单选择变化时的处理(多选模式)
const handleOrderChange = async (orderIds) => {
// 多选模式下orderIds 是数组
if (orderIds && orderIds.length > 0) {
console.log('已选择订单:', orderIds);
}
};
// 加载已关联的订单列表
const loadAssociatedOrders = async (deliveryId) => {
try {
const res = await getOrdersByDeliveryId(deliveryId);
if (res.code === 200) {
const responseData = res.data || res;
if (responseData && typeof responseData === 'object' && 'rows' in responseData) {
associatedOrders.value = responseData.rows || [];
} else if (responseData && responseData.data && 'rows' in responseData.data) {
associatedOrders.value = responseData.data.rows || [];
} else {
associatedOrders.value = [];
}
console.log('[ASSOCIATED-ORDERS] 加载已关联订单成功,数量:', associatedOrders.value.length);
if (associatedOrders.value.length > 0) {
console.log('[ASSOCIATED-ORDERS] 第一个订单示例:', associatedOrders.value[0]);
console.log('[ASSOCIATED-ORDERS] 订单信息检查:', {
id: associatedOrders.value[0].id,
buyerName: associatedOrders.value[0].buyerName,
sellerName: associatedOrders.value[0].sellerName,
firmPrice: associatedOrders.value[0].firmPrice
});
}
} else {
associatedOrders.value = [];
}
} catch (error) {
console.error('加载已关联订单失败:', error);
associatedOrders.value = [];
}
};
// 解除订单关联
const unbindOrder = async (orderId) => {
try {
await ElMessageBox.confirm('确定要解除该订单与运送清单的关联吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
});
unbindingOrderId.value = orderId;
const res = await unbindOrderFromDelivery(orderId);
unbindingOrderId.value = null;
if (res.code === 200) {
ElMessage.success('解除关联成功');
// 刷新已关联订单列表
if (formData.editId) {
await loadAssociatedOrders(formData.editId);
}
} else {
ElMessage.error(res.msg || '解除关联失败');
}
} catch (error) {
unbindingOrderId.value = null;
if (error !== 'cancel') {
console.error('解除订单关联失败:', error);
ElMessage.error('解除关联失败,请重试');
}
}
};
// 司机选择变化时自动填充电话
const handleDriverChange = (driverId) => {
if (driverId) {
formData.driverId = driverId; // 保存司机ID用于后端查询
const driver = driverOptions.value.find(item => item.id === driverId);
if (driver && driver.mobile) {
formData.driverPhone = driver.mobile;
ElMessage.success('已自动填充司机手机号');
} else {
}
} else {
formData.driverId = null;
formData.driverPhone = '';
}
};
// 选择主机设备时记录设备编号deviceId到 serverDeviceId避免后端 Integer 溢出
const handleServerChange = (serverId) => {
const selected = serverList.value.find(item => (item.id === serverId) || (item.deviceId === serverId));
formData.serverDeviceId = selected ? selected.deviceId : '';
};
// 选择耳标设备变化时,记录设备编号数组
const handleEartagChange = (ids) => {
if (ids && ids.length > 0) {
formData.eartagDeviceIds = ids.map(id => {
const item = eartagList.value.find(dev => dev.id === id || dev.deviceId === id);
return item ? item.deviceId : id;
});
} else {
formData.eartagDeviceIds = [];
}
};
// 选择项圈设备变化时,记录设备编号数组
const handleCollarChange = (ids) => {
if (ids && ids.length > 0) {
formData.collarDeviceIds = ids.map(id => {
const item = collarList.value.find(dev => dev.id === id || dev.deviceId === id);
return item ? item.deviceId : id;
});
} else {
formData.collarDeviceIds = [];
}
};
// 更新选中设备的delivery_id和car_number
const updateSelectedDevicesDeliveryId = async (deliveryId) => {
try {
// 先查询运送清单详情获取ID和车牌号
let licensePlate = formData.plateNumber; // 默认使用表单中的车牌号
let actualDeliveryId = deliveryId; // 默认使用传入的deliveryId
try {
const detailRes = await getDeliveryDetail(deliveryId);
if (detailRes.code === 200 && detailRes.data) {
// 从查询结果中获取licensePlate和id
actualDeliveryId = detailRes.data.id || deliveryId;
licensePlate = detailRes.data.licensePlate || formData.plateNumber;
console.log('[DEVICE-BIND] 查询运送清单详情成功:', {
id: actualDeliveryId,
licensePlate: licensePlate
});
} else {
console.warn('[DEVICE-BIND] 查询运送清单详情失败,使用表单数据作为备选');
}
} catch (queryError) {
console.error('[DEVICE-BIND] 查询运送清单详情异常:', queryError);
// 查询失败时使用表单数据作为备选
}
const devicesToUpdate = [];
// 收集所有选中的设备
if (formData.serverDeviceId) {
devicesToUpdate.push(formData.serverDeviceId);
}
if (formData.eartagDeviceIds && formData.eartagDeviceIds.length > 0) {
devicesToUpdate.push(...formData.eartagDeviceIds);
}
if (formData.collarDeviceIds && formData.collarDeviceIds.length > 0) {
devicesToUpdate.push(...formData.collarDeviceIds);
}
// 批量更新设备的delivery_id和car_number
for (const deviceId of devicesToUpdate) {
await updateDeviceDeliveryId({
deviceId: deviceId,
deliveryId: actualDeliveryId,
carNumber: licensePlate // 使用查询到的车牌号
});
}
console.log('[DEVICE-BIND] 成功更新', devicesToUpdate.length, '个设备的绑定信息');
} catch (error) {
console.error('更新设备delivery_id和car_number失败:', error);
// 不阻止流程,只记录错误
}
};
// 提交表单
const handleSubmit = () => {
formRef.value.validate(async (valid) => {
if (valid) {
// 验证地址经纬度
if (!formData.startLat || !formData.startLon) {
ElMessage.warning('请在地图上选择起点位置');
return;
}
if (!formData.endLat || !formData.endLon) {
ElMessage.warning('请在地图上选择目的地位置');
return;
}
submitLoading.value = true;
try {
// 提交前详细日志
console.group('[CREATE-DELIVERY] 提交前检查');
try {
const formSnapshot = JSON.parse(JSON.stringify(formData));
} catch (e) {
console.warn('表单快照序列化失败:', e);
}
const submitData = buildSubmitData();
console.table(Object.keys(submitData).map(k => ({ key: k, type: typeof submitData[k], value: Array.isArray(submitData[k]) ? `Array(len=${submitData[k].length})` : submitData[k] })));
if (submitData.eartagIds && submitData.eartagIds.some(v => typeof v === 'string')) {
console.warn('eartagIds 仍包含字符串,将被后端拒绝:', submitData.eartagIds);
}
if (submitData.collarIds && submitData.collarIds.some(v => typeof v === 'string')) {
console.warn('collarIds 仍包含字符串,将被后端拒绝:', submitData.collarIds);
}
console.groupEnd();
let res;
// 判断是编辑还是新增
if (formData.editId) {
// 编辑模式:调用更新接口
// 将cattleCount映射为ratedQuantity后端DeliveryEditDto使用ratedQuantity字段
submitData.ratedQuantity = submitData.cattleCount;
submitData.deliveryId = formData.editId; // 添加deliveryId字段后端需要
// 将plateNumber映射为licensePlate后端DeliveryEditDto使用licensePlate字段
if (submitData.plateNumber !== undefined) {
submitData.licensePlate = submitData.plateNumber;
delete submitData.plateNumber; // 删除plateNumber字段避免混淆
}
res = await shippingApi.updateDeliveryInfo(submitData);
} else {
// 新增模式:调用创建接口
res = await createDelivery(submitData);
}
console.group(formData.editId ? '[EDIT-DELIVERY] 响应日志' : '[CREATE-DELIVERY] 响应日志');
console.groupEnd();
if (res.code === 200) {
// 获取运送清单ID新增返回data.id编辑直接用editId
let deliveryId = formData.editId || res.data?.id;
// 如果res.data是Delivery对象从对象中获取id
if (!deliveryId && res.data && typeof res.data === 'object' && res.data.id) {
deliveryId = res.data.id;
}
// 确保deliveryId是整数
if (deliveryId) {
if (typeof deliveryId === 'string') {
// 如果是逗号分隔的字符串,取第一个(这种情况不应该发生,但做保护)
if (deliveryId.includes(',')) {
console.warn('[CREATE-DELIVERY] deliveryId包含逗号可能是orderId:', deliveryId);
deliveryId = deliveryId.split(',')[0];
}
const parsed = parseInt(deliveryId);
if (isNaN(parsed)) {
console.error('[CREATE-DELIVERY] deliveryId格式不正确:', deliveryId);
ElMessage.error('获取运送清单ID失败');
return;
}
deliveryId = parsed;
} else if (typeof deliveryId !== 'number') {
deliveryId = parseInt(deliveryId);
if (isNaN(deliveryId)) {
console.error('[CREATE-DELIVERY] deliveryId格式不正确:', deliveryId);
ElMessage.error('获取运送清单ID失败');
return;
}
}
console.log('[CREATE-DELIVERY] 获取到的deliveryId:', deliveryId, '类型:', typeof deliveryId);
}
if (deliveryId) {
// 更新设备的delivery_id
await updateSelectedDevicesDeliveryId(deliveryId);
// 更新关联订单的delivery_id
if (formData.orderIds && formData.orderIds.length > 0) {
try {
// 确保orderIds是整数数组而不是字符串数组
const orderIdsArray = formData.orderIds.map(id => {
if (typeof id === 'string') {
const parsed = parseInt(id);
return isNaN(parsed) ? null : parsed;
}
return typeof id === 'number' ? id : parseInt(id);
}).filter(id => id !== null);
console.log('[ORDER-UPDATE] 准备更新订单关联, deliveryId:', deliveryId, 'orderIds:', orderIdsArray);
const updateOrderRes = await updateOrderDeliveryId({
deliveryId: deliveryId,
orderIds: orderIdsArray
});
if (updateOrderRes.code === 200) {
console.log('[ORDER-UPDATE] 成功更新订单delivery_id订单数量', formData.orderIds.length);
} else {
console.warn('[ORDER-UPDATE] 更新订单delivery_id失败', updateOrderRes.msg);
}
} catch (orderError) {
console.error('[ORDER-UPDATE] 更新订单delivery_id异常', orderError);
// 不阻止流程,只记录错误
}
}
}
// 提交成功后立即清除对应模式的缓存
try {
if (formData.editId) {
// 编辑模式:清除该运单的缓存
console.log('[CACHE] 提交成功,清除编辑模式缓存, editId:', formData.editId);
deliveryFormStore.clearEditFormData(formData.editId);
} else {
// 新增模式:清除新增缓存
console.log('[CACHE] 提交成功,清除新增模式缓存');
deliveryFormStore.clearNewFormData();
}
// 标记提交成功,避免关闭对话框时再次保存缓存
isSubmitSuccess.value = true;
} catch (cacheError) {
console.error('[CACHE] 清除缓存失败:', cacheError);
// 即使清除缓存失败,也不影响提交成功的流程
}
ElMessage.success(formData.editId ? '更新成功' : '创建成功');
dialogVisible.value = false;
emit('success');
} else {
ElMessage.error(res.msg || (formData.editId ? '更新失败' : '创建失败'));
}
} catch (error) {
console.group('[CREATE-DELIVERY] 异常日志');
console.error('创建失败 error 对象:', error);
if (error?.response) {
console.error('后端响应状态码:', error.response.status);
console.error('后端响应体:', error.response.data);
}
if (error?.config) {
console.error('请求URL:', error.config.url);
console.error('请求方法:', error.config.method);
console.error('请求头:', error.config.headers);
try {
console.error('请求数据:', typeof error.config.data === 'string' ? error.config.data : JSON.stringify(error.config.data));
} catch (e) {}
}
console.groupEnd();
ElMessage.error('创建失败,请稍后重试');
} finally {
submitLoading.value = false;
}
}
});
};
// 地图点击事件 - 起点
const handleStartLocationClick = (e) => {
formData.startLon = e.point.lng;
formData.startLat = e.point.lat;
// 反向地理编码获取地址
const geocoder = new window.BMap.Geocoder();
geocoder.getLocation(e.point, (res) => {
if (res) {
formData.startLocation = res.address;
ElMessage.success('已设置起点地址');
}
});
};
// 起点标记拖拽事件
const handleStartMarkerDrag = (e) => {
formData.startLon = e.point.lng;
formData.startLat = e.point.lat;
const geocoder = new window.BMap.Geocoder();
geocoder.getLocation(e.point, (res) => {
if (res) {
formData.startLocation = res.address;
}
});
};
// 打开起点地图并处理地址搜索
const openStartLocationMap = () => {
// 如果输入框有地址,先进行地理编码
if (formData.startLocation && formData.startLocation.trim()) {
// 先打开地图对话框,让地图组件加载
showStartLocationMap.value = true;
// 等待地图加载后再进行地理编码
setTimeout(() => {
// 在 vue-baidu-map-3x 中BMap 是通过组件访问的
// 需要获取地图实例并使用地理编码
if (window.BMap && window.BMap.Geocoder) {
const geocoder = new window.BMap.Geocoder();
geocoder.getPoint(formData.startLocation, (point) => {
if (point) {
// 搜索到坐标,更新地图中心点和标记
formData.startLon = point.lng;
formData.startLat = point.lat;
// 更新地图中心点
ElMessage.success('已定位到该地址');
} else {
ElMessage.warning('未找到该地址,请在地图上手动选择');
}
});
}
}, 500);
} else {
showStartLocationMap.value = true;
}
};
// 打开目的地地图并处理地址搜索
const openEndLocationMap = () => {
// 如果输入框有地址,先进行地理编码
if (formData.endLocation && formData.endLocation.trim()) {
// 先打开地图对话框,让地图组件加载
showEndLocationMap.value = true;
// 等待地图加载后再进行地理编码
setTimeout(() => {
if (window.BMap && window.BMap.Geocoder) {
const geocoder = new window.BMap.Geocoder();
geocoder.getPoint(formData.endLocation, (point) => {
if (point) {
// 搜索到坐标,更新地图中心点和标记
formData.endLon = point.lng;
formData.endLat = point.lat;
// 更新地图中心点
ElMessage.success('已定位到该地址');
} else {
ElMessage.warning('未找到该地址,请在地图上手动选择');
}
});
}
}, 500);
} else {
showEndLocationMap.value = true;
}
};
const handleEndLocationClick = (e) => {
formData.endLon = e.point.lng;
formData.endLat = e.point.lat;
// 反向地理编码获取地址
const geocoder = new window.BMap.Geocoder();
geocoder.getLocation(e.point, (res) => {
if (res) {
formData.endLocation = res.address;
ElMessage.success('已设置目的地地址');
}
});
};
// 目的地标记拖拽事件
const handleEndMarkerDrag = (e) => {
formData.endLon = e.point.lng;
formData.endLat = e.point.lat;
const geocoder = new window.BMap.Geocoder();
geocoder.getLocation(e.point, (res) => {
if (res) {
formData.endLocation = res.address;
}
});
};
// 通用从响应中提取URL兼容多种响应结构
const resolveUploadUrl = (res) => {
if (!res) return '';
// 优先级顺序url > data.src > data.url > data.fileUrl
return res.url || res.data?.src || res.data?.url || res.data?.fileUrl || '';
};
// 通用:创建成功回调
const makeUploadSuccessSetter = (key) => (response) => {
const url = resolveUploadUrl(response);
if (response?.code === 200 && url) {
formData[key] = url;
ElMessage.success('上传成功');
} else {
console.warn(`[UPLOAD] 未识别的响应结构:`, response);
ElMessage.error(response?.msg || '上传失败');
}
};
// 照片上传成功回调(统一使用工厂函数)
const handleQuarantinePhotoSuccess = makeUploadSuccessSetter('quarantineTickeyUrl');
const handlePoundListPhotoSuccess = makeUploadSuccessSetter('poundListImg');
const handleEmptyVehicleFrontPhotoSuccess = makeUploadSuccessSetter('emptyVehicleFrontPhoto');
const handleLoadedVehicleFrontPhotoSuccess = makeUploadSuccessSetter('loadedVehicleFrontPhoto');
const handleLoadedVehicleWeightPhotoSuccess = makeUploadSuccessSetter('loadedVehicleWeightPhoto');
const handleDriverIdCardPhotoSuccess = makeUploadSuccessSetter('driverIdCardPhoto');
const handleDestinationPoundListPhotoSuccess = makeUploadSuccessSetter('destinationPoundListImg');
const handleDestinationVehicleFrontPhotoSuccess = makeUploadSuccessSetter('destinationVehicleFrontPhoto');
// 视频上传成功回调(统一使用工厂函数)
const handleEntruckWeightVideoSuccess = makeUploadSuccessSetter('entruckWeightVideo');
const handleEmptyWeightVideoSuccess = makeUploadSuccessSetter('emptyWeightVideo');
const handleCattleLoadingVideoSuccess = makeUploadSuccessSetter('cattleLoadingVideo');
const handleControlSlotVideoSuccess = makeUploadSuccessSetter('controlSlotVideo');
const handleCattleLoadingCircleVideoSuccess = makeUploadSuccessSetter('cattleLoadingCircleVideo');
const handleUnloadCattleVideoSuccess = makeUploadSuccessSetter('unloadCattleVideo');
const handleDestinationWeightVideoSuccess = makeUploadSuccessSetter('destinationWeightVideo');
// 失败回调(可选)
const handleUploadError = (err) => {
console.error('[UPLOAD] 失败:', err);
ElMessage.error('上传失败');
};
// 照片删除回调
const handleDeletePhoto = (key) => {
if (formData.hasOwnProperty(key)) {
formData[key] = '';
ElMessage.success('已删除照片');
}
};
// 图片预览相关
const showImageViewer = ref(false);
const imageViewerUrlList = ref([]);
const imageViewerInitialIndex = ref(0);
// 照片预览回调
const handlePreviewPhoto = (key) => {
// 根据key找到对应的图片URL
const urlMap = {
'quarantineTickeyUrl': formData.quarantineTickeyUrl,
'poundListImg': formData.poundListImg,
'emptyVehicleFrontPhoto': formData.emptyVehicleFrontPhoto,
'loadedVehicleFrontPhoto': formData.loadedVehicleFrontPhoto,
'loadedVehicleWeightPhoto': formData.loadedVehicleWeightPhoto,
'driverIdCardPhoto': formData.driverIdCardPhoto,
'destinationPoundListImg': formData.destinationPoundListImg,
'destinationVehicleFrontPhoto': formData.destinationVehicleFrontPhoto
};
const imageUrl = urlMap[key];
if (imageUrl) {
imageViewerUrlList.value = [imageUrl];
imageViewerInitialIndex.value = 0;
showImageViewer.value = true;
}
};
// 关闭图片预览
const closeImageViewer = () => {
showImageViewer.value = false;
imageViewerUrlList.value = [];
};
// 视频移除回调
const handleRemoveVideo = (key) => {
if (formData.hasOwnProperty(key)) {
formData[key] = '';
ElMessage.success('已删除视频');
}
};
// 关闭弹窗
const handleClose = () => {
// 清除防抖定时器,立即保存
if (watchTimer) {
clearTimeout(watchTimer);
watchTimer = null;
}
// 如果刚刚提交成功,不保存缓存(缓存已在提交成功时清除)
if (isSubmitSuccess.value) {
console.log('[CACHE] 提交成功后的关闭,不保存缓存');
isSubmitSuccess.value = false; // 重置标志
} else {
// 关闭前立即保存当前表单数据到缓存(新增和编辑模式都保存)
// 不等待防抖,确保数据不丢失
try {
console.log('[CACHE] 关闭对话框,立即保存数据, editId:', formData.editId);
if (formData.editId) {
// 编辑模式:保存到 editFormData[editId]
deliveryFormStore.saveEditFormData(formData.editId, {
editId: formData.editId,
orderIds: formData.orderIds,
plateNumber: formData.plateNumber,
driverId: formData.driverId,
driverPhone: formData.driverPhone,
serverId: formData.serverId,
serverDeviceId: formData.serverDeviceId,
eartagIds: formData.eartagIds,
collarIds: formData.collarIds,
eartagDeviceIds: formData.eartagDeviceIds,
collarDeviceIds: formData.collarDeviceIds,
estimatedDepartureTime: formData.estimatedDepartureTime,
estimatedArrivalTime: formData.estimatedArrivalTime,
startLocation: formData.startLocation,
endLocation: formData.endLocation,
startLat: formData.startLat,
startLon: formData.startLon,
endLat: formData.endLat,
endLon: formData.endLon,
cattleCount: formData.cattleCount,
freight: formData.freight,
quarantineCertNo: formData.quarantineCertNo,
remark: formData.remark,
emptyWeight: formData.emptyWeight,
entruckWeight: formData.entruckWeight,
landingEntruckWeight: formData.landingEntruckWeight,
quarantineTickeyUrl: formData.quarantineTickeyUrl,
poundListImg: formData.poundListImg,
emptyVehicleFrontPhoto: formData.emptyVehicleFrontPhoto,
loadedVehicleFrontPhoto: formData.loadedVehicleFrontPhoto,
loadedVehicleWeightPhoto: formData.loadedVehicleWeightPhoto,
driverIdCardPhoto: formData.driverIdCardPhoto,
destinationPoundListImg: formData.destinationPoundListImg,
destinationVehicleFrontPhoto: formData.destinationVehicleFrontPhoto,
entruckWeightVideo: formData.entruckWeightVideo,
emptyWeightVideo: formData.emptyWeightVideo,
cattleLoadingVideo: formData.cattleLoadingVideo,
controlSlotVideo: formData.controlSlotVideo,
cattleLoadingCircleVideo: formData.cattleLoadingCircleVideo,
unloadCattleVideo: formData.unloadCattleVideo,
destinationWeightVideo: formData.destinationWeightVideo,
});
} else {
// 新增模式:保存到 newFormData
deliveryFormStore.saveNewFormData({
orderIds: formData.orderIds,
plateNumber: formData.plateNumber,
driverId: formData.driverId,
driverPhone: formData.driverPhone,
serverId: formData.serverId,
serverDeviceId: formData.serverDeviceId,
eartagIds: formData.eartagIds,
collarIds: formData.collarIds,
eartagDeviceIds: formData.eartagDeviceIds,
collarDeviceIds: formData.collarDeviceIds,
estimatedDepartureTime: formData.estimatedDepartureTime,
estimatedArrivalTime: formData.estimatedArrivalTime,
startLocation: formData.startLocation,
endLocation: formData.endLocation,
startLat: formData.startLat,
startLon: formData.startLon,
endLat: formData.endLat,
endLon: formData.endLon,
cattleCount: formData.cattleCount,
freight: formData.freight,
quarantineCertNo: formData.quarantineCertNo,
remark: formData.remark,
emptyWeight: formData.emptyWeight,
entruckWeight: formData.entruckWeight,
landingEntruckWeight: formData.landingEntruckWeight,
quarantineTickeyUrl: formData.quarantineTickeyUrl,
poundListImg: formData.poundListImg,
emptyVehicleFrontPhoto: formData.emptyVehicleFrontPhoto,
loadedVehicleFrontPhoto: formData.loadedVehicleFrontPhoto,
loadedVehicleWeightPhoto: formData.loadedVehicleWeightPhoto,
driverIdCardPhoto: formData.driverIdCardPhoto,
destinationPoundListImg: formData.destinationPoundListImg,
destinationVehicleFrontPhoto: formData.destinationVehicleFrontPhoto,
entruckWeightVideo: formData.entruckWeightVideo,
emptyWeightVideo: formData.emptyWeightVideo,
cattleLoadingVideo: formData.cattleLoadingVideo,
controlSlotVideo: formData.controlSlotVideo,
cattleLoadingCircleVideo: formData.cattleLoadingCircleVideo,
unloadCattleVideo: formData.unloadCattleVideo,
destinationWeightVideo: formData.destinationWeightVideo,
});
}
} catch (error) {
console.error('关闭时保存表单数据到缓存失败:', error);
}
}
formRef.value?.resetFields();
// 清空所有表单字段
Object.keys(formData).forEach(key => {
if (key === 'orderIds') {
formData[key] = [];
} else if (key === 'editId') {
formData[key] = null;
} else if (typeof formData[key] === 'number') {
formData[key] = key === 'cattleCount' ? 1 : null;
} else if (Array.isArray(formData[key])) {
formData[key] = [];
} else {
formData[key] = '';
}
});
dialogVisible.value = false;
showStartLocationMap.value = false;
showEndLocationMap.value = false;
};
// 暴露方法给父组件
defineExpose({
open,
});
const emit = defineEmits(['success']);
// 监听表单数据变化,同步到 Pinia store新增和编辑模式都同步
watch(
() => formData,
(newData) => {
// 如果对话框未打开,不触发保存
if (!dialogVisible.value) {
return;
}
// 如果正在从缓存恢复数据,不触发保存
if (isRestoringFromCache.value) {
return;
}
// 使用防抖,避免频繁更新
const saveToStore = () => {
try {
console.log('[CACHE] 保存表单数据到缓存, editId:', formData.editId);
if (formData.editId) {
// 编辑模式:保存到 editFormData[editId]
deliveryFormStore.saveEditFormData(formData.editId, {
editId: formData.editId,
orderIds: formData.orderIds,
plateNumber: formData.plateNumber,
driverId: formData.driverId,
driverPhone: formData.driverPhone,
serverId: formData.serverId,
serverDeviceId: formData.serverDeviceId,
eartagIds: formData.eartagIds,
collarIds: formData.collarIds,
eartagDeviceIds: formData.eartagDeviceIds,
collarDeviceIds: formData.collarDeviceIds,
estimatedDepartureTime: formData.estimatedDepartureTime,
estimatedArrivalTime: formData.estimatedArrivalTime,
startLocation: formData.startLocation,
endLocation: formData.endLocation,
startLat: formData.startLat,
startLon: formData.startLon,
endLat: formData.endLat,
endLon: formData.endLon,
cattleCount: formData.cattleCount,
quarantineCertNo: formData.quarantineCertNo,
remark: formData.remark,
emptyWeight: formData.emptyWeight,
entruckWeight: formData.entruckWeight,
landingEntruckWeight: formData.landingEntruckWeight,
quarantineTickeyUrl: formData.quarantineTickeyUrl,
poundListImg: formData.poundListImg,
emptyVehicleFrontPhoto: formData.emptyVehicleFrontPhoto,
loadedVehicleFrontPhoto: formData.loadedVehicleFrontPhoto,
loadedVehicleWeightPhoto: formData.loadedVehicleWeightPhoto,
driverIdCardPhoto: formData.driverIdCardPhoto,
destinationPoundListImg: formData.destinationPoundListImg,
destinationVehicleFrontPhoto: formData.destinationVehicleFrontPhoto,
entruckWeightVideo: formData.entruckWeightVideo,
emptyWeightVideo: formData.emptyWeightVideo,
cattleLoadingVideo: formData.cattleLoadingVideo,
controlSlotVideo: formData.controlSlotVideo,
cattleLoadingCircleVideo: formData.cattleLoadingCircleVideo,
unloadCattleVideo: formData.unloadCattleVideo,
destinationWeightVideo: formData.destinationWeightVideo,
});
} else {
// 新增模式:保存到 newFormData
console.log('[CACHE] 保存新增模式数据:', {
plateNumber: formData.plateNumber,
driverId: formData.driverId,
});
deliveryFormStore.saveNewFormData({
orderIds: formData.orderIds,
plateNumber: formData.plateNumber,
driverId: formData.driverId,
driverPhone: formData.driverPhone,
serverId: formData.serverId,
serverDeviceId: formData.serverDeviceId,
eartagIds: formData.eartagIds,
collarIds: formData.collarIds,
eartagDeviceIds: formData.eartagDeviceIds,
collarDeviceIds: formData.collarDeviceIds,
estimatedDepartureTime: formData.estimatedDepartureTime,
estimatedArrivalTime: formData.estimatedArrivalTime,
startLocation: formData.startLocation,
endLocation: formData.endLocation,
startLat: formData.startLat,
startLon: formData.startLon,
endLat: formData.endLat,
endLon: formData.endLon,
cattleCount: formData.cattleCount,
quarantineCertNo: formData.quarantineCertNo,
remark: formData.remark,
emptyWeight: formData.emptyWeight,
entruckWeight: formData.entruckWeight,
landingEntruckWeight: formData.landingEntruckWeight,
quarantineTickeyUrl: formData.quarantineTickeyUrl,
poundListImg: formData.poundListImg,
emptyVehicleFrontPhoto: formData.emptyVehicleFrontPhoto,
loadedVehicleFrontPhoto: formData.loadedVehicleFrontPhoto,
loadedVehicleWeightPhoto: formData.loadedVehicleWeightPhoto,
driverIdCardPhoto: formData.driverIdCardPhoto,
destinationPoundListImg: formData.destinationPoundListImg,
destinationVehicleFrontPhoto: formData.destinationVehicleFrontPhoto,
entruckWeightVideo: formData.entruckWeightVideo,
emptyWeightVideo: formData.emptyWeightVideo,
cattleLoadingVideo: formData.cattleLoadingVideo,
controlSlotVideo: formData.controlSlotVideo,
cattleLoadingCircleVideo: formData.cattleLoadingCircleVideo,
unloadCattleVideo: formData.unloadCattleVideo,
destinationWeightVideo: formData.destinationWeightVideo,
});
}
} catch (error) {
console.error('保存表单数据到缓存失败:', error);
}
};
// 使用 setTimeout 实现简单的防抖500ms
if (watchTimer) {
clearTimeout(watchTimer);
}
watchTimer = setTimeout(saveToStore, 500);
},
{ deep: true }
);
// 防抖定时器
let watchTimer = null;
</script>
<style scoped>
.map {
width: 100%;
height: 500px;
}
/* 重量输入框样式优化 */
:deep(.weight-input-number) {
width: 100%;
}
:deep(.weight-input-number .el-input__inner) {
font-size: 14px;
padding: 0 15px;
height: 40px;
line-height: 40px;
text-align: left;
}
:deep(.weight-input-number .el-input-number__decrease),
:deep(.weight-input-number .el-input-number__increase) {
width: 32px;
height: 40px;
line-height: 40px;
font-size: 16px;
}
:deep(.weight-input-number .el-input-group__append) {
padding: 0 12px;
font-size: 14px;
background-color: #f5f7fa;
color: #606266;
border-left: 1px solid #dcdfe6;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
}
/* 照片上传样式 */
:deep(.avatar-uploader) {
width: 178px;
height: 178px;
display: block;
}
:deep(.avatar-uploader .avatar) {
width: 178px;
height: 178px;
display: block;
}
:deep(.avatar-uploader .el-icon) {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
line-height: 178px;
text-align: center;
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: all 0.3s;
}
:deep(.avatar-uploader .el-icon:hover) {
border-color: #409eff;
}
/* 照片上传包装器 */
.photo-upload-wrapper {
position: relative;
display: inline-block;
}
.photo-upload-wrapper .preview-btn {
position: absolute;
top: -8px;
right: 30px;
z-index: 10;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.photo-upload-wrapper .delete-btn {
position: absolute;
top: -8px;
right: -8px;
z-index: 10;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
/* 视频预览包装器 */
.video-preview-wrapper {
position: relative;
margin-top: 10px;
}
.video-preview-wrapper .delete-btn {
position: absolute;
top: 10px;
right: 10px;
z-index: 10;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
</style>