@@ -138,9 +138,9 @@
>
<el-option
v-for=" item in carrierOptions "
:key=" item . id || item . customerName || item . carrierName "
:label=" item . customerName || item . carrierName || item . name "
:value=" item . customerName || item . carrierName || item . name "
:key=" item . id || item . fullName || item . customerName || item . carrierName "
:label=" item . fullName || item . customerName || item . carrierName || item . name "
:value=" item . fullName || item . customerName || item . carrierName || item . name "
/>
</el-select>
</el-form-item>
@@ -214,7 +214,7 @@
<div class=" loading - manage - page _ _search - actions ">
<el-button type=" primary " @click=" handleSearch ">查询</el-button>
<el-button @click=" handleResetSearch ">重置</el-button>
<el-link type=" primary " @click=" searchExpanded = ! searchExpanded ">{{ searchExpanded ? '收起筛选 ' : '展开筛选 ' }}</el-link>
<el-link type=" primary " @click=" searchExpanded = ! searchExpanded ">{{ searchExpanded ? '收起' : '展开' }}</el-link>
</div>
</div>
</el-form>
@@ -350,7 +350,29 @@
@closed=" resetLoadingDialog "
>
<div v-loading=" dialogLoading " class=" loading - manage - dialog _ _body ">
<div v-if=" dialogReadonly " class=" loading - detail ">
<section-card title=" 配载单详情 ">
<div class=" loading - detail _ _summary ">
<div class=" loading - detail _ _heading ">配载单详情</div>
<div>{{ dialogForm.loadingNo || '-' }}</div>
<div><el-tag :type=" detailStatusType ">{{ statusText(dialogForm) }}</el-tag></div>
<div><el-tag type=" warning ">{{ dialogForm.transportType || '-' }}</el-tag></div>
</div>
<div class=" loading - detail _ _task - row ">
<div class=" loading - detail _ _field "><span class=" loading - detail _ _label ">任务信息</span><span class=" loading - detail _ _value ">{{ taskSummary }}</span></div>
<div class=" loading - detail _ _field "><span class=" loading - detail _ _label ">附件</span><span class=" loading - detail _ _value "><el-link v-for=" file in detailAttachments " :key=" file . url || file . name " type=" primary " @click=" previewAttachment ( file ) ">{{ file.name }}</el-link><span v-if=" ! detailAttachments . length ">-</span></span></div>
</div>
</section-card>
<section-card>
<template #title><span class=" loading - detail _ _route - title ">运输路线<el-link type=" primary " @click=" openRouteChangeDialog ">变更运输路线</el-link></span></template>
<div v-if=" routeNodes . length " class=" loading - detail _ _steps "><div v-for=" ( node , index ) in routeNodes " :key=" node . key || index " class=" loading - detail _ _step "><div class=" loading - detail _ _step - line "><span :class=" [ 'loading-detail__step-dot' , index === 0 ? 'start' : index === routeNodes . length - 1 ? 'end' : 'middle' ] ">{{ index === 0 ? '起' : index === routeNodes.length - 1 ? '终' : '经' }}</span></div><div class=" loading - detail _ _step - address ">{{ node.address }}</div><div class=" loading - detail _ _step - tags "><el-tag v-for=" tag in node . tags || [ ] " :key=" tag " size=" small ">{{ tag }}</el-tag></div></div></div><el-empty v-else description=" 暂无路线 " :image-size=" 60 " />
</section-card>
<section-card title=" 货物信息 "><el-table :data=" cargoRows " border height=" 220 "><el-table-column type=" index " label=" 序号 " width=" 70 "/><el-table-column prop=" projectName " label=" 项目名称 " min-width=" 140 "/><el-table-column prop=" cargoName " label=" 货物名称 " min-width=" 140 "/><el-table-column prop=" cargoType " label=" 货物类型 " min-width=" 120 "/><el-table-column prop=" packageType " label=" 包装 " min-width=" 100 "/><el-table-column prop=" weight " label=" 重量 ( 吨 ) " min-width=" 110 "/><el-table-column prop=" volume " label=" 体积 ( 方 ) " min-width=" 110 "/><el-table-column prop=" quantity " label=" 数量 " min-width=" 90 "/><el-table-column prop=" materialCode " label=" 物料编码 " min-width=" 120 "/><el-table-column prop=" equipmentCode " label=" 设备编码 " min-width=" 120 "/><el-table-column prop=" brand " label=" 品牌 " min-width=" 120 "/><el-table-column prop=" specificationModel " label=" 规格型号 " min-width=" 140 "/><el-table-column prop=" remark " label=" 备注 " min-width=" 160 "/><el-table-column prop=" unitPrice " label=" 货物单价 ( 元 ) " min-width=" 140 "/><el-table-column prop=" planDate " label=" 计划日期 " min-width=" 130 "/></el-table></section-card>
<section-card title=" 关联运单信息 "><el-table :data=" selectedWaybillRows " border><el-table-column type=" index " label=" 序号 " width=" 70 "/><el-table-column prop=" waybillNo " label=" 运单号 " min-width=" 150 "><template #default=" { row } "><el-link type=" primary ">{{ row.waybillNo || '-' }}</el-link></template></el-table-column><el-table-column label=" 配载单号 " min-width=" 150 "><template #default>{{ dialogForm.loadingNo || '-' }}</template></el-table-column><el-table-column prop=" projectName " label=" 项目名称 " min-width=" 150 "/><el-table-column prop=" customerName " label=" 客户 " min-width=" 140 "/><el-table-column prop=" departureAddress " label=" 发货地址 " min-width=" 220 " show-overflow-tooltip/><el-table-column prop=" arrivalAddress " label=" 到货地址 " min-width=" 220 " show-overflow-tooltip/></el-table></section-card>
<div class=" loading - detail _ _bottom - grid "><section-card title=" 运输节点 "><el-timeline v-if=" processNodes . length "><el-timeline-item v-for=" ( item , index ) in processNodes " :key=" item . id || index " :timestamp=" item . time || '' ">{{ item.name || item.nodeName || item.label }}</el-timeline-item></el-timeline><el-empty v-else description=" 暂无运输节点 " :image-size=" 50 "/></section-card><section-card title=" 物流轨迹 "><template #extra><div class=" loading - detail _ _track - actions "><el-button plain @click=" handleTrackAction ( 'playback' ) ">轨迹回放</el-button><el-button plain @click=" handleTrackAction ( 'locate' ) ">实时定位</el-button></div></template><div ref=" detailAmap " class=" loading - detail _ _map "></div></section-card></div>
</div>
<el-form
v-else
ref=" loadingFormRef "
:model=" dialogForm "
:disabled=" dialogReadonly "
@@ -358,102 +380,9 @@
label-width=" auto "
class=" loading - manage - dialog _ _form "
>
<section-card title=" 配载基本信息 ">
<div class=" loading - manage - dialog _ _grid ">
<el-form-item label=" 配载单号 ">
<el-input v-model=" dialogForm . loadingNo " disabled placeholder=" 系统自动生成 " />
</el-form-item>
<el-form-item label=" 运输类型 " required>
<el-select
v-model=" dialogForm . transportType "
filterable
clearable
placeholder=" 请选择 "
@visible-change=" visible => visible && loadTransportTypeOptions ( ) "
>
<el-option
v-for=" item in transportTypeOptions "
:key=" item . value "
:label=" item . label "
:value=" item . value "
/>
</el-select>
</el-form-item>
<el-form-item label=" 项目名称 ">
<el-input v-model=" dialogForm . projectName " readonly placeholder=" 从待配载运单带出 " />
</el-form-item>
<el-form-item label=" 客户名称 ">
<el-input v-model=" dialogForm . customerName " readonly placeholder=" 从待配载运单带出 " />
</el-form-item>
<el-form-item label=" 原始单号 ">
<el-input v-model=" dialogForm . originalNo " readonly placeholder=" 从待配载运单带出 " />
</el-form-item>
<el-form-item label=" 运单批次号 ">
<el-input v-model=" dialogForm . batchNo " clearable placeholder=" 请输入 " />
</el-form-item>
<el-form-item label=" 数据来源 ">
<el-select v-model=" dialogForm . dataSource " clearable placeholder=" 请选择 ">
<el-option
v-for=" item in dataSourceOptions "
:key=" item . value "
:label=" item . label "
:value=" item . value "
/>
</el-select>
</el-form-item>
<el-form-item label=" 计划名称 ">
<el-select
v-model=" dialogForm . planName "
clearable
filterable
remote
reserve-keyword
:remote-method=" loadPlanOptions "
:loading=" planLoading "
placeholder=" 请输入 "
@visible-change=" visible => visible && loadPlanOptions ( dialogForm . planName ) "
>
<el-option
v-for=" item in planOptions "
:key=" item . id || item . planName "
:label=" item . planName "
:value=" item . planName "
/>
</el-select>
</el-form-item>
<el-form-item label=" 开始日期 ">
<el-date-picker
v-model=" dialogForm . startDate "
type=" date "
format=" YYYY - MM - DD "
value-format=" YYYY - MM - DD "
placeholder=" 请选择 "
/>
</el-form-item>
<el-form-item label=" 结束日期 ">
<el-date-picker
v-model=" dialogForm . endDate "
type=" date "
format=" YYYY - MM - DD "
value-format=" YYYY - MM - DD "
placeholder=" 请选择 "
/>
</el-form-item>
<el-form-item label=" 配载子单号 " class=" loading - manage - dialog _ _span - 2 ">
<el-input
v-model=" dialogForm . loadingSubNos "
readonly
type=" textarea "
:rows=" 2 "
placeholder=" 添加待配载运单后自动生成 "
/>
</el-form-item>
</div>
</section-card>
<section-card title=" 筛选待配载运单 ">
<template #extra>
<el-link v-if=" ! dialogReadonly " type=" primary " @click=" loadCandidateWaybills ">筛选</el-link>
<el-link v-if=" ! dialogReadonly " type=" primary " @click=" openCandidateSearch ">筛选</el-link>
</template>
<div class=" loading - manage - dialog _ _candidate - search " v-if=" candidateSearchVisible ">
<el-row :gutter=" 16 ">
@@ -584,9 +513,9 @@
>
<el-option
v-for=" item in carrierOptions "
:key=" item . id || item . customerName || item . carrierName "
:label=" item . customerName || item . carrierName || item . name "
:value=" item . customerName || item . carrierName || item . name "
:key=" item . id || item . fullName || item . customerName || item . carrierName "
:label=" item . fullName || item . customerName || item . carrierName || item . name "
:value=" item . fullName || item . customerName || item . carrierName || item . name "
/>
</el-select>
</el-form-item>
@@ -641,15 +570,12 @@
</el-col>
</el-row>
</div>
<div class=" loading - manage - dialog _ _selected - summary ">
<span>已选择 {{ selectedWaybillRows.length }} 条运单</span>
</div>
<el-table
ref=" candidateTableRef "
v-loading=" candidateLoading "
:data=" candidateRows "
border
height=" 260 "
height=" 287 "
row-key=" id "
@selection-change=" candidateSelectionChange "
>
@@ -723,7 +649,13 @@
show-overflow-tooltip
/>
</el-table>
<div class=" loading - manage - dialog _ _pagination ">
<div class=" loading - manage - dialog _ _table - toolbar ">
<el-button
type=" primary "
:disabled=" ! candidateSelection . length || candidateAdding "
:loading=" candidateAdding "
@click=" addSelectedWaybills "
>添加配载</el-button>
<el-pagination
v-model:current-page=" candidatePage . currentPage "
v-model:page-size=" candidatePage . pageSize "
@@ -736,64 +668,66 @@
</div>
</section-card>
<section-card title=" 待配载列表 ">
<el-table
:data=" selectedWaybillRows "
border
class=" loading - manage - dialog _ _selected - table "
>
<el-table-column type=" index " label=" 序号 " width=" 70 " align=" center " / >
<el-table-column prop =" waybillNo " label=" 运单 号" min- width=" 150 ">
<template #default=" { row } ">
<el-link type=" primary ">{{ row.waybillNo }}</el-link >
</template >
</el-table-column >
< el-table-column prop=" projectName " label=" 项目名称 " min-width=" 160 " / >
<el-table-column
prop=" departureAddress "
label=" 发货地 "
min-width=" 220 "
show-overflow-tooltip
/>
<el-table-column
prop=" arrivalAddress "
label=" 到货地 "
min-width=" 220 "
show-overflow-tooltip
/>
<el-table-column label=" 操作 " width=" 110 " fixed=" right " >
<template #default=" { $index } ">
<el-link
v-if=" ! dialogReadonly "
type=" primary "
@click=" removeSelectedWaybill ( $index ) "
>移除</el-link>
</template >
</el-table-column >
</el-table>
</section-card >
<div class=" loading - manage - dialog _ _waybill - route - layout ">
<section-card title=" 待配载列表 ">
<el-table
:data=" selectedWaybillRows "
border
class=" loading - manage - dialog _ _selected - table "
>
<el-table-column type =" index " label=" 序 号" width=" 70 " align=" center " / >
<el-table-column prop=" waybillNo " label=" 运单号 " min-width=" 150 ">
<template #default=" { row } " >
<el-link type=" primary ">{{ row.waybillNo }}</el-link >
</template >
</ el-table-column>
<el-table-column prop=" projectName " label=" 项目名称 " min-width=" 160 " />
<el-table-column
prop=" departureAddress "
label=" 发货地 "
min-width=" 220 "
show-overflow-tooltip
/>
<el-table-column
prop=" arrivalAddress "
label=" 到货地 "
min-width=" 220 "
show-overflow-tooltip
/ >
<el-table-column label=" 操作 " width=" 110 " fixed=" right ">
<template #default=" { $index } ">
<el-link
v-if=" ! dialogReadonly "
type=" danger "
@click=" removeSelectedWaybill ( $index ) "
>删除</el-link >
</template >
</el-table-column >
</el-table >
</section-card>
<section-card title=" 路线信息 ">
<div class=" loading - manage - dialog _ _route - list " v-if=" routeNodes . length ">
<div
v-for=" ( node , index ) in routeNodes "
:key=" node . key "
class=" loading - manage - dialog _ _route - item "
draggable=" true "
@dragstart=" handleRouteDragStart ( index ) "
@dragover.prevent
@drop=" handleRouteDrop ( index ) "
>
<el-icon class=" loading - manage - dialog _ _route - handle "><Rank /></el-icon>
<span class=" loading - manage - dialog _ _route - type ">{{ routeTypeText(index) }}</span>
<span class=" loading - manage - dialog _ _route - address ">{{ node.address }}</span>
<span class=" loading - manage - dialog _ _route - tags ">
<el-tag v-for=" tag in node . tags " :key=" tag " size=" small ">{{ tag }}</el-tag>
</span>
</div>
</div>
<el-empty v-else description=" 添加待配载运单后自动生成路线 " :image-size=" 72 " />
</section-card>
<section-card title=" 线路编辑 ">
<div class=" loading - manage - dialog _ _route - list " v-if=" routeNodes . length ">
<div
v-for=" ( node , index ) in routeNodes "
:key=" node . key "
class=" loading - manage - dialog _ _route - item "
draggable=" true "
@dragstart=" handleRouteDragStart ( index ) "
@dragover.prevent
@drop=" handleRouteDrop ( index ) "
>
<el-icon class=" loading - manage - dialog _ _route - handle "><Rank /></el-icon>
<span class=" loading - manage - dialog _ _route - type ">{{ routeTypeText(index) }}</span>
<span class=" loading - manage - dialog _ _route - address ">{{ node.address }}</span>
<span class=" loading - manage - dialog _ _route - tags ">
<el-tag v-for=" tag in node . tags " :key=" tag " size=" small ">{{ tag }}</el-tag>
</span>
</div>
</div>
<el-empty v-else description=" 添加待配载运单后自动生成路线 " :image-size=" 72 " />
</section-card>
</div>
<section-card title=" 货物信息 ">
<el-table :data=" cargoRows " border class=" loading - manage - dialog _ _cargo - table ">
@@ -805,11 +739,13 @@
<el-table-column prop=" weight " label=" 重量 ( 吨 ) " min-width=" 120 " />
<el-table-column prop=" volume " label=" 体积 ( 方 ) " min-width=" 120 " />
<el-table-column prop=" quantity " label=" 数量 " min-width=" 100 " />
<el-table-column prop=" planDate " label=" 计划日期 " min-width=" 130 " sortable/>
<el-table-column prop=" unitPrice " label=" 货物单价 ( 元 ) " min-width=" 150 " />
<el-table-column prop=" materialCode " label=" 物料编码 " min-width=" 130 " />
<el-table-column prop=" equipmentCode " label=" 设备编码 " min-width=" 130 " />
<el-table-column prop=" brand " label=" 品牌 " min-width=" 120 " />
<el-table-column prop=" specificationModel " label=" 规格型号 " min-width=" 140 " />
<el-table-column prop=" remark " label=" 备注 " min-width=" 180 " show-overflow-tooltip />
<el-table-column prop=" unitPrice " label=" 货物单价 ( 元 ) " min-width=" 150 " />
<el-table-column prop=" planDate " label=" 计划日期 " min-width=" 130 " sortable />
</el-table>
</section-card>
@@ -837,9 +773,9 @@
>
<el-option
v-for=" item in carrierOptions "
:key=" item . id || item . customerName || item . carrierName "
:label=" item . customerName || item . carrierName || item . name "
:value=" item . customerName || item . carrierName || item . name "
:key=" item . id || item . fullName || item . customerName || item . carrierName "
:label=" item . fullName || item . customerName || item . carrierName || item . name "
:value=" item . fullName || item . customerName || item . carrierName || item . name "
/>
</el-select>
</el-form-item>
@@ -939,6 +875,30 @@
</div>
</template>
</el-dialog>
<el-dialog v-model=" routeChangeVisible " title=" 变更运输路线 " append-to-body destroy-on-close width=" 78 % " class=" loading - route - change - dialog ">
<el-tabs v-model=" routeChangeTab ">
<el-tab-pane label=" 变更路线 " name=" change ">
<section-card title=" 关联子运单地址 ">
<el-table :data=" routeChangeRows " border>
<el-table-column type=" index " label=" 序号 " width=" 70 " />
<el-table-column prop=" waybillNo " label=" 运单号 " min-width=" 150 "><template #default=" { row } "><el-link type=" primary ">{{ row.waybillNo || '-' }}</el-link></template></el-table-column>
<el-table-column prop=" projectName " label=" 项目名称 " min-width=" 160 " />
<el-table-column label=" 发货地 " min-width=" 310 "><template #default=" { row } "><div class=" loading - route - change - dialog _ _original ">原:{{ row.originalDepartureAddress || '-' }}</div><el-input v-model=" row . departureAddress " placeholder=" 请选择或输入地址 " @input=" updateRouteChangeNodes "><template #suffix><el-icon class=" loading - route - change - dialog _ _location " @click=" openCommonAddressDialog ( row , 'departure' ) "><Location /></el-icon></template></el-input></template></el-table-column>
<el-table-column label=" 到货地 " min-width=" 310 "><template #default=" { row } "><div class=" loading - route - change - dialog _ _original ">原:{{ row.originalArrivalAddress || '-' }}</div><el-input v-model=" row . arrivalAddress " placeholder=" 请选择或输入地址 " @input=" updateRouteChangeNodes "><template #suffix><el-icon class=" loading - route - change - dialog _ _location " @click=" openCommonAddressDialog ( row , 'arrival' ) "><Location /></el-icon></template></el-input></template></el-table-column>
</el-table>
</section-card>
<section-card title=" 路线编辑 "><template #extra><span class=" loading - route - change - dialog _ _hint ">拖拽调整顺序</span></template><div v-if=" routeChangeNodes . length " class=" loading - route - change - dialog _ _route - list "><div v-for=" ( node , index ) in routeChangeNodes " :key=" node . key || index " class=" loading - route - change - dialog _ _route - item " draggable=" true " @dragstart=" handleRouteChangeDragStart ( index ) " @dragover.prevent @drop=" handleRouteChangeDrop ( index ) "><span class=" loading - route - change - dialog _ _route - type ">{{ routeChangeTypeText(index) }}</span><span>{{ node.address }}</span><span class=" loading - route - change - dialog _ _tags "><el-tag v-for=" tag in node . tags || [ ] " :key=" tag " size=" small ">{{ tag }}</el-tag></span><el-icon><Rank /></el-icon></div></div><el-empty v-else description=" 暂无路线 " :image-size=" 60 " /></section-card>
<el-form label-width=" auto "><el-form-item label=" 变更备注 "><el-input v-model=" routeChangeRemark " type=" textarea " :rows=" 1 " maxlength=" 200 " show-word-limit placeholder=" 请输入 " /></el-form-item></el-form>
</el-tab-pane>
<el-tab-pane label=" 变更记录 " name=" records "><el-table :data=" routeChangeRecords " border><el-table-column prop=" changeTime " label=" 变更时间 " min-width=" 170 "/><el-table-column prop=" changeUser " label=" 变更人 " min-width=" 120 "/><el-table-column prop=" content " label=" 变更内容 " min-width=" 360 "><template #default=" { row } "><div class=" loading - route - change - dialog _ _record - content ">{{ row.content || '-' }}</div></template></el-table-column><el-table-column prop=" remark " label=" 备注 " min-width=" 200 " show-overflow-tooltip/></el-table><el-empty v-if=" ! routeChangeRecords . length " description=" 暂无变更记录 " :image-size=" 60 "/></el-tab-pane>
</el-tabs>
<template #footer><el-button @click=" routeChangeVisible = false ">关闭</el-button><el-button v-if=" routeChangeTab === 'change' " type=" primary " @click=" confirmRouteChange ">确认变更</el-button></template>
</el-dialog>
<el-dialog v-model=" commonAddressVisible " title=" 选择常用地址 " append-to-body width=" 900 px " class=" loading - common - address - dialog ">
<div class=" loading - common - address - dialog _ _toolbar "><el-input v-model=" commonAddressQuery . addressName " clearable placeholder=" 请输入地址名称 " @keyup.enter=" loadCommonAddresses "/><el-button type=" primary " @click=" loadCommonAddresses ">查询</el-button></div>
<el-table v-loading=" commonAddressLoading " :data=" commonAddressRows " border @row-dblclick=" selectCommonAddress "><el-table-column prop=" addressName " label=" 地址名称 " min-width=" 180 "/><el-table-column prop=" regionName " label=" 区域 " min-width=" 180 "/><el-table-column prop=" detailAddress " label=" 详细地址 " min-width=" 260 "/><el-table-column label=" 操作 " width=" 90 "><template #default=" { row } "><el-link type=" primary " @click=" selectCommonAddress ( row ) ">选择</el-link></template></el-table-column></el-table>
<template #footer><el-pagination v-model:current-page=" commonAddressPage . currentPage " v-model:page-size=" commonAddressPage . pageSize " layout=" total , prev , pager , next " :total=" commonAddressPage . total " @current-change=" loadCommonAddresses "/><el-button @click=" commonAddressVisible = false ">关闭</el-button></template>
</el-dialog>
</basic-container>
</template>
@@ -956,6 +916,8 @@ import {
import { getParentOptions as getCargoTypeOptions } from '@/api/base/cargo-type';
import { getList as getCustomerList } from '@/api/vehicle/customer-archive';
import { getList as getDriverList } from '@/api/transportCapacity/driver';
import { getList as getProcessConfigList } from '@/api/business/process-config';
import { getList as getCommonAddressList } from '@/api/base/common-address';
import {
carrierTypeOptions,
loadingManageConfig,
@@ -963,11 +925,14 @@ import {
loadingStatusOptions,
} from '@/option/business/loading-manage';
import { ElMessage, ElMessageBox } from 'element-plus';
import { Rank, Refresh, Setting } from '@element-plus/icons-vue';
import { Location, Rank, Refresh, Setting } from '@element-plus/icons-vue';
import dayjs from 'dayjs';
const defaultTransportType = '公路整车 ';
const defaultCandidateTransportType = '公路运输 ';
const defaultTransportType = '';
const defaultCandidateTransportType = '';
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
let detailAmapLoader;
const createSearchForm = () => ({
loadingNo: '',
@@ -1091,6 +1056,7 @@ export default {
data() {
return {
Rank,
Location,
Refresh,
Setting,
loading: false,
@@ -1102,7 +1068,7 @@ export default {
total: 0,
pageSizes: [10, 20, 50, 100],
},
searchExpanded: tru e,
searchExpanded: fals e,
searchForm: createSearchForm(),
selectionRows: [],
dialogVisible: false,
@@ -1110,9 +1076,10 @@ export default {
dialogLoading: false,
dialogSaving: '',
dialogForm: createDialogForm(),
candidateSearchVisible: tru e,
candidateSearchVisible: fals e,
candidateQuery: createCandidateSearchForm(),
candidateLoading: false,
candidateAdding: false,
candidateRows: [],
candidateSelection: [],
candidatePage: {
@@ -1142,6 +1109,21 @@ export default {
statusOptions: loadingStatusOptions,
dataSourceOptions: loadingManageConfig.dataSourceOptions,
config: loadingManageConfig,
processNodes: [],
routeChangeVisible: false,
routeChangeTab: 'change',
routeChangeRows: [],
routeChangeNodes: [],
routeChangeRemark: '',
routeChangeRecords: [],
routeChangeDragIndex: -1,
commonAddressVisible: false,
commonAddressLoading: false,
commonAddressRows: [],
commonAddressQuery: { addressName: '' },
commonAddressPage: { currentPage: 1, pageSize: 10, total: 0 },
commonAddressTarget: null,
detailAmap: null,
};
},
computed: {
@@ -1158,6 +1140,20 @@ export default {
};
return titleMap[this.dialogMode] || '配载管理';
},
detailStatusType() {
const typeMap = { pending: 'success', running: 'success', completed: 'info', cancelled: 'info', draft: 'warning' };
return typeMap[this.dialogForm.businessStatus] || 'info';
},
taskSummary() {
return [this.dialogForm.carrierName, this.dialogForm.driverName, this.dialogForm.vehicleNo]
.filter(Boolean)
.join(' / ') || '-';
},
detailAttachments() {
return parseJsonArray(this.dialogForm.attachmentsJson || this.dialogForm.attachmentJson).map(item =>
typeof item === 'string' ? { name: item.split('/').pop(), url: item } : { name: item.name || item.fileName || '附件', url: item.url || item.link || item.fileUrl }
);
},
},
mounted() {
this.loadTransportTypeOptions();
@@ -1294,6 +1290,9 @@ export default {
this.dialogForm.loadingSubNos
);
this.restoreRouteAndCargo();
this.restoreRouteChangeRecords();
await this.loadProcessNodes();
this.$nextTick(() => this.initDetailAmap());
await this.loadCandidateWaybills();
} else {
this.dialogForm = createDialogForm();
@@ -1310,6 +1309,8 @@ export default {
this.dialogLoading = false;
},
resetDialogData() {
this.detailAmap?.destroy?.();
this.detailAmap = null;
this.dialogForm = createDialogForm();
this.candidateQuery = createCandidateSearchForm();
this.candidateRows = [];
@@ -1319,7 +1320,8 @@ export default {
this.cargoRows = [];
this.candidatePage.currentPage = 1;
this.candidatePage.total = 0;
this.candidateSearchVisible = tru e;
this.candidateSearchVisible = fals e;
this.processNodes = [];
},
clearLoadingDialog() {
this.resetDialogData();
@@ -1333,6 +1335,164 @@ export default {
: this.buildRouteNodes(this.selectedWaybillRows);
this.cargoRows = cargoRows.length ? cargoRows : this.buildCargoRows(this.selectedWaybillRows);
},
restoreRouteChangeRecords() {
try {
const taskInfo = JSON.parse(this.dialogForm.taskInfoJson || '{}');
this.routeChangeRecords = Array.isArray(taskInfo.routeChangeRecords)
? [...taskInfo.routeChangeRecords].sort((a, b) => String(b.changeTime).localeCompare(String(a.changeTime)))
: [];
} catch (error) {
this.routeChangeRecords = [];
}
},
async loadProcessNodes() {
const waybillProcess = parseJsonArray(this.selectedWaybillRows[0]?.processJson);
if (waybillProcess.length) {
this.processNodes = waybillProcess;
return;
}
try {
const rows = unwrapRecords(await getProcessConfigList(1, 20, { projectId: this.dialogForm.projectId, status: 1 }));
const config = rows.find(item => item.nodeConfigJson) || {};
this.processNodes = parseJsonArray(config.nodeConfigJson);
} catch (error) {
this.processNodes = [];
}
},
previewAttachment(file) {
if (!file.url) {
ElMessage.info('暂无可预览的附件地址');
return;
}
window.open(file.url, '_blank', 'noopener,noreferrer');
},
loadDetailAmap() {
if (window.AMap?.Map) return Promise.resolve();
if (!detailAmapLoader) {
detailAmapLoader = new Promise((resolve, reject) => {
window._AMapSecurityConfig = { securityJsCode: AMAP_SECURITY_CODE };
const script = document.createElement('script');
script.src = `https://webapi.amap.com/maps?v=2.0&key=${AMAP_KEY}`;
script.async = true;
script.onload = resolve;
script.onerror = () => reject(new Error('高德地图组件加载失败'));
document.body.appendChild(script);
});
}
return detailAmapLoader;
},
initDetailAmap() {
if (!this.$refs.detailAmap) return;
this.loadDetailAmap()
.then(() => {
if (!this.detailAmap) {
this.detailAmap = new window.AMap.Map(this.$refs.detailAmap, {
center: [116.40769, 39.89945],
zoom: 11,
});
window.AMap.plugin(['AMap.ToolBar'], () => this.detailAmap?.addControl(new window.AMap.ToolBar()));
} else {
this.detailAmap.resize?.();
}
})
.catch(() => ElMessage.warning('高德地图加载失败'));
},
handleTrackAction(action) {
ElMessage.info(action === 'playback' ? '暂无可回放的物流轨迹' : '暂无车辆实时定位数据');
},
openRouteChangeDialog() {
this.routeChangeTab = 'change';
this.routeChangeRemark = '';
this.routeChangeRows = this.selectedWaybillRows.map(row => ({
...row,
originalDepartureAddress: row.departureAddress || row.departureName || '',
originalArrivalAddress: row.arrivalAddress || row.arrivalName || '',
departureAddress: row.departureAddress || row.departureName || '',
arrivalAddress: row.arrivalAddress || row.arrivalName || '',
}));
this.routeChangeNodes = this.buildRouteNodes(this.routeChangeRows);
this.routeChangeVisible = true;
},
routeChangeTypeText(index) {
if (index === 0) return '起';
if (index === this.routeChangeNodes.length - 1) return '终';
return '经';
},
handleRouteChangeDragStart(index) {
this.routeChangeDragIndex = index;
},
handleRouteChangeDrop(index) {
if (this.routeChangeDragIndex < 0 || this.routeChangeDragIndex === index) return;
const nodes = [...this.routeChangeNodes];
const [node] = nodes.splice(this.routeChangeDragIndex, 1);
nodes.splice(index, 0, node);
this.routeChangeNodes = nodes;
this.routeChangeDragIndex = -1;
},
updateRouteChangeNodes() {
this.routeChangeNodes = this.buildRouteNodes(this.routeChangeRows);
},
openCommonAddressDialog(row, field) {
this.commonAddressTarget = { row, field };
this.commonAddressQuery = { addressName: '' };
this.commonAddressPage.currentPage = 1;
this.commonAddressVisible = true;
this.loadCommonAddresses();
},
async loadCommonAddresses() {
this.commonAddressLoading = true;
try {
const page = unwrapPage(await getCommonAddressList(this.commonAddressPage.currentPage, this.commonAddressPage.pageSize, { ...this.commonAddressQuery, allDept: 0 }));
this.commonAddressRows = page.records;
this.commonAddressPage.total = page.total;
} finally {
this.commonAddressLoading = false;
}
},
selectCommonAddress(address) {
const target = this.commonAddressTarget;
if (!target) return;
target.row[`${target.field}Address`] = [address.regionName, address.detailAddress || address.addressName].filter(Boolean).join('') || address.addressName || '';
this.updateRouteChangeNodes();
this.commonAddressVisible = false;
},
async confirmRouteChange() {
const changeRows = this.routeChangeRows.filter(row => row.departureAddress !== row.originalDepartureAddress || row.arrivalAddress !== row.originalArrivalAddress);
if (!changeRows.length && !this.routeChangeRemark) {
ElMessage.warning('请修改地址或填写变更备注');
return;
}
const content = changeRows.map(row => `${row.waybillNo || '运单'}:发货地 ${row.originalDepartureAddress} → ${row.departureAddress};到货地 ${row.originalArrivalAddress} → ${row.arrivalAddress}`).join('; ') || '调整运输路线顺序';
this.routeChangeRecords.unshift({ changeTime: dayjs().format('YYYY-MM-DD HH:mm:ss'), changeUser: '当前用户', content, remark: this.routeChangeRemark });
let taskInfo = {};
try {
taskInfo = JSON.parse(this.dialogForm.taskInfoJson || '{}');
} catch (error) {
taskInfo = {};
}
this.dialogForm.taskInfoJson = JSON.stringify({
...(typeof taskInfo === 'object' && !Array.isArray(taskInfo) ? taskInfo : {}),
routeChangeRecords: this.routeChangeRecords,
});
this.selectedWaybillRows = this.routeChangeRows.map(({ originalDepartureAddress, originalArrivalAddress, ...row }) => row);
this.routeNodes = this.routeChangeNodes;
this.syncRouteAddressFields();
try {
await loadingApi.changeRoute({
id: this.dialogForm.id,
routeJson: JSON.stringify(this.routeNodes),
departureAddress: this.dialogForm.departureAddress,
transitAddress: this.dialogForm.transitAddress,
arrivalAddress: this.dialogForm.arrivalAddress,
taskInfoJson: this.dialogForm.taskInfoJson,
});
this.routeChangeVisible = false;
ElMessage.success('路线变更已保存');
} catch (error) {
this.restoreRouteChangeRecords();
throw error;
}
},
async restoreWaybillRows(idsJson, loadingSubNos) {
const ids = parseJsonArray(idsJson).filter(Boolean);
if (ids.length) {
@@ -1369,6 +1529,10 @@ export default {
this.candidateLoading = false;
}
},
openCandidateSearch() {
this.candidateSearchVisible = true;
this.loadCandidateWaybills();
},
handleCandidateSearch() {
this.candidatePage.currentPage = 1;
this.loadCandidateWaybills();
@@ -1390,20 +1554,38 @@ export default {
candidateSelectionChange(rows) {
this.candidateSelection = rows || [];
},
addSelectedWaybills() {
const exists = new Set(this.selectedWaybillRows.map(row => String(row.id || row.waybillNo))) ;
const nextRows = [...this.selectedWaybillRows];
this.candidateSelection.forEach(row => {
const key = String(row.id || row.waybillNo);
if (!exists.has(key)) {
nextRows.push(row);
exists.add(key );
}
});
this.selectedWaybillRows = nextR ows ;
this.rebuildSummaryFromWaybills();
this.$refs.candidateTableRef?.clearSelection?.();
this.candidateSelection = [] ;
async addSelectedWaybills() {
this.candidateAdding = true ;
try {
const selectedRows = await Promise.all(
this.candidateSelection.map(async row => {
if (!row.id) return row;
try {
const res = await getWaybillDetail(row.id );
return { ...row, ...(res.data?.data || res.data || {}) };
} catch (error) {
return r ow;
}
})
) ;
const exists = new Set(
this.selectedWaybillRows.map(row => String(row.id || row.waybillNo))
);
const nextRows = [...this.selectedWaybillRows];
selectedRows.forEach(row => {
const key = String(row.id || row.waybillNo);
if (!exists.has(key)) {
nextRows.push(row);
exists.add(key);
}
});
this.selectedWaybillRows = nextRows;
this.rebuildSummaryFromWaybills();
this.$refs.candidateTableRef?.clearSelection?.();
this.candidateSelection = [];
} finally {
this.candidateAdding = false;
}
},
removeSelectedWaybill(index) {
this.selectedWaybillRows.splice(index, 1);
@@ -1444,17 +1626,18 @@ export default {
},
buildRouteNodes(rows) {
const nodeMap = new Map();
const appendNode = (address, tag) => {
if (!address) return;
if (!nodeMap.has(address)) nodeMap.set(address, { address, tags: [] });
nodeMap.get(address).tags.push(tag);
};
rows.forEach((row, rowIndex) => {
const departure = row.departureAddress || row.departureName;
appendNode(departure, `装${rowIndex + 1}`);
});
rows.forEach((row, rowIndex) => {
const arrival = row.arrivalAddress || row.arrivalName;
if ( dep arture) {
if (!nodeMap.has(departure)) nodeMap.set(departure, { address: departure, tags: [] });
nodeMap.get(departure).tags.push(`装${rowIndex + 1}`);
}
if (arrival) {
if (!nodeMap.has(arrival)) nodeMap.set(arrival, { address: arrival, tags: [] });
nodeMap.get(arrival).tags.push(`卸${rowIndex + 1}`);
}
appendNo de( arrival, `卸${rowIndex + 1}`);
});
return [...nodeMap.values()].map((node, index) => ({
...node,
@@ -1489,7 +1672,7 @@ export default {
},
buildCargoRows(rows) {
return rows.flatMap(row => {
const goodsRows = parseJsonArray(row.goodsJson);
const goodsRows = parseJsonArray(row.goodsList || row.goodsRows || row.goodsJson);
if (goodsRows.length) {
return goodsRows.map(item => this.normalizeCargoRow(row, item));
}
@@ -1509,6 +1692,9 @@ export default {
unitPrice: item.unitPrice || waybill.unitPrice || '',
materialCode: item.materialCode || '',
equipmentCode: item.equipmentCode || '',
brand: item.brand || item.brandName || waybill.brand || '',
specificationModel:
item.specificationModel || item.specModel || item.specification || waybill.specificationModel || '',
remark: item.remark || '',
};
},
@@ -1694,9 +1880,6 @@ export default {
label: item.dictValue || item.label || item.name,
value: item.dictKey || item.value || item.dictValue || item.name,
}));
if (!this.transportTypeOptions.length) {
this.transportTypeOptions = [{ label: defaultTransportType, value: defaultTransportType }];
}
},
async loadProjectOptions(keyword = '') {
this.projectLoading = true;
@@ -1710,7 +1893,7 @@ export default {
this.customerLoading = true;
try {
this.customerOptions = unwrapRecords(
await getCustomerList(1, 20, { customerName: keyword })
await getCustomerList(1, 20, { customerName: keyword, customerType: '承运商' })
);
} finally {
this.customerLoading = false;
@@ -1892,10 +2075,17 @@ export default {
margin - bottom : 8 px ;
}
. loading - manage - dialog _ _waybill - route - layout {
display : grid ;
grid - template - columns : minmax ( 0 , 2 fr ) minmax ( 320 px , 1 fr ) ;
gap : 16 px ;
align - items : start ;
margin - bottom : 16 px ;
}
. loading - manage - dialog _ _candidate - actions ,
. loading - manage - dialog _ _collapsed - search ,
. loading - manage - dialog _ _table - toolbar ,
. loading - manage - dialog _ _pagination ,
. loading - manage - dialog _ _footer {
display : flex ;
align - items : center ;
@@ -1908,16 +2098,18 @@ export default {
margin : 10 px 0 ;
}
. loading - manage - dialog _ _pagination {
padding : 10 px 0 4 px ;
}
. loading - manage - dialog _ _route - list {
display : flex ;
flex - direction : column ;
gap : 8 px ;
}
@ media ( max - width : 1200 px ) {
. loading - manage - dialog _ _waybill - route - layout {
grid - template - columns : 1 fr ;
}
}
. loading - manage - dialog _ _route - item {
display : grid ;
grid - template - columns : 28 px 36 px minmax ( 0 , 1 fr ) auto ;
@@ -1998,4 +2190,33 @@ export default {
-- el - table - border - color : # eff1f7 ;
}
}
. loading - detail {
display : flex ;
flex - direction : column ;
gap : 8 px ;
color : # 303133 ;
. loading - detail _ _summary { display : flex ; align - items : center ; flex - wrap : wrap ; gap : 24 px ; padding : 4 px 0 16 px ; }
. loading - detail _ _heading { font - size : 20 px ; font - weight : 600 ; margin - right : 8 px ; }
. loading - detail _ _route - title { display : inline - flex ; align - items : center ; gap : 16 px ; }
. loading - detail _ _label { color : # 909399 ; margin - right : 10 px ; }
. loading - detail _ _task - row { display : grid ; grid - template - columns : 1 fr 1 fr ; gap : 24 px ; padding - bottom : 4 px ; }
. loading - detail _ _field { display : flex ; flex - direction : column ; align - items : flex - start ; gap : 8 px ; }
. loading - detail _ _field . loading - detail _ _label { margin - right : 0 ; }
. loading - detail _ _value { display : flex ; flex - wrap : wrap ; gap : 8 px ; text - align : left ; }
. loading - detail _ _steps { display : flex ; align - items : flex - start ; overflow - x : auto ; padding : 8 px 4 px 16 px ; }
. loading - detail _ _step { position : relative ; min - width : 190 px ; flex : 1 ; text - align : center ; }
. loading - detail _ _step : not ( : last - child ) : : after { position : absolute ; z - index : 0 ; top : 16 px ; left : calc ( 50 % + 25 px ) ; width : calc ( 100 % - 50 px ) ; height : 2 px ; content : '' ; background : # 606266 ; }
. loading - detail _ _step - line { display : flex ; align - items : center ; justify - content : center ; }
. loading - detail _ _step - dot { position : relative ; z - index : 1 ; display : inline - flex ; width : 34 px ; height : 34 px ; align - items : center ; justify - content : center ; color : # fff ; border - radius : 4 px ; font - weight : 600 ; }
. loading - detail _ _step - dot . start { background : # 409 eff ; } . loading - detail _ _step - dot . middle { background : # 67 c23a ; } . loading - detail _ _step - dot . end { background : # e6a23c ; }
. loading - detail _ _step - address { margin - top : 8 px ; font - weight : 600 ; white - space : nowrap ; overflow : hidden ; text - overflow : ellipsis ; }
. loading - detail _ _step - tags { display : flex ; justify - content : center ; gap : 4 px ; margin - top : 6 px ; }
. loading - detail _ _bottom - grid { display : grid ; grid - template - columns : 1 fr 1 fr ; gap : 8 px ; }
. loading - detail _ _track - actions { display : flex ; gap : 8 px ; }
. loading - detail _ _map { width : 100 % ; min - height : 260 px ; border : 1 px solid # eff1f7 ; }
}
. loading - route - change - dialog { . loading - route - change - dialog _ _original { color : # a8abb2 ; line - height : 20 px ; margin - bottom : 4 px ; } . loading - route - change - dialog _ _location { cursor : pointer ; color : # 909399 ; } . loading - route - change - dialog _ _hint { color : # 909399 ; } . loading - route - change - dialog _ _route - list { display : flex ; flex - direction : column ; gap : 8 px ; } . loading - route - change - dialog _ _route - item { display : grid ; grid - template - columns : 34 px minmax ( 0 , 1 fr ) auto 24 px ; align - items : center ; gap : 12 px ; padding : 10 px 12 px ; background : # f5f6f7 ; cursor : move ; } . loading - route - change - dialog _ _route - type { width : 30 px ; height : 30 px ; border - radius : 50 % ; background : # dcdfe6 ; text - align : center ; line - height : 30 px ; } . loading - route - change - dialog _ _tags { display : flex ; gap : 4 px ; } . loading - route - change - dialog _ _record - content { white - space : normal ; overflow - wrap : anywhere ; line - height : 22 px ; } }
. loading - common - address - dialog _ _toolbar { display : flex ; gap : 8 px ; margin - bottom : 12 px ; } . loading - common - address - dialog _ _toolbar . el - input { flex : 1 ; }
@ media ( max - width : 900 px ) { . loading - detail _ _task - row , . loading - detail _ _bottom - grid { grid - template - columns : 1 fr ; } }
< / style >