Merge remote-tracking branch 'origin/master' into feature/hcs-api-p0-web

# Conflicts:
#	src/views/business/contract-manage-change.vue
This commit is contained in:
刘泉佳
2026-10-23 19:20:12 +08:00
27 changed files with 1924 additions and 572 deletions
@@ -13824,13 +13824,13 @@ export default {
}
const unavailableWaybills = this.selectionList.filter(
row =>
this.statusValue(row) !== 'pending' ||
!['pending', 'processing', 'running'].includes(this.statusValue(row)) ||
row.loadingId ||
row.loadingManageId ||
row.loadingNo
);
if (unavailableWaybills.length) {
this.$message.warning('公路配载仅支持进行中且未配载的运单');
this.$message.warning('公路配载仅支持待执行/进行中且未关联有效配载单的运单');
return;
}
try {
@@ -94,7 +94,7 @@
<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="isBasicRouteReadonly || !route.transportType" placeholder="请先选择运输方式" /><el-cascader v-else v-model="addressRegionPaths[`route-${index}`]" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="isBasicRouteReadonly || !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="isBasicRouteReadonly || !route.transportType" placeholder="请先选择运输方式" @click="handleAddressDetailClick(`route-${index}`)" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="isBasicRouteReadonly || !route.transportType" @click="openCommonAddress(`route-${index}`)" /></el-tooltip></div
><el-input v-if="isNonRoadAddress(`route-${index}`)" v-model="route.departureName" class="route-address-name" readonly :disabled="isBasicRouteReadonly" placeholder="请选择站点" /><el-cascader v-else v-model="addressRegionPaths[`route-${index}`]" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="isBasicRouteReadonly" 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="isBasicRouteReadonly" placeholder="请输入详细地址" @click="handleAddressDetailClick(`route-${index}`)" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="isBasicRouteReadonly" @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" :disabled="isBasicRouteReadonly" placeholder="请输入" /></el-form-item>
<el-form-item label="联系方式" class="route-phone-form-item"><div class="route-contact-actions"><el-input v-model="route.departurePhone" :disabled="isBasicRouteReadonly" placeholder="请输入" /><template v-if="!isBasicRouteReadonly"><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></template></div></el-form-item>
@@ -111,7 +111,7 @@
<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="isBasicRouteReadonly || !form.finalTransportType" placeholder="请先选择运输方式" /><el-cascader v-else v-model="addressRegionPaths.arrival" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="isBasicRouteReadonly || !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="isBasicRouteReadonly || !form.finalTransportType" placeholder="请先选择运输方式" @click="handleAddressDetailClick('arrival')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="isBasicRouteReadonly || !form.finalTransportType" @click="openCommonAddress('arrival')" /></el-tooltip></div
><el-input v-if="isNonRoadAddress('arrival')" v-model="form.arrivalName" class="route-address-name" readonly :disabled="isBasicRouteReadonly" placeholder="请选择站点" /><el-cascader v-else v-model="addressRegionPaths.arrival" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="isBasicRouteReadonly" 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="isBasicRouteReadonly" placeholder="请输入详细地址" @click="handleAddressDetailClick('arrival')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="isBasicRouteReadonly" @click="openCommonAddress('arrival')" /></el-tooltip></div
></el-form-item>
<el-form-item label="联系人" class="route-contact-form-item"
><el-input v-model="form.arrivalContact" :disabled="isBasicRouteReadonly" placeholder="请输入" /></el-form-item>
@@ -714,7 +714,7 @@ export default {
'msg',
'zip',
],
transports: ['公路运输', '铁路运输', '水路运输', '航空运输'],
transports: ['铁路运输', '水路运输', '航空运输', '公路运输'],
quantityUnitOptions: [],
quantityUnitLoading: false,
form: {
@@ -798,6 +798,7 @@ export default {
methods: {
formatContractOptionLabel,
defaultQuantityUnit() {
if (this.quantityUnitOptions.includes('吨')) return '吨';
return this.quantityUnitOptions[0] || '吨';
},
loadQuantityUnitOptions() {
@@ -1164,22 +1165,30 @@ export default {
const field = this.addressModel(target);
if (!field) return false;
const siteCode = String(field.model[field.siteCode] || '').trim();
// 地址展示模式由实际选择的数据决定:有站点编码表示非公路站点名称,
// 没有站点编码则保留省市区级联,避免切换运输方式时改写已有地址。
// 展示模式由已选数据决定:有站点编码则显示站点名,否则省市区;暂不按运输方式强制站点限制。
return Boolean(siteCode && siteCode !== '/');
},
hasAddressValue(target) {
const field = this.addressModel(target);
if (!field) return false;
return Boolean(
String(field.model[field.address] || '').trim() || String(field.model[field.name] || '').trim()
);
},
tipEmptyAddressDetail(target) {
// 替代原「请先选择运输方式」placeholder:空地址点击时 toast,已有值不再提示。
if (!this.hasAddressValue(target)) {
this.$message.warning('请输入详细地址');
}
},
resolveAddressRegionCode(row = {}) {
return row.regionCode || row.districtCode || row.areaCode || row.adcode || '';
},
openCommonAddress(target) {
if (this.isBasicRouteReadonly) return;
this.tipEmptyAddressDetail(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;
@@ -1279,18 +1288,11 @@ export default {
},
handleAddressDetailClick(target) {
if (this.isBasicRouteReadonly) return;
if (this.addressTransportMode(target) !== 'road') {
this.openCommonAddress(target);
return;
}
this.tipEmptyAddressDetail(target);
this.openAddressMap(target);
},
openAddressMap(target) {
if (this.isBasicRouteReadonly) return;
if (this.addressTransportMode(target) !== 'road') {
this.$message.info('非公路运输地址不能使用地图选择');
return;
}
const field = this.addressModel(target);
if (!field) return;
this.mapTarget = target;
@@ -1690,7 +1692,11 @@ export default {
'',
cargoTypePath,
quantity: row.quantity || '',
quantityUnit: row.quantityUnit || this.defaultQuantityUnit(),
// 常用货物单位若为空或「个」,新建总单统一默认「吨」
quantityUnit:
!row.quantityUnit || String(row.quantityUnit).trim() === '个'
? this.defaultQuantityUnit()
: row.quantityUnit,
packageType: row.packageType || row.package || '',
brand: row.brand || '',
specification: row.specification || row.spec || '',
@@ -1775,17 +1781,21 @@ export default {
return formatUpdateUserName(row);
},
selectRoute(route) {
// 先写入运输方式(会触发地点清空),再回填线路地址,避免被清空逻辑覆盖。
this.form.finalTransportType = '公路运输';
if (!this.form.routes.length) this.addRoute();
if (this.form.routes[0]) this.form.routes[0].transportType = '公路运输';
this.form.departureName = route.departureName || '';
this.form.departureAddress = route.departureAddress || '';
this.form.departureContact = route.departureContact || '';
this.form.departurePhone = route.departurePhone || '';
this.form.departureSiteCode = '';
this.form.arrivalName = route.arrivalName || '';
this.form.arrivalAddress = route.arrivalAddress || '';
this.form.arrivalContact = route.arrivalContact || '';
this.form.arrivalPhone = route.arrivalPhone || '';
this.form.arrivalSiteCode = '';
this.syncAddressRegionPaths();
this.form.finalTransportType = '公路运输';
if (!this.form.routes.length) this.addRoute();
this.routeDialogVisible = false;
},
createRoute(index = this.form.routes.length) {
@@ -1591,8 +1591,8 @@
<el-link type="primary" v-if="canCancel(row)" @click="handleAction('cancel', row)">
取消
</el-link>
<el-link type="primary" v-if="canComplete(row)" @click="handleAction('complete', row)">
{{ completeActionLabel }}
<el-link type="primary" v-if="canComplete(row)" @click="handleCompleteAction(row)">
{{ completeActionLabelFor(row) }}
</el-link>
<el-link type="primary" v-if="canMaintainMileage(row)" @click="openMileageDialog(row)">
维护里程
@@ -5111,7 +5111,29 @@ export default {
return false;
}
const status = this.statusValue(row);
return status === 'processing' || status === 'running';
return status === 'processing' || status === 'running' || status === 'pending_return';
},
completeActionLabelFor(row) {
return this.statusValue(row) === 'pending_return' ? '确认回单' : this.completeActionLabel;
},
handleCompleteAction(row) {
const isPendingReturn = this.statusValue(row) === 'pending_return';
const actionName = this.completeActionLabelFor(row);
this.$confirm(`确定${actionName}该${this.config.title}?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
if (isPendingReturn && typeof this.api.confirmReturn === 'function') {
return this.api.confirmReturn(row.id);
}
return this.api.complete(row.id);
})
.then(() => {
this.$message.success(`${actionName}成功`);
this.onLoad(this.page, this.query);
});
},
canMaintainMileage(row) {
return (
@@ -6543,6 +6565,9 @@ export default {
if (status === 'completed') {
return 'primary';
}
if (status === 'pending_return') {
return 'warning';
}
return 'warning';
},
normalizeRow(row, saveAsDraft = false) {
@@ -10356,7 +10381,14 @@ export default {
this.$message.warning('请选择至少一条数据');
return;
}
this.$confirm('确定完成选中的运单?', '提示', {
const pendingReturnCount = this.selectionList.filter(
row => this.statusValue(row) === 'pending_return'
).length;
const tip =
pendingReturnCount > 0
? `确定完成选中的运单?其中 ${pendingReturnCount} 条为待确认回单,确认后将转为已完成。`
: '确定完成选中的运单?';
this.$confirm(tip, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
@@ -10382,13 +10414,13 @@ export default {
}
const unavailableWaybills = this.selectionList.filter(
row =>
!['processing', 'running'].includes(this.statusValue(row)) ||
!['pending', 'processing', 'running'].includes(this.statusValue(row)) ||
row.loadingId ||
row.loadingManageId ||
row.loadingNo
);
if (unavailableWaybills.length) {
this.$message.warning('公路配载仅支持进行中且未配载的运单');
this.$message.warning('公路配载仅支持待执行/进行中且未关联有效配载单的运单');
return;
}
try {
+306 -28
View File
@@ -8,11 +8,17 @@
<el-form-item label="变更类型" class="contract-basic-section__change-type">
<el-radio-group v-model="form.changeType">
<el-radio label="合同信息变更" />
<el-radio label="结算规则变更" />
<el-radio label="终止合同" />
</el-radio-group>
</el-form-item>
<el-form-item label="签约类型" prop="signType">
<el-select v-model="form.signType" placeholder="请选择签约类型" clearable>
<el-select
v-model="form.signType"
placeholder="请选择签约类型"
clearable
:disabled="isSettlementRuleChange"
>
<el-option
v-for="item in signTypeOptions"
:key="item.value"
@@ -30,6 +36,7 @@
maxlength="100"
show-word-limit
placeholder="请输入合同名称"
:disabled="isSettlementRuleChange"
/>
</el-form-item>
<el-form-item label="合同类型">
@@ -80,6 +87,7 @@
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
placeholder="请输入"
:disabled="isSettlementRuleChange"
/>
<span>至</span>
<el-date-picker
@@ -88,6 +96,7 @@
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
placeholder="请输入"
:disabled="isSettlementRuleChange"
/>
</div>
</el-form-item>
@@ -95,6 +104,7 @@
<el-input
v-model="form.paymentDays"
placeholder="请输入"
:disabled="isSettlementRuleChange"
@input="value => positiveIntegerInput('paymentDays', value)"
>
<template #suffix>天</template>
@@ -105,22 +115,36 @@
:model-value="contractAmountDisplay"
placeholder="请输入"
clearable
:disabled="isSettlementRuleChange"
@input="handleContractAmountInput"
@blur="formatContractAmountField"
@clear="form.contractAmount = null"
/>
</el-form-item>
<el-form-item label="是否范本">
<el-select v-model="form.templateFlag" placeholder="请选择">
<el-select
v-model="form.templateFlag"
placeholder="请选择"
:disabled="isSettlementRuleChange"
>
<el-option label="否" :value="0" />
<el-option label="是" :value="1" />
</el-select>
</el-form-item>
<el-form-item label="原件合同编号">
<el-input v-model="form.originalContractNo" maxlength="100" placeholder="请输入" />
<el-input
v-model="form.originalContractNo"
maxlength="100"
placeholder="请输入"
:disabled="isSettlementRuleChange"
/>
</el-form-item>
<el-form-item label="是否电子章">
<el-select v-model="form.electronicSealFlag" placeholder="请选择">
<el-select
v-model="form.electronicSealFlag"
placeholder="请选择"
:disabled="isSettlementRuleChange"
>
<el-option label="否" :value="0" />
<el-option label="是" :value="1" />
</el-select>
@@ -129,13 +153,19 @@
<el-input
v-model="form.invoiceCycle"
placeholder="请输入"
:disabled="isSettlementRuleChange"
@input="value => positiveIntegerInput('invoiceCycle', value)"
>
<template #suffix>天</template>
</el-input>
</el-form-item>
<el-form-item label="结算币种" prop="settlementCurrency">
<el-select v-model="form.settlementCurrency" placeholder="请选择结算币种" clearable>
<el-select
v-model="form.settlementCurrency"
placeholder="请选择结算币种"
clearable
:disabled="isSettlementRuleChange"
>
<el-option
v-for="item in settlementCurrencyOptions"
:key="item.value"
@@ -145,7 +175,12 @@
</el-select>
</el-form-item>
<el-form-item label="合同格式">
<el-select v-model="form.contractFormat" placeholder="请选择合同格式" clearable>
<el-select
v-model="form.contractFormat"
placeholder="请选择合同格式"
clearable
:disabled="isSettlementRuleChange"
>
<el-option
v-for="item in contractFormatOptions"
:key="item.value"
@@ -158,19 +193,30 @@
<el-input
v-model="form.copyCount"
placeholder="请输入"
:disabled="isSettlementRuleChange"
@input="value => positiveIntegerInput('copyCount', value)"
>
<template #suffix>份</template>
</el-input>
</el-form-item>
<el-form-item label="是否需要加盖法人章">
<el-select v-model="form.legalSealFlag" placeholder="请选择" clearable>
<el-select
v-model="form.legalSealFlag"
placeholder="请选择"
clearable
:disabled="isSettlementRuleChange"
>
<el-option label="否" :value="0" />
<el-option label="是" :value="1" />
</el-select>
</el-form-item>
<el-form-item label="结算方式">
<el-select v-model="form.settlementMode" placeholder="请选择结算方式" clearable>
<el-select
v-model="form.settlementMode"
placeholder="请选择结算方式"
clearable
:disabled="isSettlementRuleChange"
>
<el-option
v-for="item in settlementModeOptions"
:key="item.value"
@@ -191,6 +237,7 @@
maxlength="1000"
show-word-limit
placeholder="请输入合同主要内容"
:disabled="isSettlementRuleChange"
/>
</el-form-item>
<el-form-item label="其他事项说明" class="contract-basic-section__remark">
@@ -201,6 +248,7 @@
maxlength="200"
show-word-limit
placeholder="请输入其他事项说明"
:disabled="isSettlementRuleChange"
/>
</el-form-item>
<!-- 风险客商提示:合同已推送合规并留痕高风险/黑名单时展示,变更提交同样要求特批附件 -->
@@ -214,6 +262,7 @@
title="用印材料"
description
attachment-type
:readonly="isSettlementRuleChange"
:attachment-location-options="changeAttachmentLocationOptions"
:rows="contractFileRows"
:preview="previewAttachment"
@@ -224,12 +273,36 @@
<section class="change-section">
<div class="section-head">
<div class="dialog-section-title">计费信息</div>
<el-button v-if="feeGenerationMode !== 'manual'" type="primary" plain @click="addPlan"
<el-button
v-if="feeGenerationMode !== 'manual'"
type="primary"
plain
@click="addPlan"
>添加</el-button
>
</div>
<el-radio-group v-model="feeGenerationMode"><el-radio label="system">系统生成</el-radio><el-radio label="manual">账单导入生成</el-radio></el-radio-group>
<el-table :data="plans" border class="change-table"><el-table-column type="index" label="序号" width="70" /><el-table-column prop="planName" label="方案名称" min-width="220" /><el-table-column label="默认方案" min-width="120"><template #default="{ row }">{{ row.defaultPlan ? '是' : '否' }}</template></el-table-column><el-table-column prop="remark" label="备注" min-width="260" /><el-table-column label="操作" width="180"><template #default="{ row, $index }"><el-link type="primary" @click="editPlan(row, $index)">编辑</el-link><el-link type="danger" @click="plans.splice($index, 1)">删除</el-link></template></el-table-column></el-table>
<el-radio-group v-model="feeGenerationMode"
><el-radio label="system">系统生成</el-radio
><el-radio label="manual">账单导入生成</el-radio></el-radio-group
>
<el-table :data="plans" border class="change-table"
><el-table-column type="index" label="序号" width="70" /><el-table-column
prop="planName"
label="方案名称"
min-width="220"
/><el-table-column label="默认方案" min-width="120"
><template #default="{ row }">{{
row.defaultPlan ? '是' : '否'
}}</template></el-table-column
><el-table-column prop="remark" label="备注" min-width="260" /><el-table-column
label="操作"
width="180"
><template #default="{ row, $index }"
><el-link type="primary" @click="editPlan(row, $index)">编辑</el-link
><el-link type="danger" @click="plans.splice($index, 1)">删除</el-link></template
></el-table-column
></el-table
>
</section>
<section class="change-section">
@@ -248,19 +321,106 @@
<el-radio :label="0">关闭</el-radio>
</el-radio-group>
</div>
<el-form v-if="settlementRule.autoGenerate === 1" :model="settlementRule" label-position="right" label-width="auto" class="settlement-form">
<el-form-item label="账单起始日" required><el-date-picker v-model="settlementRule.billStartDate" type="date" placeholder="请选择账单起始日" format="YYYY-MM-DD" value-format="YYYY-MM-DD" /></el-form-item>
<el-form-item label="结算类型" required><el-select v-model="settlementRule.settlementType" placeholder="请选择结算类型" @change="handleSettlementTypeChange"><el-option v-for="item in settlementTypeOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item>
<el-form-item v-if="showSettlementBillCycleType" label="结算周期" required><el-select v-model="settlementRule.billCycleType" placeholder="请选择结算周期" @change="handleCycleTypeChange"><el-option v-for="item in billCycleTypeOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item>
<el-form-item v-if="showSettlementBillCutoffDay" label="账单截单日" required><el-select v-model="settlementRule.billCutoffDay" placeholder="请选择账单截单日"><el-option v-for="item in billCutoffDayOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item>
<el-form-item v-if="showSettlementCycleDays" label="周期天数" required><el-select v-model="settlementRule.cycleDays" placeholder="请选择周期天数"><el-option v-for="item in cycleDayOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item>
<el-form
v-if="settlementRule.autoGenerate === 1"
:model="settlementRule"
label-position="right"
label-width="auto"
class="settlement-form"
>
<el-form-item label="账单起始日" required
><el-date-picker
v-model="settlementRule.billStartDate"
type="date"
placeholder="请选择账单起始日"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
/></el-form-item>
<el-form-item label="结算类型" required
><el-select
v-model="settlementRule.settlementType"
placeholder="请选择结算类型"
@change="handleSettlementTypeChange"
><el-option
v-for="item in settlementTypeOptions"
:key="item"
:label="item"
:value="item"
/></el-select></el-form-item>
<el-form-item v-if="showSettlementBillCycleType" label="结算周期" required
><el-select
v-model="settlementRule.billCycleType"
placeholder="请选择结算周期"
@change="handleCycleTypeChange"
><el-option
v-for="item in billCycleTypeOptions"
:key="item"
:label="item"
:value="item"
/></el-select></el-form-item>
<el-form-item v-if="showSettlementBillCutoffDay" label="账单截单日" required
><el-select v-model="settlementRule.billCutoffDay" placeholder="请选择账单截单日"
><el-option
v-for="item in billCutoffDayOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/></el-select></el-form-item>
<el-form-item v-if="showSettlementCycleDays" label="周期天数" required
><el-select v-model="settlementRule.cycleDays" placeholder="请选择周期天数"
><el-option
v-for="item in cycleDayOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/></el-select></el-form-item>
</el-form>
<el-table v-if="settlementRule.autoGenerate === 1 && showSettlementCustomPeriods" :data="settlementRule.customPeriods || []" border class="change-table custom-periods-table">
<el-table
v-if="settlementRule.autoGenerate === 1 && showSettlementCustomPeriods"
:data="settlementRule.customPeriods || []"
border
class="change-table custom-periods-table"
>
<el-table-column type="index" label="序号" width="70" />
<el-table-column label="运单区间-开始日" min-width="180"><template #default="{ row, $index }"><el-select :model-value="row.startDay" placeholder="请选择" clearable @update:model-value="value => handleCustomPeriodStartDayChange($index, value)"><el-option v-for="item in billCutoffDayOptions" :key="`start-${item.value}`" :label="item.label" :value="item.value" /></el-select></template></el-table-column>
<el-table-column label="运单区间-结束日" min-width="200"><template #default="{ row, $index }"><el-select :model-value="row.endDay" placeholder="请选择" clearable @update:model-value="value => handleCustomPeriodEndDayChange($index, value)"><el-option v-for="item in customPeriodEndDayOptions(row)" :key="item.value" :label="item.label" :value="item.value" /></el-select></template></el-table-column>
<el-table-column label="操作" width="100"><template #default="{ $index }"><el-link v-if="$index === 0" type="primary" @click="addCustomPeriodRow">添加</el-link><el-link v-else type="danger" @click="removeCustomPeriodRow($index)">删除</el-link></template></el-table-column>
<template #empty><el-link type="primary" @click="addCustomPeriodRow">添加</el-link></template>
<el-table-column label="运单区间-开始日" min-width="180"
><template #default="{ row, $index }"
><el-select
:model-value="row.startDay"
placeholder="请选择"
clearable
@update:model-value="value => handleCustomPeriodStartDayChange($index, value)"
><el-option
v-for="item in billCutoffDayOptions"
:key="`start-${item.value}`"
:label="item.label"
:value="item.value"
/></el-select></template
></el-table-column>
<el-table-column label="运单区间-结束日" min-width="200"
><template #default="{ row, $index }"
><el-select
:model-value="row.endDay"
placeholder="请选择"
clearable
@update:model-value="value => handleCustomPeriodEndDayChange($index, value)"
><el-option
v-for="item in customPeriodEndDayOptions(row)"
:key="item.value"
:label="item.label"
:value="item.value"
/></el-select></template
></el-table-column>
<el-table-column label="操作" width="100"
><template #default="{ $index }"
><el-link v-if="$index === 0" type="primary" @click="addCustomPeriodRow">添加</el-link
><el-link v-else type="danger" @click="removeCustomPeriodRow($index)"
>删除</el-link
></template
></el-table-column
>
<template #empty
><el-link type="primary" @click="addCustomPeriodRow">添加</el-link></template
>
</el-table>
</section>
@@ -268,17 +428,48 @@
<div class="dialog-section-title">
付款比例设置
<el-tooltip content="非必填;配置付款比例时,合计必须等于100%。" placement="top">
<el-icon style="margin-left: 4px; color: #909399; cursor: pointer"><el-icon-question-filled /></el-icon>
<el-icon style="margin-left: 4px; color: #909399; cursor: pointer"
><el-icon-question-filled
/></el-icon>
</el-tooltip>
</div>
<el-table :data="paymentRatioRows" border class="change-table">
<el-table-column type="index" label="序号" width="70" />
<el-table-column prop="paymentTerm" label="付款笔数" min-width="180" />
<el-table-column label="付款比例上限(%)" min-width="220"><template #default="{ row }"><el-input-number v-model="row.ratioLimit" :min="0" :max="100" :precision="2" controls-position="right" /></template></el-table-column>
<el-table-column label="备注" min-width="220"><template #default="{ row }"><el-input v-model="row.remark" maxlength="200" /></template></el-table-column>
<el-table-column label="操作" width="100"><template #default="{ $index }"><el-link type="danger" @click="paymentRatioRows.splice($index, 1)">删除</el-link></template></el-table-column>
<el-table-column label="付款比例上限(%)" min-width="220"
><template #default="{ row }"
><el-input-number
v-model="row.ratioLimit"
:min="0"
:max="100"
:precision="2"
controls-position="right"
:disabled="isSettlementRuleChange"
/></template
></el-table-column>
<el-table-column label="备注" min-width="220"
><template #default="{ row }"
><el-input
v-model="row.remark"
maxlength="200"
:disabled="isSettlementRuleChange"
/></template
></el-table-column>
<el-table-column v-if="!isSettlementRuleChange" label="操作" width="100"
><template #default="{ $index }"
><el-link type="danger" @click="paymentRatioRows.splice($index, 1)"
>删除</el-link
></template
></el-table-column
>
</el-table>
<el-button type="primary" plain @click="addPaymentRatioRow">添加</el-button>
<el-button
v-if="!isSettlementRuleChange"
type="primary"
plain
@click="addPaymentRatioRow"
>添加</el-button
>
</section>
<billing-plan-editor
@@ -295,6 +486,7 @@
attachment-type
attachment-location="附件(佐证材料等)"
attachment-type-dict-code="contract_supporting_attachment_type"
:readonly="isSettlementRuleChange"
:attachment-location-options="changeAttachmentLocationOptions"
:rows="attachments"
:preview="previewAttachment"
@@ -510,7 +702,13 @@ export default {
.join('、');
return `【${parties || '相对方'}】:被列入外部黑名单;涉贪污受贿裁判相关提及方(3年以上及其他);被执行人信息;买卖合同纠纷;劳动纠纷;重大纠纷。提交变更前须上传「特批附件」类型附件`;
},
isSettlementRuleChange() {
return this.form.changeType === '结算规则变更';
},
changeAttachmentLocationOptions() {
if (this.isSettlementRuleChange) {
return ['变更材料'];
}
return ['用印材料', '附件(佐证材料等)', '变更材料'];
},
contractAmountDisplay() {
@@ -736,6 +934,12 @@ export default {
previewAttachment(row, rows = this.attachments) { const url = this.attachmentUrl(row); if (!url) { this.$message.warning('附件地址为空,无法预览'); return; } if (this.isAttachmentImage(row)) { this.imagePreviewUrls = (rows || []).filter(item => this.isAttachmentImage(item) && this.attachmentUrl(item)).map(item => this.attachmentUrl(item)); this.imagePreviewIndex = Math.max(this.imagePreviewUrls.indexOf(url), 0); this.imagePreviewVisible = true; return; } this.previewFile = { name: this.attachmentName(row), url, mimeType: row.mimeType || row.contentType || '' }; this.documentPreviewVisible = true; },
handleAttachmentUploadToLocation({ location, rows = [] }) {
if (!rows.length) return;
if (this.isSettlementRuleChange) {
if (location === '变更材料') {
this.changeMaterials = [...(this.changeMaterials || []), ...rows];
}
return;
}
if (isContractSealMaterialLocation(location)) {
this.contractFileRows = [...(this.contractFileRows || []), ...rows];
return;
@@ -759,7 +963,81 @@ export default {
removeCustomPeriodRow(index) { if (index <= 0) return; const periods = [...(this.settlementRule.customPeriods || [])]; periods.splice(index, 1); this.settlementRule.customPeriods = normalizeCustomPeriods(periods); },
validateCustomPeriods(periods = [], label) { const rows = normalizeCustomPeriods(periods); if (!rows.length) { this.$message.warning(`${label}:请至少配置一段自定义周期`); return false; } for (let index = 0; index < rows.length; index += 1) { const row = rows[index]; const startDay = Number(row.startDay); const endDay = Number(row.endDay); if (!Number.isFinite(startDay) || startDay < 1 || startDay > 31) { this.$message.warning(`${label}:请选择第${index + 1}行运单区间开始日`); return false; } if (!Number.isFinite(endDay) || endDay < 1 || endDay > 31) { this.$message.warning(`${label}:请选择第${index + 1}行运单区间结束日`); return false; } if (endDay < startDay) { this.$message.warning(`${label}:第${index + 1}行结束日不能早于开始日`); return false; } if (index > 0 && startDay !== Number(rows[index - 1].endDay) + 1) { this.$message.warning(`${label}:自定义多周期区间必须连续,不允许重叠或存在日期缺口`); return false; } } return true; },
validateSettlementRule(rule, label) { if (Number(rule.autoGenerate) !== 1) return true; if (!rule.billStartDate || !rule.settlementType) { this.$message.warning(`${label}:请完整填写账单起始日和结算类型`); return false; } if (rule.settlementType === '月结' && !rule.billCycleType) { this.$message.warning(`${label}:请选择结算周期`); return false; } if (rule.settlementType === '月结' && rule.billCycleType === '固定截单日' && !rule.billCutoffDay) { this.$message.warning(`${label}:请选择账单截单日`); return false; } if (rule.settlementType === '月结' && rule.billCycleType === '自定义多周期') return this.validateCustomPeriods(rule.customPeriods, label); if (rule.settlementType === '固定天数周期结算' && !rule.cycleDays) { this.$message.warning(`${label}:请选择周期天数`); return false; } return true; },
async submit() { await this.$refs.formRef.validate(); const total = this.paymentRatioRows.reduce((sum, row) => sum + Number(row.ratioLimit || 0), 0); if (this.paymentRatioRows.length && Math.abs(total - 100) > 0.0001) { this.$message.warning('付款比例上限合计必须等于100%'); return; } if (this.settlementConfigTab === 'pre') this.preSettlementConfig = { ...this.settlementRule }; else this.formalSettlementConfig = { ...this.settlementRule }; if (!this.validateSettlementRule(this.preSettlementConfig, '预结算配置') || !this.validateSettlementRule(this.formalSettlementConfig, '正式结算配置')) return; const settlementRule = { preSettlementConfig: this.preSettlementConfig, formalSettlementConfig: this.formalSettlementConfig }; await api.submitChange({ ...this.form, settlementCurrency: String(this.form.settlementCurrency || '').trim() || 'RMB', copyCount: normalizeOptionalPositiveInteger(this.form.copyCount), invoiceCycle: normalizeOptionalPositiveInteger(this.form.invoiceCycle), paymentDays: normalizeOptionalPositiveInteger(this.form.paymentDays), contractAmount: normalizeOptionalAmount(this.form.contractAmount), startDate: this.period[0], endDate: this.period[1], feeGenerationMode: this.feeGenerationMode, billingEnabled: this.feeGenerationMode === 'system' ? 1 : 0, billingPlanJson: JSON.stringify(this.plans), settlementRuleJson: JSON.stringify(settlementRule), preSettlementConfigJson: JSON.stringify(this.preSettlementConfig), formalSettlementConfigJson: JSON.stringify(this.formalSettlementConfig), paymentRatioJson: JSON.stringify(this.paymentRatioRows), contractFileJson: JSON.stringify(this.contractFileRows), attachmentsJson: JSON.stringify(this.attachments), changeContent: this.form.changeContent, changeReason: this.form.changeReason, changeAttachmentsJson: JSON.stringify(this.changeMaterials) }); await submitMkApprovalFlow({ bizType: 'contract-manage', bizId: this.form.id, subjectName: this.form.contractName || '', approvalStatus: this.form.approvalStatus || 'change_rejected', mkEnabled: await loadMkEnabled().catch(() => false) }); this.$message.success('变更已提交'); this.$router.back(); },
async submit() {
if (this.form.changeType === '终止合同') {
await api.terminate(this.form.id, this.form.changeReason);
this.$message.success('合同已终止');
this.$router.back();
return;
}
await this.$refs.formRef.validate();
if (!this.isSettlementRuleChange) {
const total = this.paymentRatioRows.reduce((sum, row) => sum + Number(row.ratioLimit || 0), 0);
if (this.paymentRatioRows.length && Math.abs(total - 100) > 0.0001) {
this.$message.warning('付款比例上限合计必须等于100%');
return;
}
}
if (this.settlementConfigTab === 'pre') this.preSettlementConfig = { ...this.settlementRule };
else this.formalSettlementConfig = { ...this.settlementRule };
if (
!this.validateSettlementRule(this.preSettlementConfig, '预结算配置') ||
!this.validateSettlementRule(this.formalSettlementConfig, '正式结算配置')
) {
return;
}
const settlementRule = {
preSettlementConfig: this.preSettlementConfig,
formalSettlementConfig: this.formalSettlementConfig,
};
const payload = this.isSettlementRuleChange
? {
id: this.form.id,
changeType: this.form.changeType,
changeContent: this.form.changeContent,
changeReason: this.form.changeReason,
feeGenerationMode: this.feeGenerationMode,
billingEnabled: this.feeGenerationMode === 'system' ? 1 : 0,
billingPlanJson: JSON.stringify(this.plans),
settlementRuleJson: JSON.stringify(settlementRule),
preSettlementConfigJson: JSON.stringify(this.preSettlementConfig),
formalSettlementConfigJson: JSON.stringify(this.formalSettlementConfig),
changeAttachmentsJson: JSON.stringify(this.changeMaterials),
}
: {
...this.form,
changeType: this.form.changeType || '合同信息变更',
settlementCurrency: String(this.form.settlementCurrency || '').trim() || 'RMB',
copyCount: normalizeOptionalPositiveInteger(this.form.copyCount),
invoiceCycle: normalizeOptionalPositiveInteger(this.form.invoiceCycle),
paymentDays: normalizeOptionalPositiveInteger(this.form.paymentDays),
contractAmount: normalizeOptionalAmount(this.form.contractAmount),
startDate: this.period[0],
endDate: this.period[1],
feeGenerationMode: this.feeGenerationMode,
billingEnabled: this.feeGenerationMode === 'system' ? 1 : 0,
billingPlanJson: JSON.stringify(this.plans),
settlementRuleJson: JSON.stringify(settlementRule),
preSettlementConfigJson: JSON.stringify(this.preSettlementConfig),
formalSettlementConfigJson: JSON.stringify(this.formalSettlementConfig),
paymentRatioJson: JSON.stringify(this.paymentRatioRows),
contractFileJson: JSON.stringify(this.contractFileRows),
attachmentsJson: JSON.stringify(this.attachments),
changeContent: this.form.changeContent,
changeReason: this.form.changeReason,
changeAttachmentsJson: JSON.stringify(this.changeMaterials),
};
await api.submitChange(payload);
await submitMkApprovalFlow({
bizType: 'contract-manage',
bizId: this.form.id,
subjectName: this.form.contractName || '',
approvalStatus: this.form.approvalStatus || 'change_rejected',
mkEnabled: await loadMkEnabled().catch(() => false),
});
this.$message.success('变更已提交');
this.$router.back();
},
},
};
</script>
+350 -38
View File
@@ -635,10 +635,11 @@
min-width="100" /><el-table-column
prop="quantity"
label="数量"
min-width="90" /><el-table-column
prop="quantityUnit"
label="数量单位"
min-width="100" /><el-table-column
min-width="120"
><template #default="{ row }">{{
formatCargoQuantityDisplay(row)
}}</template></el-table-column
><el-table-column
prop="materialCode"
label="物料编码"
min-width="120" /><el-table-column
@@ -897,6 +898,16 @@
>
<div title="配载信息" class="loading-manage-dialog__load-info-card">
<section-card>
<div class="loading-manage-dialog__admission-tip">
<div class="loading-manage-dialog__admission-title">可配载条件</div>
<div class="loading-manage-dialog__admission-text">
同时满足以下条件的运单可被配载:运输方式为「公路」;运单状态为「待执行」或「进行中」;未关联其他有效配载单。
</div>
<div class="loading-manage-dialog__admission-title">配载后状态说明</div>
<div class="loading-manage-dialog__admission-text">
子运单:按项目过程配置是否需司机接单,无需接单则为进行中,需接单则为待执行。配载单:按所选运单所属项目的过程配置,需接单则为待执行,无需接单则为进行中。
</div>
</div>
<div class="loading-manage-dialog__candidate-search">
<el-row :gutter="16">
<el-col :span="6">
@@ -1357,19 +1368,29 @@
</template>
</el-table-column>
<el-table-column label="操作" width="110" fixed="right">
<template #default="{ $index }">
<template #default="{ row, $index }">
<el-link
v-if="!dialogReadonly"
v-if="!dialogReadonly && !isSelectedWaybillRemoveLocked(row)"
type="danger"
@click="removeSelectedWaybill($index)"
>删除</el-link
>
<span
v-else-if="!dialogReadonly && isSelectedWaybillRemoveLocked(row)"
class="loading-manage-dialog__locked-tip"
>已打卡</span
>
</template>
</el-table-column>
</el-table>
</section-card>
<section-card title="线路编辑">
<template v-if="isRunningEdit" #extra>
<span class="loading-manage-dialog__route-hint"
>已打卡地点置灰不可拖拽,新增地址仅可在未打卡节点间调整</span
>
</template>
<div class="loading-manage-dialog__route-list" v-if="routeNodes.length">
<div
v-for="(node, index) in routeNodes"
@@ -1415,8 +1436,9 @@
<el-table-column prop="cargoName" label="货物名称" min-width="140" />
<el-table-column prop="cargoType" label="货物类型" min-width="140" />
<el-table-column prop="packageType" label="包装" min-width="100" />
<el-table-column prop="quantity" label="数量" min-width="100" />
<el-table-column prop="quantityUnit" label="数量单位" min-width="100" />
<el-table-column prop="quantity" label="数量" min-width="120">
<template #default="{ row }">{{ formatCargoQuantityDisplay(row) }}</template>
</el-table-column>
<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" />
@@ -1436,7 +1458,7 @@
<el-segmented
v-model="dialogForm.carrierType"
:options="carrierTypeOptions"
:disabled="dialogReadonly"
:disabled="dialogReadonly || isRunningTaskRequiredLocked"
@change="handleCarrierTypeChange"
/>
</div>
@@ -1450,7 +1472,11 @@
"
clearable
filterable
:disabled="dialogReadonly || !selectedWaybillRows.length"
:disabled="
dialogReadonly ||
isRunningTaskRequiredLocked ||
!selectedWaybillRows.length
"
:loading="taskCarrierLoading"
:placeholder="taskCarrierPlaceholder"
@change="handleTaskCarrierChange"
@@ -1483,6 +1509,7 @@
:fetch-suggestions="fetchDriverSuggestions"
:loading="driverLoading"
placeholder="请输入"
:disabled="dialogReadonly || isRunningSelfDriverLocked"
@select="item => handleDriverSuggestionSelect('dialog', item)"
/>
</el-form-item>
@@ -1490,13 +1517,28 @@
label="手机号"
:required="dialogForm.carrierType === '自运'"
>
<el-input v-model="dialogForm.driverPhone" clearable placeholder="请输入" />
<el-input
v-model="dialogForm.driverPhone"
clearable
placeholder="请输入"
:disabled="dialogReadonly || isRunningSelfDriverLocked"
/>
</el-form-item>
<el-form-item label="车牌号" required>
<el-input v-model="dialogForm.vehicleNo" clearable placeholder="请输入" />
<el-input
v-model="dialogForm.vehicleNo"
clearable
placeholder="请输入"
:disabled="dialogReadonly || isRunningTaskRequiredLocked"
/>
</el-form-item>
<el-form-item v-if="dialogForm.carrierType !== '承运商'" label="挂车车牌号">
<el-input v-model="dialogForm.trailerVehicleNo" clearable placeholder="请输入" />
<el-input
v-model="dialogForm.trailerVehicleNo"
clearable
placeholder="请输入"
:disabled="dialogReadonly"
/>
</el-form-item>
<el-form-item v-if="dialogForm.carrierType !== '承运商'" label="押运人">
<el-autocomplete
@@ -1506,11 +1548,17 @@
:fetch-suggestions="fetchEscortSuggestions"
:loading="escortLoading"
placeholder="请输入"
:disabled="dialogReadonly"
@select="item => handleEscortSuggestionSelect('dialog', item)"
/>
</el-form-item>
<el-form-item v-if="dialogForm.carrierType !== '承运商'" label="押运人手机号">
<el-input v-model="dialogForm.escortPhone" clearable placeholder="请输入" />
<el-input
v-model="dialogForm.escortPhone"
clearable
placeholder="请输入"
:disabled="dialogReadonly"
/>
</el-form-item>
<el-form-item label="里程(km)">
<el-input
@@ -1528,6 +1576,7 @@
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
placeholder="请选择"
:disabled="dialogReadonly"
/>
</el-form-item>
<el-form-item label="预计完成日期">
@@ -1537,6 +1586,7 @@
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
placeholder="请选择"
:disabled="dialogReadonly"
/>
</el-form-item>
<el-form-item label="备注" class="loading-manage-dialog__span-2">
@@ -1547,6 +1597,7 @@
maxlength="500"
show-word-limit
placeholder="请输入,500字以内"
:disabled="dialogReadonly"
/>
</el-form-item>
</div>
@@ -1561,11 +1612,17 @@
</template>
<template v-else>
<el-button @click="closeLoadingDialog">关闭</el-button>
<el-button type="primary" plain :loading="dialogSaving === 'draft'" @click="saveDraft">
<el-button
v-if="!isRunningEdit"
type="primary"
plain
:loading="dialogSaving === 'draft'"
@click="saveDraft"
>
暂存
</el-button>
<el-button type="primary" :loading="dialogSaving === 'submit'" @click="submitLoading">
{{ dialogMode === 'reassign' ? '确认派单' : '确认' }}
{{ dialogMode === 'reassign' ? '确认派单' : isRunningEdit ? '保存' : '确认' }}
</el-button>
</template>
</div>
@@ -2107,6 +2164,8 @@ export default {
routeNodes: [],
cargoRows: [],
routeDragIndex: -1,
/** 进行中编辑:子运单打卡记录映射 waybillId -> records[] */
waybillPunchMap: {},
transportTypeOptions: [],
projectOptions: [],
customerOptions: [],
@@ -2170,6 +2229,23 @@ export default {
dialogReadonly() {
return this.dialogMode === 'view';
},
/** 进行中配载编辑:可加子单/调未打卡路线,但任务必填字段锁定 */
isRunningEdit() {
return (
this.dialogMode === 'edit' &&
String(this.dialogForm.businessStatus || '') === 'running'
);
},
/** 进行中任务必填字段(已派车)不可改 */
isRunningTaskRequiredLocked() {
return this.isRunningEdit;
},
/** 自运进行中:司机/手机号同属必填派车信息,不可改 */
isRunningSelfDriverLocked() {
return (
this.isRunningEdit && String(this.dialogForm.carrierType || '').trim() === '自运'
);
},
// 打卡时间轴:已打卡按时间升序,未打卡保持原节点顺序排在其后
orderedPunchRecords() {
const records = Array.isArray(this.punchRecords) ? this.punchRecords : [];
@@ -2913,6 +2989,7 @@ export default {
if (status === 'running') {
return [
{ type: 'view', label: '详情' },
{ type: 'edit', label: '编辑' },
{ type: 'complete', label: '完成' },
];
}
@@ -3014,7 +3091,12 @@ export default {
query: {
mode,
...(mode === 'edit' && row?.id ? { id: row.id } : {}),
name: mode === 'edit' ? '编辑配载管理' : '新增配载管理',
name:
mode === 'edit'
? row?.businessStatus === 'running'
? '编辑进行中配载'
: '编辑配载管理'
: '新增配载管理',
},
});
return;
@@ -3037,6 +3119,9 @@ export default {
this.dialogForm.loadingSubNos
);
this.syncProcessWaybillOptions();
if (this.isRunningEdit) {
await this.refreshWaybillPunchMap();
}
this.restoreRouteAndCargo();
this.restoreRouteChangeRecords();
this.loadPunchRecords();
@@ -3071,6 +3156,8 @@ export default {
this.taskCarrierLoading = false;
this.routeNodes = [];
this.cargoRows = [];
this.routeDragIndex = -1;
this.waybillPunchMap = {};
this.candidatePage.currentPage = 1;
this.candidatePage.total = 0;
this.candidateSearchVisible = false;
@@ -3435,20 +3522,144 @@ export default {
const statusName = String(row.businessStatusName || row.statusName || '');
return statusName.includes('已完成');
},
isPunchRecordDone(record = {}) {
if (record.punched === false || record.statusName === '未打卡') return false;
return Boolean(record.punched || record.punchTime || record.punchAt || record.punchDateTime);
},
isLoadPunchRecord(record = {}) {
if (!this.isPunchRecordDone(record)) return false;
const code = String(record.nodeCode || '')
.trim()
.toLowerCase();
const name = String(record.nodeName || '').trim();
return code === 'load' || name.includes('装货');
},
isUnloadPunchRecord(record = {}) {
if (!this.isPunchRecordDone(record)) return false;
const code = String(record.nodeCode || '')
.trim()
.toLowerCase();
const name = String(record.nodeName || '').trim();
return (
code === 'unload' ||
name.includes('卸货') ||
code === 'arrival' ||
name.includes('到货') ||
code === 'sign' ||
name.includes('签收')
);
},
waybillPunchRecords(row = {}) {
const key = String(row?.id || '');
if (!key) return [];
const records = this.waybillPunchMap[key];
return Array.isArray(records) ? records : [];
},
isWaybillLoadPunched(row = {}) {
return this.waybillPunchRecords(row).some(record => this.isLoadPunchRecord(record));
},
isWaybillUnloadPunched(row = {}) {
return this.waybillPunchRecords(row).some(record => this.isUnloadPunchRecord(record));
},
isWaybillPunchLocked(row = {}) {
return this.isWaybillLoadPunched(row) || this.isWaybillUnloadPunched(row);
},
isSelectedWaybillRemoveLocked(row = {}) {
if (this.isWaybillCompleted(row)) return true;
return this.isRunningEdit && this.isWaybillPunchLocked(row);
},
isRouteNodeTagLocked(tag, rows = this.selectedWaybillRows) {
const match = String(tag || '').match(/^(装|卸)(\d+)$/);
if (!match) return false;
const row = rows[Number(match[2]) - 1];
if (!row) return false;
if (this.isRunningEdit) {
return match[1] === '装' ? this.isWaybillLoadPunched(row) : this.isWaybillUnloadPunched(row);
}
return this.isWaybillCompleted(row);
},
isRouteChangeNodeLocked(node = {}) {
return Boolean(node?.locked);
},
syncRouteNodeLockedFlags(nodes = [], rows = this.selectedWaybillRows) {
return (nodes || []).map(node => {
const locked = (node.tags || []).some(tag => {
const match = String(tag).match(/^(装|卸)(\d+)$/);
if (!match) return false;
const row = rows[Number(match[2]) - 1];
return row ? this.isWaybillCompleted(row) : false;
});
const locked = (node.tags || []).some(tag => this.isRouteNodeTagLocked(tag, rows));
return { ...node, locked };
});
},
async refreshWaybillPunchMap() {
const rows = (this.selectedWaybillRows || []).filter(row => row?.id);
if (!rows.length) {
this.waybillPunchMap = {};
return;
}
const entries = await Promise.all(
rows.map(async row => {
try {
const res = await getWaybillPunchRecords(row.id);
const data = res?.data?.data || res?.data || {};
return [String(row.id), Array.isArray(data.records) ? data.records : []];
} catch (error) {
return [String(row.id), []];
}
})
);
this.waybillPunchMap = Object.fromEntries(entries);
},
/**
* 进行中编辑:已打卡地点保持在前且不可插入,新增地址只能排在已打卡节点之后。
*/
mergeRouteNodesKeepingPunched(prevNodes = [], builtNodes = []) {
const prev = Array.isArray(prevNodes) ? prevNodes : [];
const built = Array.isArray(builtNodes) ? builtNodes : [];
if (!this.isRunningEdit) {
return built;
}
const builtByAddress = new Map();
built.forEach(node => {
if (node?.address && !builtByAddress.has(node.address)) {
builtByAddress.set(node.address, node);
}
});
const lockedAddresses = new Set(
built.filter(node => node?.locked && node.address).map(node => node.address)
);
const used = new Set();
const lockedResult = [];
prev.forEach(node => {
if (!node?.address || !lockedAddresses.has(node.address) || used.has(node.address)) {
return;
}
const latest = builtByAddress.get(node.address);
if (!latest) return;
lockedResult.push({ ...latest, locked: true });
used.add(node.address);
});
built.forEach(node => {
if (!node?.locked || !node.address || used.has(node.address)) return;
lockedResult.push({ ...node, locked: true });
used.add(node.address);
});
const unlockedResult = [];
prev.forEach(node => {
if (!node?.address || lockedAddresses.has(node.address) || used.has(node.address)) {
return;
}
const latest = builtByAddress.get(node.address);
if (!latest) return;
unlockedResult.push({ ...latest, locked: false });
used.add(node.address);
});
built.forEach(node => {
if (node?.locked || !node?.address || used.has(node.address)) return;
unlockedResult.push({ ...node, locked: false });
used.add(node.address);
});
return [...lockedResult, ...unlockedResult].map((node, index) => ({
...node,
key: `${node.address}-${index}`,
}));
},
openRouteChangeMapDialog(row, field) {
if (this.isWaybillCompleted(row)) return;
this.routeChangeMapTarget = { row, field };
@@ -3701,12 +3912,10 @@ export default {
try {
const query = {
...this.buildQueryParams(this.candidateQuery),
// 待执行(需接单未接)与进行中(无需接单或已接单)均可配载
businessStatus: 'pending,running',
onlyUnassignedLoading: 1,
// 与后端 LoadableAdmissionSupport 一致:公路 + 待执行/进行中 + 未关联有效配载单
loadableOnly: 1,
// 仅查当前账户所属组织及子组织下的运单
allDept: 0,
...(this.dialogMode === 'add' ? { transportType: 'road' } : {}),
};
const transportType = this.getCandidateTransportTypeQuery(query.transportType);
const res = await getWaybillList(
@@ -3766,6 +3975,33 @@ export default {
}
})
);
if (this.isRunningEdit && String(this.dialogForm.carrierType || '').trim() === '承运商') {
const contractId = this.dialogForm.carrierContractId;
if (contractId) {
const projectIds = [
...new Set(
[...this.selectedWaybillRows, ...selectedRows]
.map(row => row.projectId)
.filter(Boolean)
),
];
if (projectIds.length) {
try {
const response = await loadingApi.getCarrierContracts(projectIds);
const contracts = response?.data?.data || response?.data || [];
const stillAllowed = (Array.isArray(contracts) ? contracts : []).some(
item => String(item.id) === String(contractId)
);
if (!stillAllowed) {
ElMessage.warning('新增子运单须兼容当前配载单已选定的承运商合同');
return;
}
} catch (error) {
// 合同校验失败时仍允许添加,最终由后端确认拦截
}
}
}
}
const exists = new Set(
this.selectedWaybillRows.map(row => String(row.id || row.waybillNo))
);
@@ -3777,8 +4013,12 @@ export default {
exists.add(key);
}
});
const prevRouteNodes = [...this.routeNodes];
this.selectedWaybillRows = nextRows;
this.rebuildSummaryFromWaybills();
if (this.isRunningEdit) {
await this.refreshWaybillPunchMap();
}
this.rebuildSummaryFromWaybills(true, prevRouteNodes);
await this.loadTaskCarrierOptions();
this.$refs.candidateTableRef?.clearSelection?.();
this.candidateSelection = [];
@@ -3790,11 +4030,23 @@ export default {
await this.addWaybillRows(this.candidateSelection);
},
removeSelectedWaybill(index) {
const row = this.selectedWaybillRows[index];
if (this.isSelectedWaybillRemoveLocked(row)) {
ElMessage.warning('已打卡的子运单不可移除');
return;
}
this.selectedWaybillRows.splice(index, 1);
if (this.isRunningEdit) {
this.refreshWaybillPunchMap().then(() => {
this.rebuildSummaryFromWaybills();
this.loadTaskCarrierOptions();
});
return;
}
this.rebuildSummaryFromWaybills();
this.loadTaskCarrierOptions();
},
rebuildSummaryFromWaybills(rebuildRoute = true) {
rebuildSummaryFromWaybills(rebuildRoute = true, prevRouteNodes = null) {
const rows = this.selectedWaybillRows;
this.dialogForm.loadingSubNos = rows
.map(row => row.waybillNo)
@@ -3819,7 +4071,13 @@ export default {
this.dialogForm.transportType = rows.find(row => row.transportType)?.transportType || '';
}
if (rebuildRoute) {
this.routeNodes = this.buildRouteNodes(rows);
const builtNodes = this.buildRouteNodes(rows);
this.routeNodes = this.isRunningEdit
? this.mergeRouteNodesKeepingPunched(
prevRouteNodes || this.routeNodes,
builtNodes
)
: builtNodes;
}
this.cargoRows = this.buildCargoRows(rows);
// 编辑回填阶段 routeNodes 尚未恢复时跳过,避免清空已有发到货地
@@ -3839,7 +4097,9 @@ export default {
if (locked) node.locked = true;
};
rows.forEach((row, rowIndex) => {
const locked = this.isWaybillCompleted(row);
const locked = this.isRunningEdit
? this.isWaybillLoadPunched(row)
: this.isWaybillCompleted(row);
const departure = row.departureAddress || row.departureName;
appendNode(
departure,
@@ -3849,7 +4109,9 @@ export default {
);
});
rows.forEach((row, rowIndex) => {
const locked = this.isWaybillCompleted(row);
const locked = this.isRunningEdit
? this.isWaybillUnloadPunched(row)
: this.isWaybillCompleted(row);
const arrival = row.arrivalAddress || row.arrivalName;
appendNode(
arrival,
@@ -3901,7 +4163,9 @@ export default {
if (
originalLockedKeys.some((key, lockedIndex) => key && key !== nextLockedKeys[lockedIndex])
) {
ElMessage.warning('已完成运单对应的路线不可调整');
ElMessage.warning(
this.isRunningEdit ? '已打卡地点不可调整,新地址只能在未打卡节点间拖拽' : '已完成运单对应的路线不可调整'
);
this.routeDragIndex = -1;
return;
}
@@ -3941,14 +4205,28 @@ export default {
return [this.normalizeCargoRow(row, row)];
});
},
formatCargoQuantityDisplay(row = {}) {
const quantity = blankCargoSentinel(row.quantity ?? row.cargoQuantity ?? '');
const unit = String(
blankCargoSentinel(
row.quantityUnit ||
row.goodsQuantityUnit ||
row.cargoUnit ||
row.unit ||
''
) || ''
).trim();
if (quantity === '' || quantity === undefined || quantity === null) {
return unit || '';
}
return unit ? `${quantity} ${unit}` : String(quantity);
},
normalizeCargoRow(waybill, item = {}) {
return sanitizeCargoRow({
projectName: item.projectName || waybill.projectName || '',
cargoName: item.cargoName || item.goodsName || waybill.cargoName || '',
cargoType: item.cargoType || item.goodsType || waybill.cargoType || '',
packageType: item.packageType || item.package || '',
weight: item.weight || item.cargoWeight || '',
volume: item.volume || item.cargoVolume || '',
quantity: item.quantity || item.cargoQuantity || waybill.quantity || '',
quantityUnit:
item.quantityUnit ||
@@ -4145,15 +4423,15 @@ export default {
try {
const payload = {
...this.normalizePayload(),
// 确认生成正式配载单(待执行),覆盖草稿状态
businessStatus: 'pending',
// 进行中编辑保持 running;新建/草稿确认由后端按项目是否需接单写入 pending/running
...(this.isRunningEdit ? { businessStatus: 'running' } : {}),
};
if (this.dialogMode === 'reassign') {
await loadingApi.reassign(payload);
ElMessage.success('派单成功');
} else {
await loadingApi.submit(payload);
ElMessage.success('确认成功');
ElMessage.success(this.isRunningEdit ? '保存成功' : '确认成功');
}
sessionStorage.setItem(loadingManageListRefreshKey, '1');
this.closeLoadingDialog();
@@ -4715,6 +4993,30 @@ export default {
justify-content: space-between;
}
.loading-manage-dialog__admission-tip {
margin-bottom: 12px;
padding: 12px 14px;
background: #f5f9ff;
border: 1px solid #d9ecff;
border-radius: 4px;
line-height: 1.6;
color: #606266;
}
.loading-manage-dialog__admission-title {
margin-bottom: 4px;
color: #303133;
font-weight: 600;
}
.loading-manage-dialog__admission-title + .loading-manage-dialog__admission-text + .loading-manage-dialog__admission-title {
margin-top: 8px;
}
.loading-manage-dialog__admission-text {
font-size: 13px;
}
.loading-manage-dialog__candidate-search {
padding: 0;
margin-bottom: 0;
@@ -4787,6 +5089,16 @@ export default {
color: #909399;
}
.loading-manage-dialog__route-hint {
color: #909399;
font-size: 12px;
}
.loading-manage-dialog__locked-tip {
color: #909399;
font-size: 12px;
}
.loading-manage-dialog__route-type {
width: 28px;
height: 28px;
+91 -16
View File
@@ -955,7 +955,7 @@ import {
getList as getCustomerArchiveList,
getDetail as getCustomerArchiveDetail,
} from '@/api/vehicle/customer-archive';
import { getDeptTree, getPlatformCompanySelect } from '@/api/system/dept';
import { getBusinessDeptSelect, getDeptTree, getPlatformCompanySelect } from '@/api/system/dept';
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
import { getDictionary as getBizDictionary } from '@/api/system/dictbiz';
import { getList as getUserList } from '@/api/system/user';
@@ -1596,6 +1596,7 @@ export default {
projectType: type === 'majorSupplement' ? '重大项目' : '普通项目',
});
this.fillDefaultUsers();
this.fillDefaultBusinessDept();
return;
}
if (!id) return;
@@ -2032,6 +2033,7 @@ export default {
.filter(Boolean);
this.syncSelectedCustomerOptions();
this.ensureCurrentPlatformOption();
this.ensureCurrentBusinessDeptOption();
},
fillDefaultUsers() {
const userId = this.userInfo?.userId || this.userInfo?.id || '';
@@ -2050,6 +2052,25 @@ export default {
this.form.principalUserName = userName;
}
},
/**
* 新增/重大补录:业务部门默认取当前用户所属部门中第一个「是否业务部门=是」的组织。
*/
fillDefaultBusinessDept() {
if (!['add', 'majorSupplement'].includes(this.dialogType)) return;
if (this.form.businessDeptId) return;
const raw = this.userInfo?.deptId || this.userInfo?.dept_id || '';
const userDeptIds = String(raw)
.split(/[,,]/)
.map(item => String(item || '').trim())
.filter(item => item && item !== '-1');
if (!userDeptIds.length || !this.deptOptions.length) return;
const matched = userDeptIds.find(id =>
this.deptOptions.some(item => String(item.value) === String(id))
);
if (!matched) return;
this.form.businessDeptId = matched;
this.handleBusinessDeptChange(matched);
},
resolveUserRealName(row = {}, role) {
return (
row[`${role}RealName`] ||
@@ -2632,17 +2653,61 @@ export default {
this.$refs.projectForm?.validateField('projectType');
},
loadDeptOptions() {
getDeptTree().then(res => {
const deptTree = res.data.data || [];
// 新增/编辑/重大补录/项目变更均可选择业务部门,排除外部组织
const businessDeptTree = ['add', 'edit', 'majorSupplement', 'change'].includes(this.dialogType)
? this.excludeExternalOrganization(deptTree)
: deptTree;
this.deptOptions = this.flattenDept(businessDeptTree);
this.businessDeptTreeOptions = this.toDeptCascaderOptions(businessDeptTree);
});
Promise.all([getDeptTree(), getBusinessDeptSelect()])
.then(([treeRes, bizRes]) => {
const deptTree = this.excludeExternalOrganization(treeRes.data.data || []);
const bizList = bizRes.data.data || [];
const bizIds = new Set(
bizList
.map(item => (item.id == null || item.id === '' ? '' : String(item.id)))
.filter(Boolean)
);
// 仅保留「是否业务部门=是」的节点及其祖先,祖先不可选
const businessDeptTree = this.filterBusinessDeptTree(deptTree, bizIds);
this.deptOptions = this.flattenDept(businessDeptTree).filter(item =>
bizIds.has(String(item.value))
);
this.businessDeptTreeOptions = this.toDeptCascaderOptions(businessDeptTree, bizIds);
this.fillDefaultBusinessDept();
this.ensureCurrentBusinessDeptOption();
})
.catch(() => {
this.deptOptions = [];
this.businessDeptTreeOptions = [];
});
this.loadPlatformCompanyOptions();
},
/**
* 剪枝组织树:只保留业务部门节点及其祖先路径。
*/
filterBusinessDeptTree(list = [], bizIds = new Set()) {
return (list || []).reduce((result, item) => {
const children = item.children?.length
? this.filterBusinessDeptTree(item.children, bizIds)
: [];
const isBiz = bizIds.has(String(item.id));
if (!isBiz && !children.length) {
return result;
}
result.push({
...item,
children: children.length ? children : undefined,
});
return result;
}, []);
},
ensureCurrentBusinessDeptOption() {
const id = this.form.businessDeptId;
if (!id) return;
const exists = this.deptOptions.some(item => String(item.value) === String(id));
if (exists) return;
const label = this.form.businessDeptName || String(id);
this.deptOptions.unshift({ label, rawLabel: label, value: id });
const pathExists = this.findDeptPath(id, this.businessDeptTreeOptions);
if (!pathExists) {
this.businessDeptTreeOptions.unshift({ label, value: id });
}
},
loadPlatformCompanyOptions() {
getPlatformCompanySelect().then(res => {
const list = res.data.data || [];
@@ -2686,12 +2751,22 @@ export default {
return result;
}, []);
},
toDeptCascaderOptions(list = []) {
return list.map(item => ({
label: item.title || item.deptName || item.name,
value: item.id,
children: item.children?.length ? this.toDeptCascaderOptions(item.children) : undefined,
}));
toDeptCascaderOptions(list = [], bizIds = null) {
return list.map(item => {
const value = item.id == null || item.id === '' ? item.value : item.id;
const option = {
label: item.title || item.deptName || item.name || item.label,
value: value == null || value === '' ? '' : String(value),
children: item.children?.length
? this.toDeptCascaderOptions(item.children, bizIds)
: undefined,
};
// 仅业务部门可选;祖先节点保留路径但禁用
if (bizIds && !bizIds.has(String(option.value))) {
option.disabled = true;
}
return option;
});
},
flattenDept(list = [], level = 0) {
return list.reduce((result, item) => {
+14 -4
View File
@@ -203,7 +203,11 @@
@click="openBatchDialog(row)"
>更换运单批次</el-link
><el-link
v-if="(row.processStatus !== '处理完成' || row.auditStatus === '待审核') && row.auditStatus !== '审核驳回'"
v-if="
row.processStatus !== '处理失败' &&
(row.processStatus !== '处理完成' || row.auditStatus === '待审核') &&
row.auditStatus !== '审核驳回'
"
type="primary"
@click="openBatchDialog(row)"
>更换运单批次</el-link
@@ -320,7 +324,7 @@
<el-form-item label="执行凭证" required
><el-upload
action="#"
accept=".zip,.7z"
accept=".zip"
:auto-upload="false"
:limit="1"
:on-change="selectVoucherFile"
@@ -332,7 +336,7 @@
>上传</el-button
><template #tip
><span class="voucher-manage-page__upload-tip"
>请上传车辆的执行凭证压缩包,支持zip、7z格式。 上传附件大于50M时为超大附件,确认提交后可在“批量补录->上传任务”中查看上传进度。</span
>请上传车辆的执行凭证压缩包,仅支持 zip 格式;无法解析的文件将标记为「处理失败」,可重新上传。上传附件大于50M时为超大附件,确认提交后可在“批量补录->上传任务”中查看上传进度。</span
></template
></el-upload
><el-progress
@@ -549,7 +553,7 @@
ref="resumeFileInput"
class="voucher-manage-page__hidden-file"
type="file"
accept=".zip,.7z"
accept=".zip"
@change="resumeFileSelected"
/>
</template>
@@ -998,6 +1002,12 @@ const uploadVoucher = async file => {
};
const selectVoucherFile = file => {
if (!file?.raw) return;
const fileName = String(file.raw.name || '').toLowerCase();
if (!fileName.endsWith('.zip')) {
ElMessage.warning('仅支持 zip 压缩包,无法解析的文件将标记为处理失败');
clearFile();
return;
}
selectedVoucherFile.value = file.raw;
editing.fileName = file.raw.name;
editing.fileUrl = '';