Files
tms-erp-web/src/views/business/components/master-order-editor.vue
T
b2894lxlx cba2ad3b3b 1、调整凭证
2、调整运单
2026-09-08 17:50:34 +08:00

2252 lines
91 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>
<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>