cba2ad3b3b
2、调整运单
2252 lines
91 KiB
Vue
2252 lines
91 KiB
Vue
<template>
|
||
<div class="master-editor" v-loading="loading">
|
||
<section>
|
||
<h3>基本信息</h3>
|
||
<el-form ref="form" :model="form" :rules="rules" label-position="right" label-width="auto"
|
||
><el-row :gutter="24"
|
||
><el-col :span="6"
|
||
><el-form-item label="项目" prop="projectName"
|
||
><el-select
|
||
v-model="form.projectId"
|
||
filterable
|
||
placeholder="请输入并选择"
|
||
@change="projectChange"
|
||
><el-option
|
||
v-for="project in projects"
|
||
:key="project.id"
|
||
:label="project.projectName"
|
||
:value="project.id" /></el-select></el-form-item></el-col
|
||
><el-col :span="6"
|
||
><el-form-item label="客户合同" prop="contractName"
|
||
><el-select
|
||
v-model="form.contractId"
|
||
:disabled="!form.projectId"
|
||
placeholder="选择项目后带出"
|
||
@change="contractChange"
|
||
><el-option
|
||
v-for="contract in contracts"
|
||
:key="contract.id"
|
||
:label="contract.contractName"
|
||
:value="contract.id" /></el-select></el-form-item></el-col
|
||
><el-col :span="6"
|
||
><el-form-item label="总单号" required
|
||
><el-input
|
||
:model-value="form.masterNo || '系统自动生成'"
|
||
readonly /></el-form-item></el-col
|
||
><el-col :span="6"
|
||
><el-form-item label="运输类型" required
|
||
><el-select v-model="form.transportOrganizationType" disabled
|
||
><el-option label="多式联运" value="多式联运" /></el-select></el-form-item></el-col
|
||
><el-col :span="24"
|
||
><el-form-item label="备注"
|
||
><el-input
|
||
v-model="form.remark"
|
||
maxlength="200"
|
||
show-word-limit
|
||
placeholder="请输入" /></el-form-item></el-col></el-row
|
||
></el-form>
|
||
</section>
|
||
<section>
|
||
<div class="section-head">
|
||
<h3>收发货信息</h3>
|
||
<div class="section-head__actions">
|
||
<el-link type="primary" @click="addRoute">新增途经点</el-link>
|
||
<el-link type="primary" @click="openRouteDialog">选择线路</el-link>
|
||
</div>
|
||
</div>
|
||
<el-form class="route-info-form" :model="form" label-position="right" label-width="96px"
|
||
><div class="route-flow">
|
||
<div class="route-flow-row">
|
||
<span class="route-flow-marker route-flow-marker--start">起</span>
|
||
<div class="route-flow-body">
|
||
<div class="route-node-form">
|
||
<el-form-item label="发货地" required class="route-address-form-item"
|
||
><div class="route-address-inputs"
|
||
><el-input v-if="isNonRoadAddress('departure')" v-model="form.departureName" class="route-address-name" readonly placeholder="请选择站点" /><el-cascader v-else v-model="addressRegionPaths.departure" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange('departure', value)" /><el-input v-model="form.departureAddress" class="route-address-detail" readonly placeholder="请输入详细地址" @click="handleAddressDetailClick('departure')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" @click="openCommonAddress('departure')" /></el-tooltip></div
|
||
></el-form-item>
|
||
<el-form-item label="联系人" class="route-contact-form-item"
|
||
><el-input v-model="form.departureContact" placeholder="请输入"
|
||
/></el-form-item>
|
||
<el-form-item label="联系方式" class="route-phone-form-item"
|
||
><el-input v-model="form.departurePhone" placeholder="请输入"
|
||
/></el-form-item>
|
||
<el-form-item label="计划开始" class="route-date-form-item"
|
||
><el-date-picker
|
||
v-model="form.planStartTime"
|
||
type="date"
|
||
format="YYYY-MM-DD"
|
||
value-format="YYYY-MM-DD"
|
||
placeholder="请选择"
|
||
/></el-form-item>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<template v-for="(route, index) in form.routes" :key="index">
|
||
<div class="route-flow-segment">
|
||
<div class="route-step-title"><span class="route-segment-label">{{ circledSegmentNumber(index + 1) }} 段{{ index + 1 }}</span><el-tooltip :content="getRouteTitle(index).replace(`段${index + 1}:`, '')" placement="top"><span class="route-title-path">{{ formatRouteTitle(getRouteTitle(index), index + 1) }}</span></el-tooltip><div class="route-transport-control"><span>运输方式<span class="required-asterisk">*</span></span><el-select v-model="route.transportType" placeholder="请选择"><el-option v-for="type in transports" :key="type" :label="type" :value="type" /></el-select></div></div>
|
||
</div>
|
||
<div class="route-flow-row">
|
||
<span class="route-flow-marker route-flow-marker--middle">经</span>
|
||
<div class="route-flow-body">
|
||
<div class="route-node-form">
|
||
<el-form-item label="途经地" required class="route-address-form-item"
|
||
><div class="route-address-inputs"
|
||
><el-input v-if="isNonRoadAddress(`route-${index}`)" v-model="route.departureName" class="route-address-name" readonly :disabled="!route.transportType" placeholder="请先选择运输方式" /><el-cascader v-else v-model="addressRegionPaths[`route-${index}`]" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="!route.transportType" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange(`route-${index}`, value)" /><el-input v-model="route.departureAddress" class="route-address-detail" readonly :disabled="!route.transportType" placeholder="请先选择运输方式" @click="handleAddressDetailClick(`route-${index}`)" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="!route.transportType" @click="openCommonAddress(`route-${index}`)" /></el-tooltip></div
|
||
></el-form-item>
|
||
<el-form-item label="联系人" class="route-contact-form-item"><el-input v-model="route.departureContact" placeholder="请输入" /></el-form-item>
|
||
<el-form-item label="联系方式" class="route-phone-form-item"><div class="route-contact-actions"><el-input v-model="route.departurePhone" placeholder="请输入" /><el-tooltip v-if="index === form.routes.length - 1" content="新增途经点" placement="top"><el-button class="route-action-button" type="primary" link @click="addRoute"><el-icon><CirclePlus /></el-icon></el-button></el-tooltip><el-tooltip content="删除途经点" placement="top"><el-button class="route-action-button" type="danger" link @click="removeRoute(index)"><el-icon><Remove /></el-icon></el-button></el-tooltip></div></el-form-item>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<div class="route-flow-segment">
|
||
<div class="route-step-title"><span class="route-segment-label">{{ circledSegmentNumber(form.routes.length + 1) }} 段{{ form.routes.length + 1 }}</span><el-tooltip :content="getFinalRouteTitle().replace(`段${form.routes.length + 1}:`, '')" placement="top"><span class="route-title-path">{{ formatRouteTitle(getFinalRouteTitle(), form.routes.length + 1) }}</span></el-tooltip><div class="route-transport-control"><span>运输方式<span class="required-asterisk">*</span></span><el-select v-model="form.finalTransportType" placeholder="请选择"><el-option v-for="type in transports" :key="type" :label="type" :value="type" /></el-select></div></div>
|
||
</div>
|
||
<div class="route-flow-row">
|
||
<span class="route-flow-marker route-flow-marker--end">终</span>
|
||
<div class="route-flow-body">
|
||
<div class="route-node-form">
|
||
<el-form-item label="收货地" required class="route-address-form-item"
|
||
><div class="route-address-inputs"
|
||
><el-input v-if="isNonRoadAddress('arrival')" v-model="form.arrivalName" class="route-address-name" readonly :disabled="!form.finalTransportType" placeholder="请先选择运输方式" /><el-cascader v-else v-model="addressRegionPaths.arrival" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="!form.finalTransportType" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange('arrival', value)" /><el-input v-model="form.arrivalAddress" class="route-address-detail" readonly :disabled="!form.finalTransportType" placeholder="请先选择运输方式" @click="handleAddressDetailClick('arrival')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="!form.finalTransportType" @click="openCommonAddress('arrival')" /></el-tooltip></div
|
||
></el-form-item>
|
||
<el-form-item label="联系人" class="route-contact-form-item"
|
||
><el-input v-model="form.arrivalContact" placeholder="请输入" /></el-form-item>
|
||
<el-form-item label="联系方式" class="route-phone-form-item"
|
||
><el-input v-model="form.arrivalPhone" placeholder="请输入" /></el-form-item>
|
||
<el-form-item label="计划结束" class="route-date-form-item"
|
||
><el-date-picker
|
||
v-model="form.planEndTime"
|
||
type="date"
|
||
format="YYYY-MM-DD"
|
||
value-format="YYYY-MM-DD"
|
||
placeholder="请选择"
|
||
/></el-form-item>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div></el-form
|
||
>
|
||
</section>
|
||
<section>
|
||
<div class="goods-heading">
|
||
<h3>货物信息</h3>
|
||
<div class="goods-toolbar">
|
||
<!-- <el-link type="primary" @click="cargoImportVisible = true">导入货物</el-link> -->
|
||
<el-link type="primary" @click="openCommonCargoDialog">常用货物</el-link>
|
||
</div>
|
||
</div>
|
||
<el-table :data="form.goods" border
|
||
><el-table-column type="index" label="序号" width="60" /><el-table-column
|
||
label="货物名称"
|
||
min-width="130"
|
||
><template #default="{ row }"
|
||
><el-autocomplete
|
||
v-model="row.cargoName"
|
||
clearable
|
||
placeholder="请输入货物名称"
|
||
:debounce="300"
|
||
:fetch-suggestions="
|
||
(queryString, callback) => fetchCargoSuggestions(queryString, callback, row.cargoTypePath)
|
||
"
|
||
:loading="isCargoOptionsLoading(row.cargoTypePath)"
|
||
@select="cargo => selectCargoSuggestion(row, cargo)"
|
||
><template #default="{ item }">
|
||
<div class="master-editor__cargo-autocomplete-item">
|
||
<span>{{ item.cargoName || item.name || '' }}</span>
|
||
<small v-if="item.cargoCode">{{ item.cargoCode }}</small>
|
||
</div>
|
||
</template></el-autocomplete
|
||
></template></el-table-column
|
||
><el-table-column label="货物类型" min-width="130"
|
||
><template #header><span>货物类型<span class="required-asterisk">*</span></span></template>
|
||
<template #default="{ row }"
|
||
><el-cascader
|
||
v-model="row.cargoTypePath"
|
||
:options="cargoTypeOptions"
|
||
:props="cargoTypeCascaderProps"
|
||
:placeholder="row.cargoType || '请选择'"
|
||
clearable
|
||
filterable
|
||
:loading="cargoTypeLoading"
|
||
@visible-change="visible => visible && loadCargoTypeOptions()"
|
||
@change="value => handleCargoTypeChange(row, value)"
|
||
/></template></el-table-column
|
||
><el-table-column label="数量" width="180" class-name="master-editor__quantity-cell"
|
||
><template #header><span>数量<span class="required-asterisk">*</span></span></template>
|
||
<template #default="{ row }"
|
||
><el-input
|
||
v-model="row.quantity"
|
||
inputmode="decimal"
|
||
placeholder="请输入"
|
||
@input="value => handleQuantityInput(row, value)" /></template></el-table-column
|
||
><el-table-column label="数量单位" width="120"
|
||
><template #header><span>数量单位<span class="required-asterisk">*</span></span></template>
|
||
<template #default="{ row }"
|
||
><el-select v-model="row.quantityUnit"
|
||
><el-option v-for="unit in quantityUnitOptions" :key="unit" :label="unit" :value="unit" /></el-select></template></el-table-column
|
||
><el-table-column label="包装" width="130"
|
||
><template #default="{ row }"
|
||
><el-input v-model="row.packageType" placeholder="请选择" /></template></el-table-column
|
||
><el-table-column label="品牌" min-width="120"
|
||
><template #default="{ row }"
|
||
><el-input v-model="row.brand" placeholder="请输入" /></template></el-table-column
|
||
><el-table-column label="规格" min-width="120"
|
||
><template #default="{ row }"
|
||
><el-input
|
||
v-model="row.specification"
|
||
placeholder="请输入" /></template></el-table-column
|
||
><el-table-column label="型号" min-width="120"
|
||
><template #default="{ row }"
|
||
><el-input v-model="row.model" placeholder="请输入" /></template></el-table-column
|
||
><el-table-column label="物料编码" min-width="140"
|
||
><template #default="{ row }"
|
||
><el-input v-model="row.materialCode" placeholder="请输入" /></template></el-table-column
|
||
><el-table-column label="设备编码" min-width="140"
|
||
><template #default="{ row }"
|
||
><el-input v-model="row.deviceCode" placeholder="请输入" /></template></el-table-column
|
||
><el-table-column label="备注" min-width="180"
|
||
><template #default="{ row }"
|
||
><el-input v-model="row.remark" maxlength="200" show-word-limit placeholder="请输入" /></template></el-table-column
|
||
><el-table-column label="操作" width="140"
|
||
><template #default="{ $index }"
|
||
><div class="goods-actions"
|
||
><el-link
|
||
v-if="$index === form.goods.length - 1"
|
||
type="primary"
|
||
@click="addGoods($index)"
|
||
>新增</el-link
|
||
><el-link
|
||
type="danger"
|
||
:disabled="form.goods.length === 1"
|
||
@click="removeGoods($index)"
|
||
>删除</el-link
|
||
></div
|
||
></template
|
||
></el-table-column
|
||
></el-table
|
||
>
|
||
</section>
|
||
<section>
|
||
<div class="section-head">
|
||
<h3>附件</h3>
|
||
<div class="section-head__actions">
|
||
<el-button
|
||
type="primary"
|
||
:disabled="!attachmentRows.length"
|
||
@click="handleAttachmentBatchDownload"
|
||
>
|
||
批量下载
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
<div class="master-editor__attachment">
|
||
<el-table
|
||
:data="attachmentRows"
|
||
border
|
||
class="master-editor__attachment-table"
|
||
@selection-change="handleAttachmentSelectionChange"
|
||
>
|
||
<el-table-column type="selection" width="55" align="center" />
|
||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||
<el-table-column label="文件名" min-width="240" align="center" show-overflow-tooltip>
|
||
<template #default="{ row }">
|
||
<el-link type="primary" @click="previewAttachment(row)">
|
||
{{ attachmentName(row) }}
|
||
</el-link>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="附件描述" min-width="220" align="center">
|
||
<template #default="{ row }">
|
||
<el-input
|
||
v-model="row.description"
|
||
maxlength="200"
|
||
show-word-limit
|
||
placeholder="请输入附件描述"
|
||
@input="syncAttachmentsJson"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="文件大小" width="120" align="center">
|
||
<template #default="{ row }">{{ formatFileSize(row.size) }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="uploadUserName" label="上传人" width="140" align="center" />
|
||
<el-table-column prop="uploadTime" label="上传时间" width="170" align="center" sortable />
|
||
<el-table-column label="操作" width="120" align="center" fixed="right">
|
||
<template #default="{ row, $index }">
|
||
<el-link type="danger" @click="removeAttachment($index)">删除</el-link>
|
||
<el-link type="primary" @click="downloadAttachment(row)">下载</el-link>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div class="master-editor__attachment-upload">
|
||
<vehicle-attachment-upload
|
||
v-model="attachmentRows"
|
||
:file-types="attachmentFileTypes"
|
||
:max-size="50"
|
||
:show-file-list="false"
|
||
button-text="上传附件"
|
||
@change="handleAttachmentChange"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
<el-dialog
|
||
v-model="attachmentDocumentPreviewVisible"
|
||
:title="attachmentPreviewFile.name || '附件预览'"
|
||
append-to-body
|
||
destroy-on-close
|
||
width="90%"
|
||
top="4vh"
|
||
class="master-editor__attachment-viewer-dialog"
|
||
>
|
||
<open-file-viewer
|
||
v-if="attachmentDocumentPreviewVisible && attachmentPreviewFile.url"
|
||
:file="attachmentPreviewFile.url"
|
||
:file-name="attachmentPreviewFile.name"
|
||
:mime-type="attachmentPreviewFile.mimeType"
|
||
width="100%"
|
||
height="72vh"
|
||
fit="contain"
|
||
theme="auto"
|
||
locale="zh-CN"
|
||
:toolbar="attachmentViewerToolbar"
|
||
:plugins="attachmentViewerPlugins"
|
||
@unsupported="handleAttachmentPreviewUnsupported"
|
||
@error="handleAttachmentPreviewError"
|
||
/>
|
||
</el-dialog>
|
||
<el-image-viewer
|
||
v-if="attachmentImagePreviewVisible"
|
||
:url-list="attachmentImagePreviewUrls"
|
||
:initial-index="attachmentImagePreviewIndex"
|
||
@close="attachmentImagePreviewVisible = false"
|
||
/>
|
||
<el-dialog
|
||
v-model="addressDialogVisible"
|
||
title="选择常用地址"
|
||
append-to-body
|
||
width="1100px"
|
||
@opened="loadAddressList"
|
||
>
|
||
<div class="route-dialog__search common-address-dialog__search">
|
||
<el-form :model="addressQuery" label-position="right" label-width="auto">
|
||
<el-row :gutter="16">
|
||
<el-col :span="6">
|
||
<el-form-item label="地址名称">
|
||
<el-input v-model="addressQuery.addressName" clearable placeholder="请输入" @keyup.enter="searchAddressList" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<el-form-item label="详细地址">
|
||
<el-input v-model="addressQuery.detailAddress" clearable placeholder="请输入" @keyup.enter="searchAddressList" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<el-form-item label="行政区划">
|
||
<el-input v-model="addressQuery.regionName" clearable placeholder="请输入" @keyup.enter="searchAddressList" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="6" class="route-dialog__search-actions">
|
||
<el-button type="primary" @click="searchAddressList">查询</el-button>
|
||
<el-button @click="resetAddressList">重置</el-button>
|
||
</el-col>
|
||
</el-row>
|
||
</el-form>
|
||
</div>
|
||
<el-table v-loading="addressLoading" :data="addressRows" border height="360">
|
||
<el-table-column type="index" label="序号" width="70" />
|
||
<el-table-column prop="addressName" label="地址名称" min-width="150" />
|
||
<el-table-column
|
||
prop="detailAddress"
|
||
label="详细地址"
|
||
min-width="260"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column prop="regionName" label="行政区划" min-width="150" />
|
||
<el-table-column prop="contactName" label="联系人" min-width="120" />
|
||
<el-table-column prop="contactPhone" label="联系方式" min-width="140" />
|
||
<el-table-column label="操作" width="100" fixed="right"
|
||
><template #default="{ row }"
|
||
><el-link type="primary" @click="selectAddress(row)">选择</el-link></template
|
||
></el-table-column
|
||
>
|
||
</el-table>
|
||
<div class="address-pagination">
|
||
<el-pagination
|
||
v-model:current-page="addressPage.current"
|
||
v-model:page-size="addressPage.size"
|
||
:page-sizes="[10, 20, 50]"
|
||
layout="total, prev, pager, next, sizes"
|
||
:total="addressPage.total"
|
||
@size-change="loadAddressList"
|
||
@current-change="loadAddressList"
|
||
/>
|
||
</div>
|
||
</el-dialog>
|
||
<el-dialog
|
||
v-model="mapDialogVisible"
|
||
title="地图选择地址"
|
||
append-to-body
|
||
width="920px"
|
||
@opened="initAddressMap"
|
||
>
|
||
<div class="address-map-toolbar">
|
||
<el-input
|
||
v-model="mapKeyword"
|
||
clearable
|
||
placeholder="输入地址关键词"
|
||
@keyup.enter="searchAddressMap"
|
||
/><el-button type="primary" :icon="Search" :loading="mapLoading" @click="searchAddressMap"
|
||
>搜索</el-button
|
||
>
|
||
</div>
|
||
<div class="map-picker-content">
|
||
<map-search-results :results="mapSearchResults" @select="selectAddressMapSearchResult" />
|
||
<div ref="addressMap" class="address-map" />
|
||
</div>
|
||
<div class="address-map-status">
|
||
{{ mapStatus
|
||
}}<span v-if="mapSelected.regionName"> 行政区划:{{ mapSelected.regionName }}</span>
|
||
</div>
|
||
<template #footer
|
||
><el-button @click="mapDialogVisible = false">取消</el-button
|
||
><el-button type="primary" :disabled="!mapSelected.longitude" @click="confirmAddressMap"
|
||
>确定</el-button
|
||
></template
|
||
>
|
||
</el-dialog>
|
||
<el-dialog
|
||
v-model="stationDialogVisible"
|
||
:title="`选择${stationTypeLabel}`"
|
||
append-to-body
|
||
width="1100px"
|
||
@opened="loadStationList"
|
||
>
|
||
<div class="route-dialog__search">
|
||
<el-form :model="stationQuery" label-position="right" label-width="auto">
|
||
<el-row :gutter="16">
|
||
<el-col :span="8"><el-form-item label="站点名称"><el-input v-model="stationQuery.name" clearable placeholder="请输入" /></el-form-item></el-col>
|
||
<el-col :span="8"><el-form-item label="详细地址"><el-input v-model="stationQuery.detailAddress" clearable placeholder="请输入" /></el-form-item></el-col>
|
||
<el-col :span="8" class="route-dialog__search-actions"><el-button type="primary" @click="searchStationList">查询</el-button><el-button @click="resetStationList">重置</el-button></el-col>
|
||
</el-row>
|
||
</el-form>
|
||
</div>
|
||
<el-table v-loading="stationLoading" :data="stationRows" border height="360">
|
||
<el-table-column type="index" label="序号" width="70" />
|
||
<el-table-column prop="stationName" label="站点名称" min-width="160" />
|
||
<el-table-column prop="stationType" label="类型" min-width="120" />
|
||
<el-table-column prop="stationCode" label="站点编码" min-width="130" />
|
||
<el-table-column prop="detailAddress" label="详细地址" min-width="260" show-overflow-tooltip />
|
||
<el-table-column prop="regionName" label="行政区划" min-width="150" />
|
||
<el-table-column label="操作" width="100" fixed="right"><template #default="{ row }"><el-link type="primary" @click="selectStation(row)">选择</el-link></template></el-table-column>
|
||
</el-table>
|
||
<div class="address-pagination"><el-pagination v-model:current-page="stationPage.current" v-model:page-size="stationPage.size" :page-sizes="[10, 20, 50]" layout="total, prev, pager, next, sizes" :total="stationPage.total" @size-change="loadStationList" @current-change="loadStationList" /></div>
|
||
</el-dialog>
|
||
<el-dialog v-model="commonCargoVisible" title="选择常用货物" append-to-body width="1100px" @opened="loadCommonCargoList">
|
||
<div class="route-dialog__search common-cargo-dialog__search">
|
||
<el-form :model="commonCargoQuery" label-position="right" label-width="86px">
|
||
<el-row :gutter="16">
|
||
<el-col :span="6"><el-form-item label="货物名称"><el-input v-model="commonCargoQuery.cargoName" clearable placeholder="请输入" /></el-form-item></el-col>
|
||
<el-col :span="6"><el-form-item label="货物类型"><el-cascader v-model="commonCargoQuery.cargoTypePath" :options="cargoTypeOptions" :props="cargoTypeCascaderProps" clearable filterable placeholder="请选择" :loading="cargoTypeLoading" @visible-change="visible => visible && loadCargoTypeOptions()" @change="handleCommonCargoTypeChange" /></el-form-item></el-col>
|
||
<el-col :span="6"><el-form-item label="规格型号"><el-input v-model="commonCargoQuery.specificationModel" clearable placeholder="请输入" /></el-form-item></el-col>
|
||
<el-col :span="6" class="route-dialog__search-actions"><el-button type="primary" @click="searchCommonCargoList">查询</el-button><el-button @click="resetCommonCargoList">重置</el-button></el-col>
|
||
</el-row>
|
||
</el-form>
|
||
</div>
|
||
<el-table v-loading="commonCargoLoading" :data="commonCargoRows" border height="360" @selection-change="handleCommonCargoSelectionChange">
|
||
<el-table-column type="selection" width="55" align="center" fixed="left" />
|
||
<el-table-column type="index" label="序号" width="70" /><el-table-column prop="cargoName" label="货物名称" min-width="150" /><el-table-column prop="cargoCode" label="货物编号" min-width="130" /><el-table-column prop="firstCargoTypeName" label="一级货物类型" min-width="150" /><el-table-column prop="secondCargoTypeName" label="二级货物类型" min-width="150" /><el-table-column prop="packageType" label="包装" min-width="100" /><el-table-column prop="brand" label="品牌" min-width="120" /><el-table-column prop="specification" label="规格" min-width="120" /><el-table-column prop="model" label="型号" min-width="120" /><el-table-column label="操作" width="100" fixed="right"><template #default="{ row }"><el-link type="primary" @click="selectCommonCargo(row)">选择</el-link></template></el-table-column>
|
||
</el-table>
|
||
<div class="address-pagination"><el-pagination v-model:current-page="commonCargoPage.current" v-model:page-size="commonCargoPage.size" :page-sizes="[10, 20, 50]" layout="total, prev, pager, next, sizes" :total="commonCargoPage.total" @size-change="loadCommonCargoList" @current-change="loadCommonCargoList" /></div>
|
||
<template #footer><el-button @click="commonCargoVisible = false">关闭</el-button><el-button type="primary" :disabled="!commonCargoSelected.length" @click="confirmCommonCargoSelection">确定</el-button></template>
|
||
</el-dialog>
|
||
<el-dialog v-model="cargoImportVisible" title="导入货物" append-to-body width="560px">
|
||
<section-card>
|
||
<div class="cargo-import">
|
||
<el-upload
|
||
action="#"
|
||
accept=".xls,.xlsx"
|
||
:auto-upload="false"
|
||
:show-file-list="false"
|
||
:disabled="cargoImportLoading"
|
||
:on-change="handleCargoImport"
|
||
>
|
||
<el-button type="primary" :loading="cargoImportLoading">上传</el-button>
|
||
<template #tip>
|
||
<div class="el-upload__tip">支持 .xlsx、.xls 文件,单个文件不超过 50M</div>
|
||
</template>
|
||
</el-upload>
|
||
<el-button type="primary" plain @click="downloadCargoTemplate">点击下载模板</el-button>
|
||
</div>
|
||
</section-card>
|
||
<template #footer><el-button @click="cargoImportVisible = false">关闭</el-button></template>
|
||
</el-dialog>
|
||
<el-dialog v-model="routeDialogVisible" title="选择线路" width="1280px" @opened="loadRouteList">
|
||
<div class="route-dialog__search">
|
||
<el-form :model="routeQuery" label-position="right" label-width="86px">
|
||
<el-row :gutter="16">
|
||
<el-col :span="6">
|
||
<el-form-item label="线路名称">
|
||
<el-input v-model="routeQuery.routeName" clearable placeholder="请输入" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<el-form-item label="发货地址">
|
||
<el-input v-model="routeQuery.departureAddress" clearable placeholder="请输入" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<el-form-item label="收货地址">
|
||
<el-input v-model="routeQuery.arrivalAddress" clearable placeholder="请输入" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="6" class="route-dialog__search-actions">
|
||
<el-button type="primary" @click="searchRouteList">查询</el-button>
|
||
<el-button @click="resetRouteList">重置</el-button>
|
||
</el-col>
|
||
</el-row>
|
||
</el-form>
|
||
</div>
|
||
<el-table v-loading="routeLoading" :data="routeRows" border height="320">
|
||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||
<el-table-column prop="routeName" label="线路名称" min-width="150" />
|
||
<el-table-column prop="routeCode" label="线路编号" min-width="140" />
|
||
<el-table-column prop="departureName" label="发货地" min-width="140" />
|
||
<el-table-column
|
||
prop="departureAddress"
|
||
label="发货地址"
|
||
min-width="220"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column prop="departureContact" label="发货联系人" min-width="120" />
|
||
<el-table-column prop="departurePhone" label="发货联系方式" min-width="140" />
|
||
<el-table-column prop="arrivalName" label="收货地" min-width="140" />
|
||
<el-table-column
|
||
prop="arrivalAddress"
|
||
label="收货地址"
|
||
min-width="220"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column prop="arrivalContact" label="收货联系人" min-width="120" />
|
||
<el-table-column prop="arrivalPhone" label="收货联系方式" min-width="140" />
|
||
<el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip />
|
||
<el-table-column prop="deptName" label="组织" min-width="150" show-overflow-tooltip />
|
||
<el-table-column
|
||
prop="updateUserName"
|
||
label="更新人"
|
||
min-width="120"
|
||
:formatter="formatRouteUpdateUserName"
|
||
/>
|
||
<el-table-column prop="updateTime" label="更新时间" min-width="160" sortable />
|
||
<el-table-column prop="createTime" label="创建时间" min-width="160" sortable />
|
||
<el-table-column label="操作" width="110" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-link type="primary" @click="selectRoute(row)">选择</el-link>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div class="route-dialog__pagination">
|
||
<el-pagination
|
||
v-model:current-page="routePage.current"
|
||
v-model:page-size="routePage.size"
|
||
:page-sizes="[10, 20, 50, 100]"
|
||
layout="total, prev, pager, next, sizes"
|
||
:total="routePage.total"
|
||
@size-change="changeRoutePageSize"
|
||
@current-change="changeRoutePage"
|
||
/>
|
||
</div>
|
||
<template #footer>
|
||
<el-button @click="routeDialogVisible = false">关闭</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
<footer>
|
||
<el-button @click="$emit('back')">取消</el-button>
|
||
<el-button type="primary" plain @click="saveDraft">暂存</el-button>
|
||
<el-button type="primary" plain @click="confirmCreate">确认创建</el-button>
|
||
<el-button type="primary" @click="createAndDispatch">创建并调度</el-button>
|
||
</footer>
|
||
</div>
|
||
</template>
|
||
<script>
|
||
import { getList as getCommonAddressList } from '@/api/base/common-address';
|
||
import { getList as getAirportMasterList } from '@/api/base/airport-master';
|
||
import { getList as getPortTerminalList } from '@/api/base/port-terminal';
|
||
import { getList as getRailwayStationList } from '@/api/base/railway-station';
|
||
import { getList as getCargoTypeList } from '@/api/base/cargo-type';
|
||
import { getLazyTree as getRegionLazyTree } from '@/api/base/region';
|
||
import { getList as getCommonCargoList } from '@/api/business/common-cargo';
|
||
import { exportBlob, importBlob } from '@/api/common';
|
||
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||
import { OfficeBuilding, Search } from '@element-plus/icons-vue';
|
||
import { ElImageViewer } from 'element-plus';
|
||
import { OpenFileViewer } from '@open-file-viewer/vue';
|
||
import { fallbackPlugin, imagePlugin, officePlugin, pdfPlugin, textPlugin } from '@open-file-viewer/core';
|
||
import '@open-file-viewer/core/style.css';
|
||
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
|
||
import { mapGetters } from 'vuex';
|
||
|
||
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
|
||
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
|
||
const quantityUnitOptions = ['吨', '件', '方'];
|
||
const attachmentViewerPlugins = [
|
||
imagePlugin(),
|
||
pdfPlugin({ workerSrc: pdfWorkerSrc, useFetchData: true }),
|
||
officePlugin({ pdf: { workerSrc: pdfWorkerSrc, useFetchData: true } }),
|
||
textPlugin(),
|
||
fallbackPlugin(),
|
||
];
|
||
let amapLoader;
|
||
import { getList as getCommonRouteList } from '@/api/business/common-route';
|
||
import * as api from '@/api/business/master-order';
|
||
import { formatUpdateUserName } from '@/utils/audit';
|
||
import { isMobile } from '@/utils/validate';
|
||
export default {
|
||
components: {
|
||
ElImageViewer,
|
||
OpenFileViewer,
|
||
},
|
||
props: { id: [String, Number] },
|
||
emits: ['back', 'dispatch'],
|
||
data() {
|
||
return {
|
||
OfficeBuilding,
|
||
Search,
|
||
loading: false,
|
||
projects: [],
|
||
contracts: [],
|
||
routeDialogVisible: false,
|
||
routeLoading: false,
|
||
routeQuery: {},
|
||
routeRows: [],
|
||
routePage: { current: 1, size: 10, total: 0 },
|
||
addressDialogVisible: false,
|
||
addressLoading: false,
|
||
addressTarget: '',
|
||
addressRows: [],
|
||
addressQuery: {},
|
||
addressPage: { current: 1, size: 10, total: 0 },
|
||
addressRegionPaths: { departure: [], arrival: [] },
|
||
regionOptions: [],
|
||
regionLoading: false,
|
||
regionRequest: null,
|
||
stationDialogVisible: false,
|
||
stationLoading: false,
|
||
stationQuery: {},
|
||
stationRows: [],
|
||
stationPage: { current: 1, size: 10, total: 0 },
|
||
mapDialogVisible: false,
|
||
mapLoading: false,
|
||
mapTarget: '',
|
||
mapKeyword: '',
|
||
mapStatus: '可搜索地址或点击地图选点',
|
||
mapSelected: {},
|
||
mapSearchResults: [],
|
||
addressMapInstance: null,
|
||
addressMapMarker: null,
|
||
addressMapGeocoder: null,
|
||
commonCargoVisible: false,
|
||
commonCargoLoading: false,
|
||
commonCargoRows: [],
|
||
commonCargoQuery: {},
|
||
commonCargoSelected: [],
|
||
commonCargoPage: { current: 1, size: 10, total: 0 },
|
||
cargoTypeLoading: false,
|
||
cargoTypeOptions: [],
|
||
cargoTypeFlatOptions: [],
|
||
cargoOptionsMap: {},
|
||
cargoOptionsLoadingMap: {},
|
||
cargoImportVisible: false,
|
||
cargoImportLoading: false,
|
||
attachmentRows: [],
|
||
selectedAttachmentRows: [],
|
||
attachmentImagePreviewVisible: false,
|
||
attachmentImagePreviewUrls: [],
|
||
attachmentImagePreviewIndex: 0,
|
||
attachmentDocumentPreviewVisible: false,
|
||
attachmentPreviewFile: {},
|
||
attachmentViewerPlugins,
|
||
attachmentViewerToolbar: {
|
||
download: true,
|
||
fullscreen: true,
|
||
print: true,
|
||
rotate: true,
|
||
zoom: true,
|
||
},
|
||
attachmentFileTypes: [
|
||
'pdf',
|
||
'bmp',
|
||
'jpeg',
|
||
'png',
|
||
'jpg',
|
||
'doc',
|
||
'docx',
|
||
'ppt',
|
||
'pptx',
|
||
'xlsx',
|
||
'xls',
|
||
'eml',
|
||
'msg',
|
||
'zip',
|
||
],
|
||
transports: ['公路运输', '铁路运输', '水路运输', '航空运输'],
|
||
quantityUnitOptions,
|
||
form: {
|
||
routes: [{ segmentNo: '段1', transportType: '' }],
|
||
goods: [{ quantityUnit: quantityUnitOptions[0] }],
|
||
attachmentsJson: '[]',
|
||
transportOrganizationType: '多式联运',
|
||
},
|
||
rules: {
|
||
projectName: [{ required: true, message: '请选择项目', trigger: 'change' }],
|
||
contractName: [{ required: true, message: '请选择客户合同', trigger: 'change' }],
|
||
},
|
||
};
|
||
},
|
||
computed: {
|
||
...mapGetters(['userInfo']),
|
||
lastRouteName() {
|
||
return this.form.routes[this.form.routes.length - 1]?.departureName || '途经地';
|
||
},
|
||
stationTypeLabel() {
|
||
return {
|
||
rail: '铁路车站',
|
||
water: '港口/码头',
|
||
air: '空港机场',
|
||
}[this.addressTransportMode(this.addressTarget)] || '站点';
|
||
},
|
||
cargoTypeCascaderProps() {
|
||
return {
|
||
value: 'id',
|
||
label: 'label',
|
||
children: 'children',
|
||
emitPath: true,
|
||
};
|
||
},
|
||
regionCascaderProps() {
|
||
return {
|
||
value: 'id',
|
||
label: 'title',
|
||
children: 'children',
|
||
emitPath: true,
|
||
};
|
||
},
|
||
},
|
||
async mounted() {
|
||
this.loading = true;
|
||
try {
|
||
await Promise.all([this.loadProjects(), this.loadCargoTypeOptions(), this.loadRegionOptions()]);
|
||
if (this.id) {
|
||
await this.loadDetail(this.id);
|
||
}
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
},
|
||
methods: {
|
||
responseData(response) {
|
||
return response?.data?.data || response?.data || response || {};
|
||
},
|
||
circledSegmentNumber(number) {
|
||
const value = Number(number);
|
||
const circledNumbers = '①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳';
|
||
return circledNumbers[value - 1] || String(value);
|
||
},
|
||
async loadRegionOptions() {
|
||
if (this.regionOptions.length) return this.regionOptions;
|
||
if (this.regionRequest) return this.regionRequest;
|
||
this.regionLoading = true;
|
||
this.regionRequest = getRegionLazyTree()
|
||
.then(response => {
|
||
const data = response?.data?.data || response?.data || response || [];
|
||
this.regionOptions = this.extractChinaRegionOptions(this.buildRegionTree(data));
|
||
this.syncAddressRegionPaths();
|
||
return this.regionOptions;
|
||
})
|
||
.finally(() => {
|
||
this.regionLoading = false;
|
||
this.regionRequest = null;
|
||
});
|
||
return this.regionRequest;
|
||
},
|
||
buildRegionTree(regions = []) {
|
||
const flat = [];
|
||
const collect = list =>
|
||
(list || []).forEach(item => {
|
||
flat.push({ ...item, children: undefined });
|
||
if (item.children?.length) collect(item.children);
|
||
});
|
||
collect(regions);
|
||
const nodeMap = new Map();
|
||
flat.forEach(item => {
|
||
const id = item.id ?? item.code ?? item.value;
|
||
if (id === undefined || id === null) return;
|
||
nodeMap.set(String(id), {
|
||
...item,
|
||
id: String(id),
|
||
parentId: String(item.parentId ?? item.parentCode ?? ''),
|
||
children: [],
|
||
});
|
||
});
|
||
const roots = [];
|
||
nodeMap.forEach(node => {
|
||
const parent = nodeMap.get(node.parentId);
|
||
if (parent && parent !== node) parent.children.push(node);
|
||
else roots.push(node);
|
||
});
|
||
const normalize = node => {
|
||
const children = node.children.map(item => normalize(item));
|
||
return { ...node, title: node.title || node.name || '', children: children.length ? children : undefined, leaf: !children.length };
|
||
};
|
||
return roots.map(item => normalize(item));
|
||
},
|
||
extractChinaRegionOptions(regions = []) {
|
||
const china = regions.find(item => ['中国', '中华人民共和国'].includes(item.title));
|
||
const chinaChildren = china?.children || [];
|
||
// 部分行政区划数据的省级节点 parentId 为 0,另一部分挂在“中国(+86)”下,需合并两类根节点。
|
||
const rootRegions = regions.filter(
|
||
item => item !== china && ['0', '', 'null', 'undefined'].includes(String(item.parentId ?? ''))
|
||
);
|
||
const merged = [...chinaChildren, ...rootRegions];
|
||
const seen = new Set();
|
||
return merged.filter(item => {
|
||
const key = String(item.id ?? item.value ?? item.title ?? '');
|
||
if (seen.has(key)) return false;
|
||
seen.add(key);
|
||
return true;
|
||
});
|
||
},
|
||
findRegionPathByName(options = [], regionName = '', parents = []) {
|
||
const target = String(regionName || '').replace(/\s+/g, '');
|
||
if (!target) return [];
|
||
for (const item of options) {
|
||
const nextParents = [...parents, item];
|
||
const name = nextParents.map(region => region.title || region.name || '').join('');
|
||
if (name === target) return nextParents.map(region => region.id);
|
||
const path = this.findRegionPathByName(item.children, target, nextParents);
|
||
if (path.length) return path;
|
||
}
|
||
return [];
|
||
},
|
||
findRegionPathByCode(options = [], regionCode = '', parents = []) {
|
||
const target = String(regionCode || '').trim();
|
||
if (!target) return [];
|
||
for (const item of options) {
|
||
const nextParents = [...parents, item];
|
||
if (String(item.id) === target) return nextParents.map(region => region.id);
|
||
const path = this.findRegionPathByCode(item.children, target, nextParents);
|
||
if (path.length) return path;
|
||
}
|
||
return [];
|
||
},
|
||
regionLabels(path = []) {
|
||
let options = this.regionOptions;
|
||
const labels = [];
|
||
path.forEach(value => {
|
||
const option = options.find(item => String(item.id) === String(value));
|
||
if (!option) return;
|
||
labels.push(option.title || option.name || '');
|
||
options = option.children || [];
|
||
});
|
||
return labels;
|
||
},
|
||
setAddressRegionNames(field, labels = []) {
|
||
if (!field) return;
|
||
const normalized = labels.map(label => String(label || '').trim()).filter(Boolean);
|
||
const province = normalized.find(label => /省|自治区|特别行政区/.test(label)) || normalized[0] || '';
|
||
const city = normalized.filter(label => /市|州|盟/.test(label)).at(-1) || '';
|
||
const district = normalized.filter(label => /区|县|旗/.test(label)).at(-1) || '';
|
||
if (field.provinceName) field.model[field.provinceName] = province;
|
||
if (field.cityName) field.model[field.cityName] = city;
|
||
if (field.districtName) field.model[field.districtName] = district;
|
||
},
|
||
setAddressRegionNamesFromRow(field, row = {}) {
|
||
const labels = [row.provinceName, row.cityName, row.districtName].filter(Boolean);
|
||
if (labels.length) {
|
||
this.setAddressRegionNames(field, labels);
|
||
return;
|
||
}
|
||
const text = String(row.regionName || '').replace(/\s+/g, '');
|
||
if (!text) return;
|
||
const provinceMatch = text.match(/^.*?(?:省|自治区|特别行政区)/);
|
||
const rest = provinceMatch ? text.slice(provinceMatch[0].length) : text;
|
||
const cityMatch = rest.match(/.+?(?:市|州|盟)/);
|
||
const districtMatch = cityMatch
|
||
? rest.slice(cityMatch[0].length).match(/^[^市州盟]*?(?:区|县|旗)/)
|
||
: rest.match(/.+?(?:区|县|旗)/);
|
||
this.setAddressRegionNames(field, [provinceMatch?.[0], cityMatch?.[0], districtMatch?.[0]]);
|
||
},
|
||
handleAddressRegionChange(target, value) {
|
||
const field = this.addressModel(target);
|
||
const path = Array.isArray(value) ? value.map(item => String(item)) : [];
|
||
this.addressRegionPaths = { ...this.addressRegionPaths, [target]: path };
|
||
if (!field) return;
|
||
const labels = this.regionLabels(path);
|
||
field.model[field.name] = labels.join('');
|
||
field.model[field.regionCode] = path.at(-1) || '';
|
||
field.model[field.siteCode] = '';
|
||
this.setAddressRegionNames(field, labels);
|
||
},
|
||
syncAddressRegionPath(target) {
|
||
const field = this.addressModel(target);
|
||
if (!field) return;
|
||
const codePath = this.findRegionPathByCode(this.regionOptions, field.model[field.regionCode]);
|
||
const path = codePath.length
|
||
? codePath
|
||
: this.findRegionPathByName(this.regionOptions, field.model[field.name]);
|
||
this.addressRegionPaths = { ...this.addressRegionPaths, [target]: path };
|
||
if (path.length) this.setAddressRegionNames(field, this.regionLabels(path));
|
||
},
|
||
syncAddressRegionPaths() {
|
||
if (!this.regionOptions.length) return;
|
||
['departure', 'arrival', ...this.form.routes.map((route, index) => `route-${index}`)].forEach(target => {
|
||
this.syncAddressRegionPath(target);
|
||
});
|
||
},
|
||
async loadDetail(id) {
|
||
const res = await api.getDetail(id);
|
||
const detail = this.responseData(res);
|
||
const routes = detail.routes || [];
|
||
const finalSegment = routes[routes.length - 1];
|
||
const hasFinalSegment =
|
||
finalSegment &&
|
||
finalSegment.departureAddress === detail.arrivalAddress &&
|
||
finalSegment.departureName === detail.arrivalName;
|
||
this.form = {
|
||
...detail,
|
||
planStartTime: this.normalizeDateValue(detail.planStartTime),
|
||
planEndTime: this.normalizeDateValue(detail.planEndTime),
|
||
routes: hasFinalSegment ? routes.slice(0, -1) : routes.length ? routes : [this.createRoute(0)],
|
||
finalTransportType: hasFinalSegment
|
||
? finalSegment.transportType
|
||
: detail.finalTransportType || '',
|
||
goods: detail.goods || [{ quantityUnit: quantityUnitOptions[0] }],
|
||
attachmentsJson: detail.attachmentsJson || '[]',
|
||
transportOrganizationType: '多式联运',
|
||
};
|
||
this.syncAddressRegionPaths();
|
||
this.restoreGoodsCargoTypePaths();
|
||
this.attachmentRows = this.parseJsonArray(this.form.attachmentsJson);
|
||
this.selectedAttachmentRows = [];
|
||
if (this.form.projectId) await this.loadContracts(this.form.projectId, false);
|
||
},
|
||
parseJsonArray(value) {
|
||
if (!value) return [];
|
||
if (Array.isArray(value)) return value;
|
||
try {
|
||
const data = JSON.parse(value);
|
||
return Array.isArray(data) ? data : [];
|
||
} catch (error) {
|
||
return [];
|
||
}
|
||
},
|
||
normalizeDateValue(value) {
|
||
if (!value) return '';
|
||
if (value instanceof Date) {
|
||
const year = value.getFullYear();
|
||
const month = String(value.getMonth() + 1).padStart(2, '0');
|
||
const day = String(value.getDate()).padStart(2, '0');
|
||
return `${year}-${month}-${day}`;
|
||
}
|
||
return String(value).replace('T', ' ').slice(0, 10);
|
||
},
|
||
normalizeDateTimeValue(value, endOfDay = false) {
|
||
const date = this.normalizeDateValue(value);
|
||
return date ? `${date} ${endOfDay ? '23:59:59' : '00:00:00'}` : '';
|
||
},
|
||
getRoadRegionDisplay(target, fallback) {
|
||
const labels = this.regionLabels(this.addressRegionPaths[target] || []);
|
||
return labels.at(-1) || fallback;
|
||
},
|
||
getAddressDisplay(target, fallback) {
|
||
const field = this.addressModel(target);
|
||
const name = field?.model?.[field.name] || fallback;
|
||
return this.isNonRoadAddress(target) ? name : this.getRoadRegionDisplay(target, name);
|
||
},
|
||
getRouteTitle(index) {
|
||
const departureName =
|
||
index === 0
|
||
? this.getAddressDisplay(
|
||
'departure',
|
||
this.form.departureName || '发货地址'
|
||
)
|
||
: this.getAddressDisplay(
|
||
`route-${index - 1}`,
|
||
this.form.routes[index - 1].departureName || '途经地'
|
||
);
|
||
const arrivalName = this.getAddressDisplay(
|
||
`route-${index}`,
|
||
this.form.routes[index].departureName || '途经地'
|
||
);
|
||
return `段${index + 1}:${departureName} → ${arrivalName}`;
|
||
},
|
||
truncateRouteAddress(value) {
|
||
const text = String(value || '');
|
||
return text.length > 6 ? `${text.slice(0, 6)}...` : text;
|
||
},
|
||
formatRouteTitle(title, segmentNo) {
|
||
const fullTitle = String(title || '').replace(`段${segmentNo}:`, '');
|
||
const separator = ' → ';
|
||
const separatorIndex = fullTitle.indexOf(separator);
|
||
if (separatorIndex < 0) return this.truncateRouteAddress(fullTitle);
|
||
const departure = fullTitle.slice(0, separatorIndex);
|
||
const arrival = fullTitle.slice(separatorIndex + separator.length);
|
||
return `${this.truncateRouteAddress(departure)}${separator}${this.truncateRouteAddress(arrival)}`;
|
||
},
|
||
getFinalRouteTitle() {
|
||
const departureName = this.getAddressDisplay(
|
||
`route-${this.form.routes.length - 1}`,
|
||
this.lastRouteName
|
||
);
|
||
const arrivalName = this.getAddressDisplay(
|
||
'arrival',
|
||
this.form.arrivalName || '收货地址'
|
||
);
|
||
return `段${this.form.routes.length + 1}:${departureName} → ${arrivalName}`;
|
||
},
|
||
responseRecords(response) {
|
||
const data = response?.data?.data || response?.data || response;
|
||
return Array.isArray(data) ? data : data?.records || [];
|
||
},
|
||
async loadProjects() {
|
||
const response = await api.getAvailableProjects();
|
||
this.projects = this.responseRecords(response);
|
||
},
|
||
async loadContracts(projectId, selectFirst = true) {
|
||
const response = await api.getProjectContracts(projectId);
|
||
this.contracts = this.responseRecords(response).filter(
|
||
item =>
|
||
String(item.projectId) === String(projectId) && item.contractCategory === '客户合同'
|
||
);
|
||
if (selectFirst) {
|
||
const first = this.contracts[0];
|
||
this.form.contractId = first?.id;
|
||
this.form.contractName = first?.contractName || '';
|
||
this.form.customerName = first?.partyA || '';
|
||
}
|
||
},
|
||
async projectChange(projectId) {
|
||
const project = this.projects.find(item => String(item.id) === String(projectId));
|
||
this.form.projectName = project?.projectName || '';
|
||
this.form.customerName = project?.customerNames || '';
|
||
this.form.contractId = '';
|
||
this.form.contractName = '';
|
||
await this.loadContracts(projectId);
|
||
},
|
||
contractChange(contractId) {
|
||
const contract = this.contracts.find(item => String(item.id) === String(contractId));
|
||
this.form.contractName = contract?.contractName || '';
|
||
this.form.customerName = contract?.partyA || '';
|
||
},
|
||
addressModel(target) {
|
||
if (target.startsWith('route-')) {
|
||
const route = this.form.routes[Number(target.slice(6))];
|
||
return route
|
||
? {
|
||
model: route,
|
||
name: 'departureName',
|
||
address: 'departureAddress',
|
||
contact: 'departureContact',
|
||
phone: 'departurePhone',
|
||
longitude: 'departureLongitude',
|
||
latitude: 'departureLatitude',
|
||
regionCode: 'departureRegionCode',
|
||
siteCode: 'departureSiteCode',
|
||
provinceName: 'departureProvinceName',
|
||
cityName: 'departureCityName',
|
||
districtName: 'departureDistrictName',
|
||
}
|
||
: null;
|
||
}
|
||
const prefix = target === 'arrival' ? 'arrival' : 'departure';
|
||
return {
|
||
model: this.form,
|
||
name: `${prefix}Name`,
|
||
address: `${prefix}Address`,
|
||
contact: `${prefix}Contact`,
|
||
phone: `${prefix}Phone`,
|
||
longitude: `${prefix}Longitude`,
|
||
latitude: `${prefix}Latitude`,
|
||
regionCode: `${prefix}RegionCode`,
|
||
siteCode: `${prefix}SiteCode`,
|
||
provinceName: `${prefix}ProvinceName`,
|
||
cityName: `${prefix}CityName`,
|
||
districtName: `${prefix}DistrictName`,
|
||
};
|
||
},
|
||
addressTransportType(target) {
|
||
if (target.startsWith('route-')) {
|
||
return this.form.routes[Number(target.slice(6))]?.transportType || '';
|
||
}
|
||
return target === 'arrival' ? this.form.finalTransportType : this.form.routes[0]?.transportType || '';
|
||
},
|
||
addressTransportMode(target) {
|
||
const type = this.addressTransportType(target);
|
||
if (type.includes('铁路')) return 'rail';
|
||
if (type.includes('水路') || type.includes('水运') || type.includes('海运')) return 'water';
|
||
if (type.includes('航空') || type.includes('空运')) return 'air';
|
||
return 'road';
|
||
},
|
||
isRoadTransportType(type) {
|
||
const value = String(type || '').trim().toLowerCase();
|
||
return value === 'road' || value.includes('公路');
|
||
},
|
||
isNonRoadAddress(target) {
|
||
const field = this.addressModel(target);
|
||
if (!field) return false;
|
||
const siteCode = String(field.model[field.siteCode] || '').trim();
|
||
// 地址展示模式由实际选择的数据决定:有站点编码表示非公路站点名称,
|
||
// 没有站点编码则保留省市区级联,避免切换运输方式时改写已有地址。
|
||
return Boolean(siteCode && siteCode !== '/');
|
||
},
|
||
resolveAddressRegionCode(row = {}) {
|
||
return row.regionCode || row.districtCode || row.areaCode || row.adcode || '';
|
||
},
|
||
openCommonAddress(target) {
|
||
this.addressTarget = target;
|
||
if (this.addressTransportMode(target) !== 'road') {
|
||
this.stationQuery = {};
|
||
this.stationPage.current = 1;
|
||
this.stationDialogVisible = true;
|
||
return;
|
||
}
|
||
this.addressQuery = {};
|
||
this.addressPage.current = 1;
|
||
this.addressDialogVisible = true;
|
||
},
|
||
stationListRequest() {
|
||
return {
|
||
rail: getRailwayStationList,
|
||
water: getPortTerminalList,
|
||
air: getAirportMasterList,
|
||
}[this.addressTransportMode(this.addressTarget)];
|
||
},
|
||
normalizeStation(row = {}) {
|
||
return {
|
||
...row,
|
||
stationName: row.name || row.stationName || row.addressName || '',
|
||
stationType: row.category || this.stationTypeLabel,
|
||
stationCode: row.code || row.tmisCode || row.iataCode || row.icaoCode || row.siteCode || '',
|
||
detailAddress: row.detailAddress || row.address || '',
|
||
regionName:
|
||
row.regionName || [row.provinceName, row.cityName, row.districtName].filter(Boolean).join(''),
|
||
};
|
||
},
|
||
async loadStationList() {
|
||
const request = this.stationListRequest();
|
||
if (!request) return;
|
||
this.stationLoading = true;
|
||
try {
|
||
const response = await request(this.stationPage.current, this.stationPage.size, this.stationQuery);
|
||
const data = response?.data?.data || response?.data || response || {};
|
||
this.stationRows = (data.records || []).map(row => this.normalizeStation(row));
|
||
this.stationPage.total = data.total || 0;
|
||
} finally {
|
||
this.stationLoading = false;
|
||
}
|
||
},
|
||
searchStationList() {
|
||
this.stationPage.current = 1;
|
||
this.loadStationList();
|
||
},
|
||
resetStationList() {
|
||
this.stationQuery = {};
|
||
this.stationPage.current = 1;
|
||
this.loadStationList();
|
||
},
|
||
selectStation(row) {
|
||
const target = this.addressModel(this.addressTarget);
|
||
if (!target) return;
|
||
const station = this.normalizeStation(row);
|
||
target.model[target.name] = station.stationName || station.regionName;
|
||
target.model[target.address] = station.detailAddress;
|
||
target.model[target.longitude] = station.longitude || '';
|
||
target.model[target.latitude] = station.latitude || '';
|
||
target.model[target.regionCode] = this.resolveAddressRegionCode(station);
|
||
target.model[target.siteCode] = station.stationCode;
|
||
this.setAddressRegionNamesFromRow(target, station);
|
||
this.stationDialogVisible = false;
|
||
this.loadRegionOptions().then(() => this.syncAddressRegionPath(this.addressTarget));
|
||
},
|
||
async loadAddressList() {
|
||
this.addressLoading = true;
|
||
try {
|
||
const response = await getCommonAddressList(
|
||
this.addressPage.current,
|
||
this.addressPage.size,
|
||
{ ...this.addressQuery, allDept: 0 }
|
||
);
|
||
const data = response?.data?.data || response?.data || response || {};
|
||
this.addressRows = data.records || [];
|
||
this.addressPage.total = data.total || 0;
|
||
} finally {
|
||
this.addressLoading = false;
|
||
}
|
||
},
|
||
searchAddressList() {
|
||
this.addressPage.current = 1;
|
||
this.loadAddressList();
|
||
},
|
||
resetAddressList() {
|
||
this.addressQuery = {};
|
||
this.addressPage.current = 1;
|
||
this.loadAddressList();
|
||
},
|
||
selectAddress(row) {
|
||
const target = this.addressModel(this.addressTarget);
|
||
if (!target) return;
|
||
target.model[target.name] = row.regionName || row.addressName || '';
|
||
target.model[target.address] = row.detailAddress || row.address || '';
|
||
target.model[target.contact] = row.contactName || target.model[target.contact] || '';
|
||
target.model[target.phone] = row.contactPhone || target.model[target.phone] || '';
|
||
target.model[target.longitude] = row.longitude || '';
|
||
target.model[target.latitude] = row.latitude || '';
|
||
target.model[target.regionCode] = this.resolveAddressRegionCode(row);
|
||
target.model[target.siteCode] = '';
|
||
this.setAddressRegionNamesFromRow(target, row);
|
||
this.addressDialogVisible = false;
|
||
this.loadRegionOptions().then(() => this.syncAddressRegionPath(this.addressTarget));
|
||
},
|
||
handleAddressDetailClick(target) {
|
||
if (this.addressTransportMode(target) !== 'road') {
|
||
this.openCommonAddress(target);
|
||
return;
|
||
}
|
||
this.openAddressMap(target);
|
||
},
|
||
openAddressMap(target) {
|
||
if (this.addressTransportMode(target) !== 'road') {
|
||
this.$message.info('非公路运输地址不能使用地图选择');
|
||
return;
|
||
}
|
||
const field = this.addressModel(target);
|
||
if (!field) return;
|
||
this.mapTarget = target;
|
||
this.mapKeyword = field.model[field.address] || field.model[field.name] || '';
|
||
this.mapSelected = {
|
||
longitude: field.model[field.longitude] || '',
|
||
latitude: field.model[field.latitude] || '',
|
||
regionCode: field.model[field.regionCode] || '',
|
||
detailAddress: this.mapKeyword,
|
||
regionName: field.model[field.name] || '',
|
||
};
|
||
this.mapStatus = this.mapSelected.longitude
|
||
? '已加载当前选点,可重新选点'
|
||
: '可搜索地址或点击地图选点';
|
||
this.mapDialogVisible = true;
|
||
},
|
||
loadAmap() {
|
||
if (window.AMap?.Map) return Promise.resolve();
|
||
if (!amapLoader) {
|
||
amapLoader = new Promise((resolve, reject) => {
|
||
window._AMapSecurityConfig = { securityJsCode: AMAP_SECURITY_CODE };
|
||
const script = document.createElement('script');
|
||
script.src = `https://webapi.amap.com/maps?v=2.0&key=${AMAP_KEY}`;
|
||
script.onload = resolve;
|
||
script.onerror = reject;
|
||
document.body.appendChild(script);
|
||
});
|
||
}
|
||
return amapLoader;
|
||
},
|
||
initAddressMap() {
|
||
this.loadAmap()
|
||
.then(() =>
|
||
this.$nextTick(() => {
|
||
const center = this.mapSelected.longitude
|
||
? [Number(this.mapSelected.longitude), Number(this.mapSelected.latitude)]
|
||
: [116.40769, 39.89945];
|
||
if (!this.addressMapInstance) {
|
||
this.addressMapInstance = new window.AMap.Map(this.$refs.addressMap, {
|
||
center,
|
||
zoom: this.mapSelected.longitude ? 14 : 11,
|
||
});
|
||
window.AMap.plugin(['AMap.Geocoder'], () => {
|
||
this.addressMapGeocoder = new window.AMap.Geocoder();
|
||
});
|
||
this.addressMapInstance.on('click', event => this.pickAddressMap(event.lnglat));
|
||
} else this.addressMapInstance.resize();
|
||
if (this.mapSelected.longitude) this.renderAddressMarker(center);
|
||
})
|
||
)
|
||
.catch(() => this.$message.error('高德地图组件加载失败'));
|
||
},
|
||
searchAddressMap() {
|
||
if (!this.mapKeyword.trim()) return this.$message.warning('请输入地址关键词');
|
||
this.loadAmap()
|
||
.then(() => this.ensureAddressGeocoder())
|
||
.then(
|
||
() =>
|
||
new Promise((resolve, reject) =>
|
||
this.addressMapGeocoder.getLocation(this.mapKeyword, (status, result) =>
|
||
status === 'complete' ? resolve(result) : reject(result)
|
||
)
|
||
)
|
||
)
|
||
.then(result => {
|
||
const geocodes = result?.geocodes || [];
|
||
this.mapSearchResults = geocodes.map((item, index) => ({
|
||
id: item.id || index,
|
||
name: item.formattedAddress || this.mapKeyword,
|
||
address: item.formattedAddress || this.mapKeyword,
|
||
location: item.location,
|
||
}));
|
||
const point = geocodes[0]?.location;
|
||
if (!point) return this.$message.warning('地图搜索无匹配地址');
|
||
this.pickAddressMap(point, this.mapKeyword);
|
||
})
|
||
.catch(() => this.$message.error('地图搜索失败'));
|
||
},
|
||
selectAddressMapSearchResult(item) {
|
||
if (item && item.location) {
|
||
this.pickAddressMap(item.location, item.address || item.name || '');
|
||
}
|
||
},
|
||
ensureAddressGeocoder() {
|
||
if (this.addressMapGeocoder) return Promise.resolve(this.addressMapGeocoder);
|
||
return new Promise(resolve =>
|
||
window.AMap.plugin(['AMap.Geocoder'], () => {
|
||
this.addressMapGeocoder = new window.AMap.Geocoder();
|
||
resolve(this.addressMapGeocoder);
|
||
})
|
||
);
|
||
},
|
||
pickAddressMap(point, keyword) {
|
||
const longitude = point.getLng ? point.getLng() : point.lng;
|
||
const latitude = point.getLat ? point.getLat() : point.lat;
|
||
this.mapSelected = {
|
||
longitude,
|
||
latitude,
|
||
detailAddress: keyword || this.mapKeyword,
|
||
regionName: '',
|
||
regionCode: '',
|
||
};
|
||
this.renderAddressMarker([longitude, latitude]);
|
||
this.mapStatus = '已选点,可确认回填';
|
||
this.ensureAddressGeocoder().then(() =>
|
||
this.addressMapGeocoder.getAddress([longitude, latitude], (status, result) => {
|
||
if (status === 'complete') {
|
||
const component = result.regeocode?.addressComponent || {};
|
||
this.mapSelected.detailAddress =
|
||
result.regeocode?.formattedAddress || this.mapSelected.detailAddress;
|
||
this.mapSelected.provinceName = component.province || '';
|
||
this.mapSelected.cityName = component.city || '';
|
||
this.mapSelected.districtName = component.district || '';
|
||
this.mapSelected.regionName = [component.province, component.city, component.district]
|
||
.filter(Boolean)
|
||
.join('');
|
||
this.mapSelected.regionCode = component.adcode || '';
|
||
this.mapKeyword = this.mapSelected.detailAddress;
|
||
this.mapStatus = '已解析地址,可确认回填';
|
||
}
|
||
})
|
||
);
|
||
},
|
||
renderAddressMarker(position) {
|
||
if (!this.addressMapInstance) return;
|
||
this.addressMapMarker?.setMap(null);
|
||
this.addressMapMarker = new window.AMap.Marker({ position });
|
||
this.addressMapMarker.setMap(this.addressMapInstance);
|
||
this.addressMapInstance.setCenter(position);
|
||
},
|
||
confirmAddressMap() {
|
||
const target = this.addressModel(this.mapTarget);
|
||
if (!target || !this.mapSelected.longitude) return;
|
||
target.model[target.name] = this.mapSelected.regionName || target.model[target.name] || '';
|
||
target.model[target.address] = this.mapSelected.detailAddress || this.mapKeyword;
|
||
target.model[target.longitude] = this.mapSelected.longitude;
|
||
target.model[target.latitude] = this.mapSelected.latitude;
|
||
target.model[target.regionCode] = this.mapSelected.regionCode || '';
|
||
target.model[target.siteCode] = '';
|
||
this.setAddressRegionNamesFromRow(target, this.mapSelected);
|
||
this.mapDialogVisible = false;
|
||
this.syncAddressRegionPath(this.mapTarget);
|
||
},
|
||
openCommonCargoDialog() {
|
||
this.commonCargoQuery = {};
|
||
this.commonCargoSelected = [];
|
||
this.commonCargoPage.current = 1;
|
||
this.commonCargoVisible = true;
|
||
},
|
||
async loadCommonCargoList() {
|
||
this.commonCargoSelected = [];
|
||
this.commonCargoLoading = true;
|
||
try {
|
||
const { cargoTypePath, ...query } = this.commonCargoQuery;
|
||
const response = await getCommonCargoList(this.commonCargoPage.current, this.commonCargoPage.size, { ...query, allDept: 0 });
|
||
const data = response?.data?.data || response?.data || response || {};
|
||
this.commonCargoRows = data.records || [];
|
||
this.commonCargoPage.total = data.total || 0;
|
||
} finally {
|
||
this.commonCargoLoading = false;
|
||
}
|
||
},
|
||
searchCommonCargoList() {
|
||
this.commonCargoPage.current = 1;
|
||
this.loadCommonCargoList();
|
||
},
|
||
resetCommonCargoList() {
|
||
this.commonCargoQuery = {};
|
||
this.commonCargoPage.current = 1;
|
||
this.loadCommonCargoList();
|
||
},
|
||
handleCommonCargoTypeChange(value) {
|
||
const path = Array.isArray(value) ? value.map(item => String(item)) : [];
|
||
const firstCargoType = this.getCargoTypeByPath(path.slice(0, 1));
|
||
const secondCargoType = path.length > 1 ? this.getCargoTypeByPath(path) : null;
|
||
this.commonCargoQuery = {
|
||
...this.commonCargoQuery,
|
||
cargoTypePath: path,
|
||
firstCargoTypeName: firstCargoType?.label || '',
|
||
firstCargoTypeCode: firstCargoType?.cargoCode || firstCargoType?.code || '',
|
||
secondCargoTypeName: secondCargoType?.label || '',
|
||
secondCargoTypeCode: secondCargoType?.cargoCode || secondCargoType?.code || '',
|
||
};
|
||
},
|
||
handleCommonCargoSelectionChange(rows) {
|
||
this.commonCargoSelected = rows || [];
|
||
},
|
||
async loadCargoTypeOptions() {
|
||
if (this.cargoTypeOptions.length || this.cargoTypeLoading) return;
|
||
this.cargoTypeLoading = true;
|
||
try {
|
||
const response = await getCargoTypeList(1, 9999, { allDept: 0 });
|
||
const rows = this.responseRecords(response);
|
||
this.cargoTypeOptions = this.buildCargoTypeTree(rows);
|
||
} finally {
|
||
this.cargoTypeLoading = false;
|
||
}
|
||
},
|
||
buildCargoTypeTree(rows = []) {
|
||
const nodes = new Map();
|
||
rows.forEach(row => {
|
||
const id = String(row.id);
|
||
nodes.set(id, {
|
||
...row,
|
||
id,
|
||
label: row.cargoName || row.cargoTypeName || row.name || row.cargoCode || '',
|
||
children: [],
|
||
});
|
||
});
|
||
const roots = [];
|
||
nodes.forEach(node => {
|
||
const parentId = node.parentId == null ? '' : String(node.parentId);
|
||
if (parentId && parentId !== '0' && nodes.has(parentId)) {
|
||
nodes.get(parentId).children.push(node);
|
||
} else {
|
||
roots.push(node);
|
||
}
|
||
});
|
||
this.cargoTypeFlatOptions = [];
|
||
const normalize = (node, path = []) => {
|
||
const nextPath = [...path, node.id];
|
||
const children = node.children.map(child => normalize(child, nextPath));
|
||
const normalized = { ...node, children };
|
||
this.cargoTypeFlatOptions.push({ ...normalized, path: nextPath });
|
||
return normalized;
|
||
};
|
||
return roots.map(node => normalize(node));
|
||
},
|
||
getCargoTypeByPath(path = []) {
|
||
const normalizedPath = Array.isArray(path) ? path.map(value => String(value)) : [];
|
||
const id = normalizedPath.at(-1);
|
||
return this.cargoTypeFlatOptions.find(item => item.id === id) || null;
|
||
},
|
||
getCargoOptionsKey(path = []) {
|
||
const cargoType = this.getCargoTypeByPath(path);
|
||
return String(cargoType?.cargoCode || cargoType?.code || cargoType?.id || '');
|
||
},
|
||
getCargoOptionsByType(path = []) {
|
||
const key = this.getCargoOptionsKey(path);
|
||
return key ? this.cargoOptionsMap[key] || [] : [];
|
||
},
|
||
isCargoOptionsLoading(path = []) {
|
||
const key = this.getCargoOptionsKey(path);
|
||
return key ? Boolean(this.cargoOptionsLoadingMap[key]) : false;
|
||
},
|
||
async loadCargoOptionsByType(path = []) {
|
||
const cargoType = this.getCargoTypeByPath(path);
|
||
const key = this.getCargoOptionsKey(path);
|
||
if (!key || !cargoType) return [];
|
||
if (this.cargoOptionsMap[key] || this.cargoOptionsLoadingMap[key]) {
|
||
return this.cargoOptionsMap[key] || [];
|
||
}
|
||
this.cargoOptionsLoadingMap = { ...this.cargoOptionsLoadingMap, [key]: true };
|
||
try {
|
||
const response = await getCommonCargoList(1, 500, {
|
||
allDept: 0,
|
||
secondCargoTypeName: cargoType.label,
|
||
secondCargoTypeCode: cargoType.cargoCode || cargoType.code || '',
|
||
});
|
||
const options = this.responseRecords(response);
|
||
this.cargoOptionsMap = { ...this.cargoOptionsMap, [key]: options };
|
||
return options;
|
||
} finally {
|
||
this.cargoOptionsLoadingMap = { ...this.cargoOptionsLoadingMap, [key]: false };
|
||
}
|
||
},
|
||
async fetchCargoSuggestions(queryString, callback, path = []) {
|
||
const keyword = String(queryString || '').trim();
|
||
const cargoType = this.getCargoTypeByPath(path);
|
||
const key = this.getCargoOptionsKey(path);
|
||
if (!keyword && !cargoType) {
|
||
callback([]);
|
||
return;
|
||
}
|
||
if (key) this.cargoOptionsLoadingMap = { ...this.cargoOptionsLoadingMap, [key]: true };
|
||
try {
|
||
const response = await getCommonCargoList(1, 50, {
|
||
allDept: 0,
|
||
...(cargoType
|
||
? {
|
||
secondCargoTypeName: cargoType.label,
|
||
secondCargoTypeCode: cargoType.cargoCode || cargoType.code || '',
|
||
}
|
||
: {}),
|
||
...(keyword ? { cargoName: keyword } : {}),
|
||
});
|
||
const options = this.responseRecords(response);
|
||
if (key) this.cargoOptionsMap = { ...this.cargoOptionsMap, [key]: options };
|
||
callback(
|
||
options.map(item => ({
|
||
...item,
|
||
value: item.cargoName || item.name || '',
|
||
}))
|
||
);
|
||
} catch (error) {
|
||
window.console.log(error);
|
||
callback([]);
|
||
} finally {
|
||
if (key) this.cargoOptionsLoadingMap = { ...this.cargoOptionsLoadingMap, [key]: false };
|
||
}
|
||
},
|
||
selectCargoSuggestion(goods, cargo = {}) {
|
||
goods.cargoName = cargo.cargoName || cargo.name || '';
|
||
Object.assign(goods, this.normalizeCargoRow(cargo, goods.cargoTypePath));
|
||
},
|
||
selectCargoName(goods, cargoName) {
|
||
if (!cargoName) return;
|
||
const cargo = this.getCargoOptionsByType(goods.cargoTypePath).find(
|
||
item => item.cargoName === cargoName
|
||
);
|
||
if (!cargo) return;
|
||
Object.assign(goods, this.normalizeCargoRow(cargo, goods.cargoTypePath));
|
||
},
|
||
handleCargoTypeChange(goods, value) {
|
||
const path = Array.isArray(value) ? value.map(item => String(item)) : [];
|
||
const cargoType = this.getCargoTypeByPath(path);
|
||
goods.cargoTypePath = path;
|
||
goods.cargoType = cargoType?.label || '';
|
||
goods.cargoTypeCode = cargoType?.cargoCode || cargoType?.code || cargoType?.id || '';
|
||
goods.cargoName = '';
|
||
goods.specification = '';
|
||
goods.model = '';
|
||
this.loadCargoOptionsByType(path);
|
||
},
|
||
restoreGoodsCargoTypePaths() {
|
||
this.form.goods.forEach(goods => {
|
||
if (Array.isArray(goods.cargoTypePath) && goods.cargoTypePath.length) return;
|
||
const cargoType = this.cargoTypeFlatOptions.find(
|
||
item =>
|
||
String(item.cargoCode || item.code || '') === String(goods.cargoTypeCode || '') ||
|
||
item.label === goods.cargoType
|
||
);
|
||
if (cargoType) goods.cargoTypePath = cargoType.path;
|
||
});
|
||
},
|
||
selectCommonCargo(row) {
|
||
this.appendCommonCargoRows([row]);
|
||
this.commonCargoVisible = false;
|
||
},
|
||
confirmCommonCargoSelection() {
|
||
if (!this.commonCargoSelected.length) return;
|
||
this.appendCommonCargoRows(this.commonCargoSelected);
|
||
this.$message.success(`成功选择${this.commonCargoSelected.length}条常用货物`);
|
||
this.commonCargoVisible = false;
|
||
},
|
||
appendCommonCargoRows(rows = []) {
|
||
const firstGoods = this.form.goods[0];
|
||
const isFirstGoodsEmpty =
|
||
firstGoods &&
|
||
![
|
||
'cargoName',
|
||
'cargoType',
|
||
'quantity',
|
||
'packageType',
|
||
'brand',
|
||
'specification',
|
||
'model',
|
||
'materialCode',
|
||
'deviceCode',
|
||
'remark',
|
||
].some(key => firstGoods[key]);
|
||
|
||
rows.forEach((row, index) => {
|
||
const cargo = this.normalizeCargoRow(row, this.resolveCargoTypePath(row));
|
||
if (isFirstGoodsEmpty && index === 0) this.form.goods.splice(0, 1, cargo);
|
||
else this.form.goods.push(cargo);
|
||
});
|
||
},
|
||
resolveCargoTypePath(row = {}) {
|
||
return (
|
||
this.cargoTypeFlatOptions.find(
|
||
item =>
|
||
String(item.cargoCode || item.code || '') ===
|
||
String(row.secondCargoTypeCode || row.cargoTypeCode || '') ||
|
||
item.label === (row.secondCargoTypeName || row.cargoType || row.goodsType)
|
||
)?.path || []
|
||
);
|
||
},
|
||
normalizeCargoRow(row = {}, cargoTypePath = this.resolveCargoTypePath(row)) {
|
||
const cargoType = this.getCargoTypeByPath(cargoTypePath);
|
||
return {
|
||
...row,
|
||
cargoName: row.cargoName || row.goodsName || '',
|
||
cargoType:
|
||
cargoType?.label ||
|
||
row.cargoType ||
|
||
row.goodsType ||
|
||
row.secondCargoTypeName ||
|
||
row.firstCargoTypeName ||
|
||
'',
|
||
cargoTypeCode:
|
||
cargoType?.cargoCode ||
|
||
cargoType?.code ||
|
||
row.cargoTypeCode ||
|
||
row.secondCargoTypeCode ||
|
||
'',
|
||
cargoTypePath,
|
||
quantity: row.quantity || '',
|
||
quantityUnit: row.quantityUnit || quantityUnitOptions[0],
|
||
packageType: row.packageType || row.package || '',
|
||
brand: row.brand || '',
|
||
specification: row.specification || row.spec || '',
|
||
model: row.model || '',
|
||
materialCode: row.materialCode || '',
|
||
deviceCode: row.deviceCode || '',
|
||
remark: row.remark || row.descriptionOne || '',
|
||
};
|
||
},
|
||
async handleCargoImport(file) {
|
||
const rawFile = file.raw;
|
||
if (!rawFile) return;
|
||
if (!/\.(xls|xlsx)$/i.test(rawFile.name || '')) return this.$message.warning('请上传 .xlsx、.xls 格式文件');
|
||
if (rawFile.size > 50 * 1024 * 1024) return this.$message.warning('单个文件大小不能超过50M');
|
||
this.cargoImportLoading = true;
|
||
try {
|
||
const response = await importBlob('/blade-transport/shipping-template/import-goods', rawFile);
|
||
const contentType = response.headers?.['content-type'] || response.data?.type || '';
|
||
if (contentType.includes('application/vnd.ms-excel') || contentType.includes('spreadsheetml')) {
|
||
downloadXls(response.data, `发货模板货物导入失败明细${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`);
|
||
this.$message.warning('部分数据导入失败,已下载失败明细');
|
||
return;
|
||
}
|
||
const result = JSON.parse(await response.data.text());
|
||
if (result.code !== 200) throw new Error(result.msg || '货物导入失败');
|
||
const rows = result.data || [];
|
||
if (!rows.length) return this.$message.warning('未读取到货物数据');
|
||
rows.forEach(row => this.form.goods.push(this.normalizeCargoRow(row)));
|
||
this.$message.success(`成功导入${rows.length}条货物`);
|
||
this.cargoImportVisible = false;
|
||
} catch (error) {
|
||
window.console.log(error);
|
||
this.$message.error('货物导入失败,请检查文件格式');
|
||
} finally {
|
||
this.cargoImportLoading = false;
|
||
}
|
||
},
|
||
downloadCargoTemplate() {
|
||
exportBlob('/blade-transport/shipping-template/export-goods-template').then(response => {
|
||
downloadXls(response.data, '发货模板货物导入模板.xlsx');
|
||
});
|
||
},
|
||
openRouteDialog() {
|
||
this.routeQuery = {};
|
||
this.routePage.current = 1;
|
||
this.routeDialogVisible = true;
|
||
},
|
||
async loadRouteList() {
|
||
this.routeLoading = true;
|
||
try {
|
||
const response = await getCommonRouteList(this.routePage.current, this.routePage.size, {
|
||
...this.routeQuery,
|
||
allDept: 0,
|
||
});
|
||
const data = response?.data?.data || response?.data || response || {};
|
||
this.routeRows = data.records || [];
|
||
this.routePage.total = data.total || 0;
|
||
} finally {
|
||
this.routeLoading = false;
|
||
}
|
||
},
|
||
searchRouteList() {
|
||
this.routePage.current = 1;
|
||
this.loadRouteList();
|
||
},
|
||
resetRouteList() {
|
||
this.routeQuery = {};
|
||
this.routePage.current = 1;
|
||
this.loadRouteList();
|
||
},
|
||
changeRoutePageSize(size) {
|
||
this.routePage.size = size;
|
||
this.routePage.current = 1;
|
||
this.loadRouteList();
|
||
},
|
||
changeRoutePage(current) {
|
||
this.routePage.current = current;
|
||
this.loadRouteList();
|
||
},
|
||
formatRouteUpdateUserName(row) {
|
||
return formatUpdateUserName(row);
|
||
},
|
||
selectRoute(route) {
|
||
this.form.departureName = route.departureName || '';
|
||
this.form.departureAddress = route.departureAddress || '';
|
||
this.form.departureContact = route.departureContact || '';
|
||
this.form.departurePhone = route.departurePhone || '';
|
||
this.form.arrivalName = route.arrivalName || '';
|
||
this.form.arrivalAddress = route.arrivalAddress || '';
|
||
this.form.arrivalContact = route.arrivalContact || '';
|
||
this.form.arrivalPhone = route.arrivalPhone || '';
|
||
this.syncAddressRegionPaths();
|
||
this.form.finalTransportType = '公路运输';
|
||
if (!this.form.routes.length) this.addRoute();
|
||
this.routeDialogVisible = false;
|
||
},
|
||
createRoute(index = this.form.routes.length) {
|
||
return { segmentNo: `段${index + 1}`, transportType: '' };
|
||
},
|
||
addRoute() {
|
||
this.form.routes.push(this.createRoute());
|
||
},
|
||
removeRoute(index) {
|
||
this.form.routes.splice(index, 1);
|
||
this.syncAddressRegionPaths();
|
||
},
|
||
addGoods(index) {
|
||
this.form.goods.splice(index + 1, 0, {
|
||
quantityUnit: quantityUnitOptions[0],
|
||
materialCode: '',
|
||
deviceCode: '',
|
||
remark: '',
|
||
});
|
||
},
|
||
removeGoods(index) {
|
||
if (this.form.goods.length > 1) this.form.goods.splice(index, 1);
|
||
},
|
||
handleQuantityInput(row, value) {
|
||
const text = String(value || '').replace(/[^\d.]/g, '');
|
||
const [integer = '', ...decimals] = text.split('.');
|
||
row.quantity = decimals.length ? `${integer}.${decimals.join('').slice(0, 3)}` : integer;
|
||
},
|
||
syncAttachmentsJson() {
|
||
this.form.attachmentsJson = JSON.stringify(this.attachmentRows);
|
||
},
|
||
handleAttachmentChange(list) {
|
||
const userName = this.userInfo?.realName || this.userInfo?.userName || '';
|
||
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||
this.attachmentRows = (list || []).map(item => ({
|
||
...item,
|
||
description: item.description || '',
|
||
uploadUserName: item.uploadUserName || userName,
|
||
uploadTime: item.uploadTime || uploadTime,
|
||
}));
|
||
this.selectedAttachmentRows = [];
|
||
this.syncAttachmentsJson();
|
||
},
|
||
handleAttachmentSelectionChange(rows) {
|
||
this.selectedAttachmentRows = rows || [];
|
||
},
|
||
removeAttachment(index) {
|
||
this.attachmentRows.splice(index, 1);
|
||
this.selectedAttachmentRows = this.selectedAttachmentRows.filter(item =>
|
||
this.attachmentRows.includes(item)
|
||
);
|
||
this.syncAttachmentsJson();
|
||
},
|
||
formatFileSize(size) {
|
||
const number = Number(size);
|
||
if (!number) return '';
|
||
if (number < 1024) return `${number}B`;
|
||
if (number < 1024 * 1024) return `${(number / 1024).toFixed(1)}KB`;
|
||
return `${(number / 1024 / 1024).toFixed(1)}MB`;
|
||
},
|
||
attachmentUrl(row = {}) {
|
||
return row.url || row.link || row.fileUrl || row.domain || '';
|
||
},
|
||
attachmentName(row = {}) {
|
||
return row.originalName || row.name || row.fileName || '附件';
|
||
},
|
||
attachmentExtension(row = {}) {
|
||
const source = String(this.attachmentName(row) || this.attachmentUrl(row)).split('?')[0];
|
||
const index = source.lastIndexOf('.');
|
||
return index > -1 ? source.slice(index + 1).toLowerCase() : '';
|
||
},
|
||
isAttachmentImage(row) {
|
||
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(this.attachmentExtension(row));
|
||
},
|
||
previewAttachment(row) {
|
||
const url = this.attachmentUrl(row);
|
||
if (!url) {
|
||
this.$message.warning('附件地址为空,无法预览');
|
||
return;
|
||
}
|
||
if (this.isAttachmentImage(row)) {
|
||
this.attachmentImagePreviewUrls = this.attachmentRows
|
||
.filter(item => this.isAttachmentImage(item) && this.attachmentUrl(item))
|
||
.map(item => this.attachmentUrl(item));
|
||
this.attachmentImagePreviewIndex = Math.max(this.attachmentImagePreviewUrls.indexOf(url), 0);
|
||
this.attachmentImagePreviewVisible = true;
|
||
return;
|
||
}
|
||
this.attachmentPreviewFile = {
|
||
name: this.attachmentName(row),
|
||
url,
|
||
mimeType: row.mimeType || row.contentType || '',
|
||
};
|
||
this.attachmentDocumentPreviewVisible = true;
|
||
},
|
||
handleAttachmentPreviewUnsupported() {
|
||
this.$message.warning('当前文件暂不支持在线预览');
|
||
},
|
||
handleAttachmentPreviewError() {
|
||
this.$message.error('附件预览失败');
|
||
},
|
||
downloadAttachment(row) {
|
||
const url = this.attachmentUrl(row);
|
||
if (!url) {
|
||
this.$message.warning('附件地址为空');
|
||
return;
|
||
}
|
||
downloadFileByUrl(url, this.attachmentName(row));
|
||
},
|
||
handleAttachmentBatchDownload() {
|
||
const rows = this.selectedAttachmentRows.length
|
||
? this.selectedAttachmentRows
|
||
: this.attachmentRows;
|
||
rows.forEach(row => this.downloadAttachment(row));
|
||
},
|
||
async save(draft = false) {
|
||
const phoneFields = [
|
||
[this.form.departurePhone, '发货联系方式'],
|
||
...this.form.routes.map((route, index) => [
|
||
route.departurePhone,
|
||
`第${index + 1}个途经地联系方式`,
|
||
]),
|
||
[this.form.arrivalPhone, '收货联系方式'],
|
||
];
|
||
const invalidPhone = phoneFields.find(
|
||
([value]) => String(value || '').trim() && !isMobile(value)
|
||
);
|
||
if (invalidPhone) {
|
||
this.$message.warning(`${invalidPhone[1]}格式不正确`);
|
||
return false;
|
||
}
|
||
if (!draft) {
|
||
await this.$refs.form.validate();
|
||
if (!this.form.departureAddress || !this.form.arrivalAddress)
|
||
return this.$message.warning('请填写收发货地址');
|
||
const invalidGoodsIndex = this.form.goods.findIndex(
|
||
item =>
|
||
!String(item.cargoType || '').trim() ||
|
||
!(Number(item.quantity) > 0) ||
|
||
!String(item.quantityUnit || '').trim()
|
||
);
|
||
if (invalidGoodsIndex >= 0)
|
||
return this.$message.warning(`请完整填写第${invalidGoodsIndex + 1}条货物的类型、数量和数量单位`);
|
||
const invalidRouteIndex = this.form.routes.findIndex(
|
||
item => !item.departureName || !item.transportType
|
||
);
|
||
if (invalidRouteIndex >= 0)
|
||
return this.$message.warning(`请完整填写第${invalidRouteIndex + 1}段途经地及运输方式`);
|
||
if (!this.form.finalTransportType) return this.$message.warning('请填写最后一段运输方式');
|
||
}
|
||
const payload = {
|
||
...this.form,
|
||
planStartTime: this.normalizeDateTimeValue(this.form.planStartTime),
|
||
planEndTime: this.normalizeDateTimeValue(this.form.planEndTime, true),
|
||
attachmentsJson: JSON.stringify(this.attachmentRows),
|
||
routes: [
|
||
...this.form.routes.map((route, index) => ({
|
||
...route,
|
||
segmentNo: `段${index + 1}`,
|
||
...(index === 0
|
||
? {
|
||
departureProvinceName: this.form.departureProvinceName,
|
||
departureCityName: this.form.departureCityName,
|
||
departureDistrictName: this.form.departureDistrictName,
|
||
}
|
||
: {}),
|
||
})),
|
||
{
|
||
segmentNo: `段${this.form.routes.length + 1}`,
|
||
departureName: this.form.arrivalName,
|
||
departureAddress: this.form.arrivalAddress,
|
||
departureContact: this.form.arrivalContact,
|
||
departurePhone: this.form.arrivalPhone,
|
||
departureLongitude: this.form.arrivalLongitude,
|
||
departureLatitude: this.form.arrivalLatitude,
|
||
departureRegionCode: this.form.arrivalRegionCode,
|
||
departureSiteCode: this.form.arrivalSiteCode,
|
||
departureProvinceName: this.form.arrivalProvinceName,
|
||
departureCityName: this.form.arrivalCityName,
|
||
departureDistrictName: this.form.arrivalDistrictName,
|
||
transportType: this.form.finalTransportType,
|
||
},
|
||
],
|
||
};
|
||
delete payload.finalTransportType;
|
||
const res = await (draft ? api.saveDraft(payload) : api.submit(payload));
|
||
this.$message.success('保存成功');
|
||
// 兼容 Axios 响应及 BladeX 常见的 data.data 两层响应结构。
|
||
return res?.data?.data || res?.data || res;
|
||
},
|
||
async saveDraft() {
|
||
const result = await this.save(true);
|
||
if (result === false) return;
|
||
this.$emit('back');
|
||
},
|
||
async confirmCreate() {
|
||
const result = await this.save();
|
||
if (result === false) return;
|
||
this.$emit('back');
|
||
},
|
||
async createAndDispatch() {
|
||
const data = await this.save();
|
||
if (data === false) return;
|
||
const id = data?.id || data?.masterOrderId || data?.masterId;
|
||
if (!id) {
|
||
this.$message.error('保存成功但未获取到总单ID,无法进入调度');
|
||
return;
|
||
}
|
||
this.$emit('dispatch', id);
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
<style scoped lang="scss">
|
||
.master-editor {
|
||
padding-bottom: 80px;
|
||
}
|
||
|
||
.master-editor section {
|
||
margin-bottom: 12px;
|
||
padding: 16px;
|
||
border: 1px solid #eff1f7;
|
||
background: #fff;
|
||
border-radius: 6px;
|
||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||
h3 {
|
||
margin: 0 0 16px;
|
||
padding-left: 12px;
|
||
border-left: 4px solid #409eff;
|
||
font-size: 16px;
|
||
}
|
||
.el-form-item {
|
||
margin-bottom: 14px;
|
||
}
|
||
}
|
||
.goods-heading {
|
||
display: flex;
|
||
margin-bottom: 8px;
|
||
justify-content: flex-start;
|
||
align-items: center;
|
||
h3 {
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
.goods-toolbar {
|
||
display: flex;
|
||
margin-left: auto;
|
||
gap: 8px;
|
||
}
|
||
.goods-actions {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
}
|
||
.master-editor__attachment {
|
||
width: 100%;
|
||
}
|
||
.master-editor__attachment-upload {
|
||
display: flex;
|
||
justify-content: flex-start;
|
||
align-items: center;
|
||
margin-top: 12px;
|
||
:deep(.el-upload) {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
flex-wrap: nowrap;
|
||
text-align: left;
|
||
}
|
||
:deep(.el-upload__tip) {
|
||
display: inline-block;
|
||
margin: 0 0 0 8px;
|
||
color: #909399;
|
||
font-size: 13px;
|
||
line-height: 1.4;
|
||
white-space: normal;
|
||
text-align: left;
|
||
}
|
||
}
|
||
.master-editor__attachment-upload .vehicle-attachment-upload {
|
||
width: auto;
|
||
}
|
||
.master-editor__attachment-table {
|
||
width: 100%;
|
||
:deep(.el-table__header th) {
|
||
background: #f5f7fa;
|
||
color: #303133;
|
||
font-weight: 600;
|
||
}
|
||
:deep(.el-table__cell) {
|
||
border-color: #eff1f7;
|
||
}
|
||
:deep(.el-table__row:nth-child(even) td.el-table__cell) {
|
||
background: #fafafa;
|
||
}
|
||
}
|
||
:deep(.master-editor__quantity-cell .el-input) {
|
||
width: 156px;
|
||
}
|
||
.section-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 16px;
|
||
h3 {
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
.section-head__actions {
|
||
display: flex;
|
||
gap: 16px;
|
||
}
|
||
.route-dialog__search {
|
||
margin-bottom: 8px;
|
||
padding: 12px 12px 4px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||
}
|
||
.common-address-dialog__search,
|
||
.common-cargo-dialog__search {
|
||
background: #fff;
|
||
}
|
||
.route-dialog__search-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
}
|
||
.route-dialog__pagination {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
margin-top: 12px;
|
||
}
|
||
.route-flow {
|
||
position: relative;
|
||
padding: 4px 0;
|
||
&::before {
|
||
position: absolute;
|
||
top: 20px;
|
||
bottom: 20px;
|
||
left: 16px;
|
||
width: 2px;
|
||
background: #dcdfe6;
|
||
content: '';
|
||
}
|
||
}
|
||
.route-flow-row {
|
||
position: relative;
|
||
display: grid;
|
||
grid-template-columns: 32px minmax(0, 1fr);
|
||
column-gap: 16px;
|
||
align-items: start;
|
||
}
|
||
.route-flow-marker {
|
||
position: relative;
|
||
z-index: 1;
|
||
display: inline-flex;
|
||
width: 32px;
|
||
height: 32px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 6px;
|
||
background: #409eff;
|
||
color: #fff;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
line-height: 1;
|
||
&--middle {
|
||
background: #67c23a;
|
||
}
|
||
&--end {
|
||
background: #e6a23c;
|
||
}
|
||
}
|
||
.route-flow-body {
|
||
min-width: 0;
|
||
}
|
||
.route-flow-segment {
|
||
min-height: 48px;
|
||
padding-left: 48px;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
.route-node-form {
|
||
display: grid;
|
||
grid-template-columns: 300px 360px repeat(3, 300px);
|
||
gap: 0 16px;
|
||
align-items: start;
|
||
:deep(.el-form-item) {
|
||
margin: 0 0 12px;
|
||
}
|
||
:deep(.el-form-item.route-address-form-item) {
|
||
grid-column: span 2;
|
||
}
|
||
:deep(.el-form-item__content) {
|
||
min-width: 0;
|
||
}
|
||
:deep(.el-input),
|
||
:deep(.el-select),
|
||
:deep(.el-date-editor) {
|
||
width: 100%;
|
||
}
|
||
.route-address-inputs,
|
||
.route-contact-actions {
|
||
display: flex;
|
||
width: 100%;
|
||
gap: 8px;
|
||
}
|
||
.route-address-name {
|
||
flex: 0 0 220px;
|
||
}
|
||
.route-address-detail {
|
||
flex: 0 0 300px;
|
||
width: 300px;
|
||
}
|
||
.route-contact-actions :deep(.el-input) {
|
||
flex: 0 0 200px;
|
||
width: 200px;
|
||
}
|
||
:deep(.route-contact-form-item .el-input),
|
||
:deep(.route-phone-form-item .el-input),
|
||
:deep(.route-date-form-item .el-date-editor) {
|
||
width: 200px;
|
||
}
|
||
:deep(.route-contact-actions) {
|
||
width: auto;
|
||
}
|
||
:deep(.el-form-item__label) {
|
||
white-space: nowrap;
|
||
}
|
||
.address-picker-button {
|
||
flex: 0 0 auto;
|
||
|
||
:deep(.el-icon) {
|
||
font-size: 22px;
|
||
}
|
||
}
|
||
}
|
||
.route-step-title {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 8px 16px;
|
||
width: 100%;
|
||
color: #303133;
|
||
font-size: 16px;
|
||
.route-segment-label {
|
||
flex: 0 0 auto;
|
||
white-space: nowrap;
|
||
}
|
||
.route-title-path {
|
||
display: inline-block;
|
||
max-width: 24em;
|
||
overflow: hidden;
|
||
color: #606266;
|
||
font-size: 14px;
|
||
font-weight: 400;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.route-transport-control {
|
||
display: flex;
|
||
flex: 0 0 auto;
|
||
align-items: center;
|
||
gap: 8px;
|
||
color: #606266;
|
||
font-size: 14px;
|
||
font-weight: 400;
|
||
white-space: nowrap;
|
||
:deep(.el-select) {
|
||
width: 180px;
|
||
}
|
||
}
|
||
}
|
||
.required-asterisk {
|
||
margin-left: 4px;
|
||
color: #f56c6c;
|
||
}
|
||
.route-action-button {
|
||
flex: 0 0 auto;
|
||
}
|
||
.address-pagination {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
margin-top: 12px;
|
||
}
|
||
.address-map-toolbar {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-bottom: 12px;
|
||
}
|
||
.address-map {
|
||
height: 440px;
|
||
}
|
||
.address-map-status {
|
||
margin-top: 8px;
|
||
color: #606266;
|
||
}
|
||
.cargo-import {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
.el-upload__tip {
|
||
margin-top: 8px;
|
||
line-height: 1.5;
|
||
}
|
||
}
|
||
@media screen and (max-width: 1440px) {
|
||
.route-node-form {
|
||
grid-template-columns: repeat(2, minmax(320px, 1fr));
|
||
:deep(.el-form-item.route-address-form-item) {
|
||
grid-column: span 2;
|
||
}
|
||
}
|
||
.route-step-title {
|
||
align-items: flex-start;
|
||
gap: 8px;
|
||
}
|
||
}
|
||
footer {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
padding: 12px 24px;
|
||
position: fixed;
|
||
right: 0;
|
||
left: 230px;
|
||
bottom: 0;
|
||
margin: 0;
|
||
z-index: 10;
|
||
background: #fff;
|
||
border-top: 1px solid #eff1f7;
|
||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
|
||
}
|
||
:global(.avue--collapse footer) {
|
||
left: 60px;
|
||
}
|
||
:global(.avue-layout--horizontal footer) {
|
||
left: 0;
|
||
}
|
||
</style>
|