1、新增应付明细

2、新增应收明细
3、新增异常处置
4、新增风险处置
5、新增在途追踪
6、修复业务模块bug
This commit is contained in:
2026-08-13 00:01:23 +08:00
parent 748dd80d4a
commit 6554ad80dd
33 changed files with 7868 additions and 1657 deletions
@@ -49,13 +49,16 @@
<section>
<div class="section-head">
<h3>收发货信息</h3>
<el-link type="primary" @click="openRouteDialog">选择线路</el-link>
<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 :model="form" label-position="right" label-width="auto"
><div class="route-steps">
<el-steps direction="vertical" :active="form.routes.length + 1">
<el-step>
<template #icon><span class="route-step-icon"></span></template>
<template #icon><span class="route-step-icon route-step-icon--start"></span></template>
<template #description>
<div class="route-node-form">
<el-form-item label="发货地址" required class="route-address-form-item"
@@ -80,7 +83,7 @@
</template>
</el-step>
<el-step v-for="(route, index) in form.routes" :key="index">
<template #icon><span class="route-step-icon"></span></template>
<template #icon><span class="route-step-icon route-step-icon--middle"></span></template>
<template #title><div class="route-step-title"><span class="route-segment-label">{{ index + 1 }}</span><span class="route-title-path">{{ getRouteTitle(index).replace(`${index + 1}`, '') }}</span><div class="route-transport-control"><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></template>
<template #description>
<div class="route-node-form">
@@ -89,12 +92,12 @@
><el-input v-model="route.departureName" class="route-address-name" readonly :disabled="!route.transportType" placeholder="请先选择运输类型" @click="openAddressMap(`route-${index}`)" /><el-input v-model="route.departureAddress" class="route-address-detail" readonly :disabled="!route.transportType" placeholder="请先选择运输类型" @click="openAddressMap(`route-${index}`)" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="List" :disabled="!route.transportType" @click="openCommonAddress(`route-${index}`)" /></el-tooltip></div
></el-form-item>
<el-form-item label="联系人"><el-input v-model="route.departureContact" placeholder="请输入" /></el-form-item>
<el-form-item label="联系方式"><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 v-if="form.routes.length > 1" 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>
<el-form-item label="联系方式"><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>
</template>
</el-step>
<el-step>
<template #icon><span class="route-step-icon"></span></template>
<template #icon><span class="route-step-icon route-step-icon--end"></span></template>
<template #title><div class="route-step-title"><span class="route-segment-label">{{ form.routes.length + 1 }}</span><span class="route-title-path">{{ getFinalRouteTitle().replace(`${form.routes.length + 1}`, '') }}</span><div class="route-transport-control"><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></template>
<template #description>
<div class="route-node-form">
@@ -155,12 +158,13 @@
><el-input
v-model="row.cargoType"
placeholder="从货物类型获取" /></template></el-table-column
><el-table-column label="数量" width="104" class-name="master-editor__quantity-cell"
><el-table-column label="数量" width="132" class-name="master-editor__quantity-cell"
><template #default="{ row }"
><el-input-number
><el-input
v-model="row.quantity"
:min="0"
controls-position="right" /></template></el-table-column
inputmode="decimal"
placeholder="请输入"
@input="value => handleQuantityInput(row, value)" /></template></el-table-column
><el-table-column label="数量单位" width="120"
><template #default="{ row }"
><el-select v-model="row.quantityUnit"
@@ -200,13 +204,49 @@
</section>
<section>
<h3>附件</h3>
<el-upload
multiple
action="/api/blade-resource/oss/endpoint/put-file"
:headers="uploadHeaders"
:on-success="attach"
><el-button type="primary" plain>上传附件</el-button></el-upload
>
<div class="master-editor__attachment">
<div class="master-editor__attachment-head">
<vehicle-attachment-upload
v-model="attachmentRows"
:file-types="attachmentFileTypes"
:max-size="500"
:show-tip="false"
:show-file-list="false"
button-text="上传附件"
@change="handleAttachmentChange"
/>
<el-button
type="primary"
:disabled="!attachmentRows.length"
@click="handleAttachmentBatchDownload"
>
批量下载
</el-button>
</div>
<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 }">{{ row.originalName || row.name }}</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>
</section>
<el-dialog
v-model="addressDialogVisible"
@@ -402,9 +442,9 @@ import { getList as getPortTerminalList } from '@/api/base/port-terminal';
import { getList as getRailwayStationList } from '@/api/base/railway-station';
import { getList as getCommonCargoList } from '@/api/business/common-cargo';
import { exportBlob, importBlob } from '@/api/common';
import { downloadXls } from '@/utils/util';
import { getUploadHeaders } from '@/utils/upload';
import { List, Location, Search } from '@element-plus/icons-vue';
import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { List, Search } from '@element-plus/icons-vue';
import { mapGetters } from 'vuex';
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
@@ -455,7 +495,24 @@ export default {
cargoOptions: [],
cargoImportVisible: false,
cargoImportLoading: false,
uploadHeaders: getUploadHeaders(),
attachmentRows: [],
selectedAttachmentRows: [],
attachmentFileTypes: [
'pdf',
'bmp',
'jpeg',
'png',
'jpg',
'doc',
'docx',
'ppt',
'pptx',
'xlsx',
'xls',
'eml',
'msg',
'zip',
],
transports: ['公路运输', '铁路运输', '水路运输', '航空运输'],
quantityUnitOptions,
form: {
@@ -471,6 +528,7 @@ export default {
};
},
computed: {
...mapGetters(['userInfo']),
lastRouteName() {
return this.form.routes[this.form.routes.length - 1]?.departureName || '途经地';
},
@@ -513,10 +571,23 @@ export default {
? finalSegment.transportType
: detail.finalTransportType || '',
goods: detail.goods || [{ quantityUnit: quantityUnitOptions[0] }],
attachmentsJson: detail.attachmentsJson || '[]',
transportOrganizationType: '多式联运',
};
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 [];
}
},
getRouteTitle(index) {
const departureName =
index === 0
@@ -980,7 +1051,7 @@ export default {
this.form.routes.push(this.createRoute());
},
removeRoute(index) {
if (this.form.routes.length > 1) this.form.routes.splice(index, 1);
this.form.routes.splice(index, 1);
},
addGoods(index) {
this.form.goods.splice(index + 1, 0, { quantityUnit: quantityUnitOptions[0] });
@@ -988,10 +1059,56 @@ export default {
removeGoods(index) {
if (this.form.goods.length > 1) this.form.goods.splice(index, 1);
},
attach(response) {
const list = JSON.parse(this.form.attachmentsJson || '[]');
list.push(response.data || response);
this.form.attachmentsJson = JSON.stringify(list);
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`;
},
downloadAttachment(row) {
const url = row.url || row.link;
if (!url) {
this.$message.warning('附件地址为空');
return;
}
downloadFileByUrl(url, row.originalName || row.name || '附件');
},
handleAttachmentBatchDownload() {
const rows = this.selectedAttachmentRows.length
? this.selectedAttachmentRows
: this.attachmentRows;
rows.forEach(row => this.downloadAttachment(row));
},
async save(draft = false) {
if (!draft) {
@@ -1001,7 +1118,6 @@ export default {
if (!this.form.goods.some(item => item.cargoType && item.quantity > 0 && item.quantityUnit))
return this.$message.warning('请至少填写一条有效货物');
if (
!this.form.routes.length ||
this.form.routes.some(item => !item.departureName || !item.transportType) ||
!this.form.finalTransportType
)
@@ -1009,6 +1125,7 @@ export default {
}
const payload = {
...this.form,
attachmentsJson: JSON.stringify(this.attachmentRows),
routes: [
...this.form.routes.map((route, index) => ({ ...route, segmentNo: `${index + 1}` })),
{
@@ -1024,7 +1141,8 @@ export default {
delete payload.finalTransportType;
const res = await (draft ? api.saveDraft(payload) : api.submit(payload));
this.$message.success('保存成功');
return res.data || res;
// 兼容 Axios 响应及 BladeX 常见的 data.data 两层响应结构。
return res?.data?.data || res?.data || res;
},
async saveDraft() {
await this.save(true);
@@ -1036,7 +1154,12 @@ export default {
},
async createAndDispatch() {
const data = await this.save();
this.$emit('dispatch', data.id);
const id = data?.id || data?.masterOrderId || data?.masterId;
if (!id) {
this.$message.error('保存成功但未获取到总单ID,无法进入调度');
return;
}
this.$emit('dispatch', id);
},
},
};
@@ -1060,15 +1183,15 @@ export default {
.goods-heading {
display: flex;
margin-bottom: 8px;
justify-content: flex-end;
justify-content: flex-start;
align-items: center;
h3 {
margin-right: auto;
margin-bottom: 0;
}
}
.goods-toolbar {
display: flex;
margin-left: auto;
gap: 8px;
}
.goods-actions {
@@ -1076,8 +1199,31 @@ export default {
flex-wrap: wrap;
gap: 8px;
}
:deep(.master-editor__quantity-cell .el-input-number) {
width: 80px;
.master-editor__attachment {
width: 100%;
}
.master-editor__attachment-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.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: 108px;
}
.section-head {
display: flex;
@@ -1088,6 +1234,10 @@ export default {
margin-bottom: 0;
}
}
.section-head__actions {
display: flex;
gap: 16px;
}
.route-dialog__search {
margin-bottom: 8px;
padding: 12px 12px 4px;
@@ -1138,6 +1288,12 @@ export default {
font-size: 16px;
font-weight: 600;
line-height: 1;
&--middle {
background: #67c23a;
}
&--end {
background: #e6a23c;
}
}
}
.route-node-form {