1、新增结算模块

2、调整业务模块
3、调整客商模块
This commit is contained in:
2026-08-20 06:33:01 +08:00
parent 4199255186
commit fd5b1484e6
59 changed files with 11745 additions and 1104 deletions
+300 -38
View File
@@ -1,6 +1,6 @@
<template>
<basic-container class="loading-manage-page">
<div class="loading-manage-page__search">
<div v-if="!isStandaloneFormPage" class="loading-manage-page__search">
<el-form :model="searchForm" label-position="right" label-width="88px">
<div class="loading-manage-page__search-grid">
<el-form-item label="配载单号">
@@ -220,7 +220,7 @@
</el-form>
</div>
<div class="loading-manage-page__toolbar">
<div v-if="!isStandaloneFormPage" class="loading-manage-page__toolbar">
<div class="loading-manage-page__toolbar-left">
<el-button type="primary" @click="openLoadingDialog('add')">新建配载</el-button>
<el-button type="primary" plain @click="handleExport">批量导出</el-button>
@@ -240,6 +240,7 @@
</div>
<el-table
v-if="!isStandaloneFormPage"
ref="tableRef"
v-loading="loading"
:data="tableData"
@@ -260,7 +261,7 @@
>{{ row.loadingNo }}</el-link>
</template>
</el-table-column>
<el-table-column label="配载子单号" prop="loadingSubNos" min-width="220">
<el-table-column label="单号" prop="loadingSubNos" min-width="220">
<template #default="{ row }">
<span class="loading-manage-page__sub-nos">
<template v-if="splitText(row.loadingSubNos).length">
@@ -280,14 +281,11 @@
</template>
</el-table-column>
<el-table-column
label="车牌号"
prop="vehicleNo"
min-width="170"
show-overflow-tooltip
>
<template #header>
<span class="loading-manage-page__vehicle-header">车牌号/航班号/船号<br />班列号</span>
</template>
</el-table-column>
/>
<el-table-column label="司机" prop="driverName" min-width="120" show-overflow-tooltip />
<el-table-column label="联系电话" prop="driverPhone" min-width="140" show-overflow-tooltip />
<el-table-column label="承运类型" prop="carrierType" min-width="120" show-overflow-tooltip />
@@ -357,7 +355,7 @@
</el-table-column>
</el-table>
<div class="loading-manage-page__pagination">
<div v-if="!isStandaloneFormPage" class="loading-manage-page__pagination">
<el-pagination
v-model:current-page="page.currentPage"
v-model:page-size="page.pageSize"
@@ -370,14 +368,24 @@
/>
</div>
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
append-to-body
destroy-on-close
width="96%"
top="3vh"
class="loading-manage-dialog"
<component
:is="isStandaloneFormPage ? 'div' : 'el-dialog'"
:model-value="isStandaloneFormPage ? undefined : dialogVisible"
:title="isStandaloneFormPage ? undefined : dialogTitle"
:append-to-body="isStandaloneFormPage ? undefined : true"
:destroy-on-close="isStandaloneFormPage ? undefined : true"
:width="isStandaloneFormPage ? undefined : '96%'"
:top="isStandaloneFormPage ? undefined : '3vh'"
:class="[
'loading-manage-dialog',
{ 'loading-manage-form-dialog': isStandaloneFormPage },
{ 'loading-manage-form-page': isStandaloneFormPage },
]"
:modal="isStandaloneFormPage ? undefined : true"
:show-close="isStandaloneFormPage ? undefined : true"
:close-on-click-modal="isStandaloneFormPage ? undefined : true"
:close-on-press-escape="isStandaloneFormPage ? undefined : true"
@update:model-value="value => !isStandaloneFormPage && (dialogVisible = value)"
@closed="resetLoadingDialog"
>
<div v-loading="dialogLoading" class="loading-manage-dialog__body">
@@ -396,7 +404,7 @@
</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" />
<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.displayAddress || formatAddressText(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>
@@ -734,13 +742,21 @@
label="发货地"
min-width="220"
show-overflow-tooltip
/>
>
<template #default="{ row }">
{{ formatWaybillAddress(row, 'departure') }}
</template>
</el-table-column>
<el-table-column
prop="arrivalAddress"
label="到货地"
min-width="220"
show-overflow-tooltip
/>
>
<template #default="{ row }">
{{ formatWaybillAddress(row, 'arrival') }}
</template>
</el-table-column>
<el-table-column label="操作" width="110" fixed="right">
<template #default="{ $index }">
<el-link
@@ -766,7 +782,7 @@
>
<el-icon class="loading-manage-dialog__route-handle"><Rank /></el-icon>
<span :class="['loading-manage-dialog__route-type', `loading-manage-dialog__route-type--${routeTypeClass(index)}`]">{{ routeTypeText(index) }}</span>
<span class="loading-manage-dialog__route-address">{{ node.address }}</span>
<span class="loading-manage-dialog__route-address">{{ node.displayAddress || formatAddressText(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>
@@ -804,7 +820,7 @@
@change="handleCarrierTypeChange"
/>
</div>
<div class="loading-manage-dialog__grid">
<div class="loading-manage-dialog__grid loading-manage-dialog__task-grid">
<el-form-item v-if="!['自运', '网货平台'].includes(dialogForm.carrierType)" label="承运商" required>
<el-select
v-model="dialogForm.carrierName"
@@ -893,10 +909,24 @@
</el-form>
</div>
<template #footer>
<div v-if="isStandaloneFormPage" class="loading-manage-dialog__footer">
<template v-if="dialogReadonly">
<el-button @click="closeLoadingDialog">关闭</el-button>
</template>
<template v-else>
<el-button @click="closeLoadingDialog">关闭</el-button>
<el-button type="primary" plain :loading="dialogSaving === 'draft'" @click="saveDraft">
暂存
</el-button>
<el-button type="primary" :loading="dialogSaving === 'submit'" @click="submitLoading">
{{ dialogMode === 'reassign' ? '确认派单' : '确认' }}
</el-button>
</template>
</div>
<template v-if="!isStandaloneFormPage" #footer>
<div class="loading-manage-dialog__footer">
<template v-if="dialogReadonly">
<el-button @click="dialogVisible = false">关闭</el-button>
<el-button @click="closeLoadingDialog">关闭</el-button>
</template>
<template v-else>
<el-button @click="clearLoadingDialog">清空</el-button>
@@ -909,7 +939,7 @@
</template>
</div>
</template>
</el-dialog>
</component>
<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">
@@ -1192,19 +1222,51 @@ export default {
typeof item === 'string' ? { name: item.split('/').pop(), url: item } : { name: item.name || item.fileName || '附件', url: item.url || item.link || item.fileUrl }
);
},
isStandaloneFormPage() {
return (
this.$route.path === '/business/loading-manage/form' &&
['add', 'edit'].includes(this.$route.query.mode)
);
},
},
mounted() {
this.loadTransportTypeOptions();
this.loadTable();
if (this.isStandaloneFormPage) {
this.syncStandaloneTagTitle();
this.initStandaloneFormPage();
}
const detailId = this.$route.query.detailId;
if (detailId) this.openLoadingDialog('view', { id: detailId });
},
watch: {
$route(to, from) {
if (this.isStandaloneFormPage) {
this.syncStandaloneTagTitle();
if (
to.query.mode !== from?.query?.mode ||
String(to.query.id || '') !== String(from?.query.id || '')
) {
this.initStandaloneFormPage();
}
} else if (from?.query?.mode && this.dialogVisible) {
this.closeLoadingDialog();
}
},
'$route.query.detailId'(detailId) {
if (detailId) this.openLoadingDialog('view', { id: detailId });
},
},
methods: {
syncStandaloneTagTitle() {
if (!this.isStandaloneFormPage) return;
const title = this.$route.query.mode === 'edit' ? '编辑配载管理' : '新增配载管理';
if (this.$route.query.name === title) return;
this.$store.commit('SET_TAG', {
fullPath: this.$route.fullPath,
query: { ...this.$route.query, name: title },
});
},
buildQueryParams(form) {
const params = { ...form };
const [startDateStart, startDateEnd] = params.startDateRange || [];
@@ -1333,6 +1395,64 @@ export default {
.flatMap(value => this.splitText(value));
return [...new Set(values)].join('') || '-';
},
formatWaybillAddress(row, type) {
const prefix = type === 'departure' ? 'departure' : 'arrival';
const address = row[`${prefix}Address`] || row[`${prefix}Name`] || '';
const region = this.mergeRouteAddressParts(
this.routeProvinceName(row, prefix),
row[`${prefix}CityName`] || row[`${prefix}City`],
row[`${prefix}DistrictName`] || row[`${prefix}District`]
);
const source = this.mergeRouteAddressParts(region, '', address);
const parts = this.parseWaybillAddress(source);
const detailAddress =
row[`${prefix}DetailAddress`] ||
row[`${prefix}AddressDetail`] ||
parts.detailAddress ||
address;
return [parts.province, parts.city, parts.district, detailAddress]
.map(value => String(value || '').trim())
.filter(Boolean)
.join(' / ') || '-';
},
parseWaybillAddress(address) {
let rest = String(address || '').trim();
const result = { province: '', city: '', district: '', detailAddress: '' };
if (!rest) return result;
const municipalityMatch = rest.match(/^(北京市|天津市|上海市|重庆市)/);
if (municipalityMatch) {
result.province = municipalityMatch[0];
result.city = municipalityMatch[0];
rest = rest.slice(municipalityMatch[0].length);
} else {
const provinceMatch = rest.match(/^(.+?(?:省|自治区|特别行政区))/);
if (provinceMatch) {
result.province = provinceMatch[0];
rest = rest.slice(provinceMatch[0].length);
}
const cityMatch = rest.match(/^(.+?(?:市|自治州|地区|盟))/);
if (cityMatch) {
result.city = cityMatch[0];
rest = rest.slice(cityMatch[0].length);
}
}
const districtMatch = rest.match(/^(.+?(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗))/);
if (districtMatch) {
result.district = districtMatch[0];
rest = rest.slice(districtMatch[0].length);
}
result.detailAddress = rest;
return result;
},
formatAddressText(address) {
const parts = this.parseWaybillAddress(address);
return [parts.province, parts.city, parts.district, parts.detailAddress]
.map(value => String(value || '').trim())
.filter(Boolean)
.join(' / ') || String(address || '').trim() || '-';
},
mergeRouteAddressParts(region, district, address) {
const values = [region, district, address].map(value => String(value || '').trim()).filter(Boolean);
if (!values.length) return '';
@@ -1366,8 +1486,8 @@ export default {
if (status === 'draft') {
return [
{ type: 'edit', label: '编辑' },
{ type: 'delete', label: '删除' },
{ type: 'copy', label: '复制' },
{ type: 'delete', label: '删除' },
];
}
if (status === 'pending') {
@@ -1406,7 +1526,24 @@ export default {
};
actionMap[type]?.();
},
initStandaloneFormPage() {
if (!this.isStandaloneFormPage) return;
this.openLoadingDialog(this.$route.query.mode === 'edit' ? 'edit' : 'add', {
id: this.$route.query.id,
});
},
async openLoadingDialog(mode, row) {
if (['add', 'edit'].includes(mode) && !this.isStandaloneFormPage) {
this.$router.push({
path: '/business/loading-manage/form',
query: {
mode,
...(mode === 'edit' && row?.id ? { id: row.id } : {}),
name: mode === 'edit' ? '编辑配载管理' : '新增配载管理',
},
});
return;
}
this.dialogMode = mode;
this.dialogVisible = true;
this.dialogLoading = true;
@@ -1439,6 +1576,14 @@ export default {
this.dialogLoading = false;
}
},
closeLoadingDialog() {
if (this.isStandaloneFormPage) {
this.$router.$avueRouter?.closeTag?.();
this.$router.push({ path: '/business/loading-manage', query: {} });
return;
}
this.dialogVisible = false;
},
resetLoadingDialog() {
this.resetDialogData();
this.dialogSaving = '';
@@ -1814,18 +1959,18 @@ export default {
},
buildRouteNodes(rows) {
const nodeMap = new Map();
const appendNode = (address, tag) => {
const appendNode = (address, displayAddress, tag) => {
if (!address) return;
if (!nodeMap.has(address)) nodeMap.set(address, { address, tags: [] });
if (!nodeMap.has(address)) nodeMap.set(address, { address, displayAddress, tags: [] });
nodeMap.get(address).tags.push(tag);
};
rows.forEach((row, rowIndex) => {
const departure = row.departureAddress || row.departureName;
appendNode(departure, `装${rowIndex + 1}`);
appendNode(departure, this.formatWaybillAddress(row, 'departure'), `装${rowIndex + 1}`);
});
rows.forEach((row, rowIndex) => {
const arrival = row.arrivalAddress || row.arrivalName;
appendNode(arrival, `卸${rowIndex + 1}`);
appendNode(arrival, this.formatWaybillAddress(row, 'arrival'), `卸${rowIndex + 1}`);
});
return [...nodeMap.values()].map((node, index) => ({
...node,
@@ -1850,10 +1995,24 @@ export default {
const rows = [...this.routeNodes];
const [target] = rows.splice(this.routeDragIndex, 1);
rows.splice(index, 0, target);
this.routeNodes = rows;
this.routeDragIndex = -1;
if (!this.validateRouteEndpoints(rows)) return;
this.routeNodes = rows;
this.syncRouteAddressFields();
},
validateRouteEndpoints(nodes = this.routeNodes) {
const startTags = nodes[0]?.tags || [];
if (startTags.some(tag => String(tag).startsWith('卸'))) {
ElMessage.warning('起点不能设置卸货');
return false;
}
const endTags = nodes[nodes.length - 1]?.tags || [];
if (endTags.some(tag => String(tag).startsWith('装'))) {
ElMessage.warning('终点不能设置装货');
return false;
}
return true;
},
syncRouteAddressFields() {
const nodes = this.routeNodes.filter(item => item.address);
this.dialogForm.departureAddress = nodes[0]?.address || '';
@@ -1945,6 +2104,13 @@ export default {
if (draftMode) {
return true;
}
if (this.dialogMode === 'add' && this.selectedWaybillRows.length < 2) {
ElMessage.warning('请至少选择两条待配载运单');
return false;
}
if (!this.validateRouteEndpoints()) {
return false;
}
if (!this.dialogForm.departureAddress || !this.dialogForm.arrivalAddress) {
ElMessage.warning('请确认配载路线');
return false;
@@ -1995,7 +2161,7 @@ export default {
await loadingApi.submit(payload);
ElMessage.success('确认成功');
}
this.dialogVisible = false;
this.closeLoadingDialog();
this.loadTable();
} finally {
this.dialogSaving = '';
@@ -2249,11 +2415,6 @@ export default {
word-break: break-word;
}
.loading-manage-page__vehicle-header {
line-height: 20px;
white-space: nowrap;
}
.loading-manage-page__status {
color: #303133;
}
@@ -2340,7 +2501,7 @@ export default {
.loading-manage-dialog__waybill-route-layout {
display: grid;
grid-template-columns: minmax(0, 2fr) minmax(320px, 1fr);
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
align-items: start;
margin-bottom: 16px;
@@ -2424,6 +2585,23 @@ export default {
margin-bottom: 12px;
}
.loading-manage-dialog__task-grid {
:deep(.el-form-item__label) {
flex: 0 0 100px;
width: 100px;
}
:deep(.el-form-item__content),
:deep(.el-input),
:deep(.el-select),
:deep(.el-autocomplete),
:deep(.el-date-editor.el-input),
:deep(.el-date-editor.el-input__wrapper) {
min-width: 0;
width: 100%;
}
}
:deep(.dialog-section-title) {
display: flex;
align-items: center;
@@ -2503,6 +2681,90 @@ export default {
.loading-route-change-dialog__record-content { white-space: normal; overflow-wrap: anywhere; line-height: 22px; }
}
.loading-common-address-dialog__toolbar { display: flex; gap: 8px; margin-bottom: 12px; }.loading-common-address-dialog__toolbar .el-input { flex: 1; }
:global(.loading-manage-form-page) {
width: 100%;
min-height: 100%;
box-sizing: border-box;
background: #f5f6fa;
}
:global(.loading-manage-form-page .loading-manage-dialog__body) {
max-height: none;
overflow: visible;
padding-right: 0;
}
:global(.loading-manage-form-dialog) {
width: 100% !important;
min-height: 100%;
margin: 0 !important;
border-radius: 0;
box-shadow: none;
}
:global(.loading-manage-form-dialog .el-dialog__body) {
max-height: none;
padding: 20px 24px 96px;
background: #f5f6fa;
}
:global(.el-overlay:has(.loading-manage-form-dialog)) {
position: fixed !important;
inset: 92px 0 0 230px !important;
z-index: 100 !important;
overflow: auto;
background: transparent;
}
:global(.el-overlay:has(.loading-manage-form-dialog) .el-overlay-dialog) {
min-height: 100%;
overflow: visible;
}
:global(.loading-manage-form-dialog .el-dialog__header) {
padding: 18px 24px;
margin-right: 0;
background: #fff;
border-bottom: 1px solid #eff1f7;
}
:global(.loading-manage-form-dialog .el-dialog__footer) {
position: fixed;
right: 0;
bottom: 0;
left: 230px;
z-index: 1001;
box-sizing: border-box;
padding: 12px 24px;
background: #fff;
border-top: 1px solid #eff1f7;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
}
:global(.loading-manage-form-page .loading-manage-dialog__footer) {
position: fixed;
right: 0;
bottom: 0;
left: 230px;
z-index: 1001;
box-sizing: border-box;
min-height: 64px;
padding: 12px 24px;
background: #fff;
border-top: 1px solid #eff1f7;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
}
:global(.avue--collapse .el-overlay:has(.loading-manage-form-dialog)) {
left: 60px !important;
}
:global(.avue-layout--horizontal .el-overlay:has(.loading-manage-form-dialog)) {
top: 92px !important;
left: 0 !important;
}
:global(.avue--collapse .loading-manage-form-dialog .el-dialog__footer) {
left: 60px;
}
:global(.avue-layout--horizontal .loading-manage-form-dialog .el-dialog__footer) {
left: 0;
}
:global(.avue--collapse .loading-manage-form-page .loading-manage-dialog__footer) {
left: 60px;
}
:global(.avue-layout--horizontal .loading-manage-form-page .loading-manage-dialog__footer) {
left: 0;
}
@media (max-width: 900px) {
.loading-manage-dialog__grid {
grid-template-columns: 1fr;