✨ 进行中配载可编辑、汇票选择与收款方展示优化,并支持业务部门与待确认回单

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-22 15:41:01 +08:00
parent d7969c191a
commit 8d9fe998c0
11 changed files with 783 additions and 98 deletions
+7
View File
@@ -14,6 +14,13 @@ export const cancel = api.cancel;
export const reassign = api.reassign; export const reassign = api.reassign;
export const complete = api.complete; export const complete = api.complete;
export const batchComplete = api.batchComplete; export const batchComplete = api.batchComplete;
/** 确认回单:待确认回单运单完成后转为已完成 */
export const confirmReturn = id =>
request({
url: `${baseUrl}/confirm-return`,
method: 'post',
params: { id },
});
export const changeRoute = data => export const changeRoute = data =>
request({ request({
url: `${baseUrl}/change-route`, url: `${baseUrl}/change-route`,
+7
View File
@@ -139,3 +139,10 @@ export const getPlatformCompanySelect = () => {
method: 'get', method: 'get',
}); });
}; };
export const getBusinessDeptSelect = () => {
return request({
url: '/blade-system/dept/business-dept-select',
method: 'get',
});
};
+1
View File
@@ -42,6 +42,7 @@ export const waybillStatusOptions = [
{ label: '草稿', value: 'draft' }, { label: '草稿', value: 'draft' },
{ label: '待执行', value: 'pending' }, { label: '待执行', value: 'pending' },
{ label: '进行中', value: 'running' }, { label: '进行中', value: 'running' },
{ label: '待确认回单', value: 'pending_return' },
{ label: '已完成', value: 'completed' }, { label: '已完成', value: 'completed' },
{ label: '已取消', value: 'cancelled' }, { label: '已取消', value: 'cancelled' },
]; ];
+17 -3
View File
@@ -478,7 +478,15 @@ export const config = {
formatStatus(row, prop, defaultText) { formatStatus(row, prop, defaultText) {
if (prop !== 'businessStatus') return defaultText; if (prop !== 'businessStatus') return defaultText;
const status = String(row.businessStatus || ''); const status = String(row.businessStatus || '');
const terminalStatuses = ['draft', 'completed', 'cancelled', 'waiting_dispatch', 'dispatching']; const terminalStatuses = [
'draft',
'completed',
'cancelled',
'waiting_dispatch',
'dispatching',
'pending_return',
];
if (status === 'pending_return') return '待确认回单';
const needAccept = const needAccept =
row.requireAccept === true || row.requireAccept === true ||
row.requireAccept === 1 || row.requireAccept === 1 ||
@@ -497,14 +505,20 @@ export const config = {
getDriverRejectReason, getDriverRejectReason,
isDriverRejectedWaybill, isDriverRejectedWaybill,
canEdit(row) { canEdit(row) {
// 进行中的运单不允许编辑 // 进行中、待确认回单不允许编辑
if (isInProgressStatus(row, row.businessStatusName)) { if (isInProgressStatus(row, row.businessStatusName)) {
return false; return false;
} }
if (String(row.businessStatus || '') === 'pending_return') {
return false;
}
return !isDriverRejectedWaybill(row); return !isDriverRejectedWaybill(row);
}, },
canCancel(row) { canCancel(row) {
// 进行中的运单不允许取消 // 进行中、待确认回单不允许取消
if (String(row.businessStatus || '') === 'pending_return') {
return false;
}
return !isInProgressStatus(row, row.businessStatusName); return !isInProgressStatus(row, row.businessStatusName);
}, },
deleteStatus: ['draft'], deleteStatus: ['draft'],
@@ -1591,8 +1591,8 @@
<el-link type="primary" v-if="canCancel(row)" @click="handleAction('cancel', row)"> <el-link type="primary" v-if="canCancel(row)" @click="handleAction('cancel', row)">
取消 取消
</el-link> </el-link>
<el-link type="primary" v-if="canComplete(row)" @click="handleAction('complete', row)"> <el-link type="primary" v-if="canComplete(row)" @click="handleCompleteAction(row)">
{{ completeActionLabel }} {{ completeActionLabelFor(row) }}
</el-link> </el-link>
<el-link type="primary" v-if="canMaintainMileage(row)" @click="openMileageDialog(row)"> <el-link type="primary" v-if="canMaintainMileage(row)" @click="openMileageDialog(row)">
维护里程 维护里程
@@ -5111,7 +5111,29 @@ export default {
return false; return false;
} }
const status = this.statusValue(row); 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) { canMaintainMileage(row) {
return ( return (
@@ -6543,6 +6565,9 @@ export default {
if (status === 'completed') { if (status === 'completed') {
return 'primary'; return 'primary';
} }
if (status === 'pending_return') {
return 'warning';
}
return 'warning'; return 'warning';
}, },
normalizeRow(row, saveAsDraft = false) { normalizeRow(row, saveAsDraft = false) {
@@ -10356,7 +10381,14 @@ export default {
this.$message.warning('请选择至少一条数据'); this.$message.warning('请选择至少一条数据');
return; return;
} }
this.$confirm('确定完成选中的运单?', '提示', { const pendingReturnCount = this.selectionList.filter(
row => this.statusValue(row) === 'pending_return'
).length;
const tip =
pendingReturnCount > 0
? `确定完成选中的运单?其中 ${pendingReturnCount} 条为待确认回单,确认后将转为已完成。`
: '确定完成选中的运单?';
this.$confirm(tip, '提示', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning', type: 'warning',
+306 -28
View File
@@ -8,11 +8,17 @@
<el-form-item label="变更类型" class="contract-basic-section__change-type"> <el-form-item label="变更类型" class="contract-basic-section__change-type">
<el-radio-group v-model="form.changeType"> <el-radio-group v-model="form.changeType">
<el-radio label="合同信息变更" /> <el-radio label="合同信息变更" />
<el-radio label="结算规则变更" />
<el-radio label="终止合同" /> <el-radio label="终止合同" />
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="签约类型" prop="signType"> <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 <el-option
v-for="item in signTypeOptions" v-for="item in signTypeOptions"
:key="item.value" :key="item.value"
@@ -30,6 +36,7 @@
maxlength="100" maxlength="100"
show-word-limit show-word-limit
placeholder="请输入合同名称" placeholder="请输入合同名称"
:disabled="isSettlementRuleChange"
/> />
</el-form-item> </el-form-item>
<el-form-item label="合同类型"> <el-form-item label="合同类型">
@@ -80,6 +87,7 @@
format="YYYY-MM-DD" format="YYYY-MM-DD"
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD"
placeholder="请输入" placeholder="请输入"
:disabled="isSettlementRuleChange"
/> />
<span>至</span> <span>至</span>
<el-date-picker <el-date-picker
@@ -88,6 +96,7 @@
format="YYYY-MM-DD" format="YYYY-MM-DD"
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD"
placeholder="请输入" placeholder="请输入"
:disabled="isSettlementRuleChange"
/> />
</div> </div>
</el-form-item> </el-form-item>
@@ -95,6 +104,7 @@
<el-input <el-input
v-model="form.paymentDays" v-model="form.paymentDays"
placeholder="请输入" placeholder="请输入"
:disabled="isSettlementRuleChange"
@input="value => positiveIntegerInput('paymentDays', value)" @input="value => positiveIntegerInput('paymentDays', value)"
> >
<template #suffix>天</template> <template #suffix>天</template>
@@ -105,22 +115,36 @@
:model-value="contractAmountDisplay" :model-value="contractAmountDisplay"
placeholder="请输入" placeholder="请输入"
clearable clearable
:disabled="isSettlementRuleChange"
@input="handleContractAmountInput" @input="handleContractAmountInput"
@blur="formatContractAmountField" @blur="formatContractAmountField"
@clear="form.contractAmount = null" @clear="form.contractAmount = null"
/> />
</el-form-item> </el-form-item>
<el-form-item label="是否范本"> <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="0" />
<el-option label="是" :value="1" /> <el-option label="是" :value="1" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="原件合同编号"> <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>
<el-form-item label="是否电子章"> <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="0" />
<el-option label="是" :value="1" /> <el-option label="是" :value="1" />
</el-select> </el-select>
@@ -129,13 +153,19 @@
<el-input <el-input
v-model="form.invoiceCycle" v-model="form.invoiceCycle"
placeholder="请输入" placeholder="请输入"
:disabled="isSettlementRuleChange"
@input="value => positiveIntegerInput('invoiceCycle', value)" @input="value => positiveIntegerInput('invoiceCycle', value)"
> >
<template #suffix>天</template> <template #suffix>天</template>
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item label="结算币种" prop="settlementCurrency"> <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 <el-option
v-for="item in settlementCurrencyOptions" v-for="item in settlementCurrencyOptions"
:key="item.value" :key="item.value"
@@ -145,7 +175,12 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="合同格式"> <el-form-item label="合同格式">
<el-select v-model="form.contractFormat" placeholder="请选择合同格式" clearable> <el-select
v-model="form.contractFormat"
placeholder="请选择合同格式"
clearable
:disabled="isSettlementRuleChange"
>
<el-option <el-option
v-for="item in contractFormatOptions" v-for="item in contractFormatOptions"
:key="item.value" :key="item.value"
@@ -158,19 +193,30 @@
<el-input <el-input
v-model="form.copyCount" v-model="form.copyCount"
placeholder="请输入" placeholder="请输入"
:disabled="isSettlementRuleChange"
@input="value => positiveIntegerInput('copyCount', value)" @input="value => positiveIntegerInput('copyCount', value)"
> >
<template #suffix>份</template> <template #suffix>份</template>
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item label="是否需要加盖法人章"> <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="0" />
<el-option label="是" :value="1" /> <el-option label="是" :value="1" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="结算方式"> <el-form-item label="结算方式">
<el-select v-model="form.settlementMode" placeholder="请选择结算方式" clearable> <el-select
v-model="form.settlementMode"
placeholder="请选择结算方式"
clearable
:disabled="isSettlementRuleChange"
>
<el-option <el-option
v-for="item in settlementModeOptions" v-for="item in settlementModeOptions"
:key="item.value" :key="item.value"
@@ -191,6 +237,7 @@
maxlength="1000" maxlength="1000"
show-word-limit show-word-limit
placeholder="请输入合同主要内容" placeholder="请输入合同主要内容"
:disabled="isSettlementRuleChange"
/> />
</el-form-item> </el-form-item>
<el-form-item label="其他事项说明" class="contract-basic-section__remark"> <el-form-item label="其他事项说明" class="contract-basic-section__remark">
@@ -201,6 +248,7 @@
maxlength="200" maxlength="200"
show-word-limit show-word-limit
placeholder="请输入其他事项说明" placeholder="请输入其他事项说明"
:disabled="isSettlementRuleChange"
/> />
</el-form-item> </el-form-item>
</section> </section>
@@ -209,6 +257,7 @@
title="用印材料" title="用印材料"
description description
attachment-type attachment-type
:readonly="isSettlementRuleChange"
:attachment-location-options="changeAttachmentLocationOptions" :attachment-location-options="changeAttachmentLocationOptions"
:rows="contractFileRows" :rows="contractFileRows"
:preview="previewAttachment" :preview="previewAttachment"
@@ -219,12 +268,36 @@
<section class="change-section"> <section class="change-section">
<div class="section-head"> <div class="section-head">
<div class="dialog-section-title">计费信息</div> <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 >添加</el-button
> >
</div> </div>
<el-radio-group v-model="feeGenerationMode"><el-radio label="system">系统生成</el-radio><el-radio label="manual">账单导入生成</el-radio></el-radio-group> <el-radio-group v-model="feeGenerationMode"
<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 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>
<section class="change-section"> <section class="change-section">
@@ -243,19 +316,106 @@
<el-radio :label="0">关闭</el-radio> <el-radio :label="0">关闭</el-radio>
</el-radio-group> </el-radio-group>
</div> </div>
<el-form v-if="settlementRule.autoGenerate === 1" :model="settlementRule" label-position="right" label-width="auto" class="settlement-form"> <el-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> v-if="settlementRule.autoGenerate === 1"
<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> :model="settlementRule"
<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> label-position="right"
<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> label-width="auto"
<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> 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-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 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="180"
<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> ><template #default="{ row, $index }"
<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> ><el-select
<template #empty><el-link type="primary" @click="addCustomPeriodRow">添加</el-link></template> :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> </el-table>
</section> </section>
@@ -263,17 +423,48 @@
<div class="dialog-section-title"> <div class="dialog-section-title">
付款比例设置 付款比例设置
<el-tooltip content="非必填;配置付款比例时,合计必须等于100%。" placement="top"> <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> </el-tooltip>
</div> </div>
<el-table :data="paymentRatioRows" border class="change-table"> <el-table :data="paymentRatioRows" border class="change-table">
<el-table-column type="index" label="序号" width="70" /> <el-table-column type="index" label="序号" width="70" />
<el-table-column prop="paymentTerm" label="付款笔数" min-width="180" /> <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"
<el-table-column label="备注" min-width="220"><template #default="{ row }"><el-input v-model="row.remark" maxlength="200" /></template></el-table-column> ><template #default="{ row }"
<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-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-table>
<el-button type="primary" plain @click="addPaymentRatioRow">添加</el-button> <el-button
v-if="!isSettlementRuleChange"
type="primary"
plain
@click="addPaymentRatioRow"
>添加</el-button
>
</section> </section>
<billing-plan-editor <billing-plan-editor
@@ -290,6 +481,7 @@
attachment-type attachment-type
attachment-location="附件(佐证材料等)" attachment-location="附件(佐证材料等)"
attachment-type-dict-code="contract_supporting_attachment_type" attachment-type-dict-code="contract_supporting_attachment_type"
:readonly="isSettlementRuleChange"
:attachment-location-options="changeAttachmentLocationOptions" :attachment-location-options="changeAttachmentLocationOptions"
:rows="attachments" :rows="attachments"
:preview="previewAttachment" :preview="previewAttachment"
@@ -494,7 +686,13 @@ export default {
pageTitle() { pageTitle() {
return this.$route.query.name || '合同变更'; return this.$route.query.name || '合同变更';
}, },
isSettlementRuleChange() {
return this.form.changeType === '结算规则变更';
},
changeAttachmentLocationOptions() { changeAttachmentLocationOptions() {
if (this.isSettlementRuleChange) {
return ['变更材料'];
}
return ['用印材料', '附件(佐证材料等)', '变更材料']; return ['用印材料', '附件(佐证材料等)', '变更材料'];
}, },
contractAmountDisplay() { contractAmountDisplay() {
@@ -720,6 +918,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; }, 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 = [] }) { handleAttachmentUploadToLocation({ location, rows = [] }) {
if (!rows.length) return; if (!rows.length) return;
if (this.isSettlementRuleChange) {
if (location === '变更材料') {
this.changeMaterials = [...(this.changeMaterials || []), ...rows];
}
return;
}
if (isContractSealMaterialLocation(location)) { if (isContractSealMaterialLocation(location)) {
this.contractFileRows = [...(this.contractFileRows || []), ...rows]; this.contractFileRows = [...(this.contractFileRows || []), ...rows];
return; return;
@@ -743,7 +947,81 @@ export default {
removeCustomPeriodRow(index) { if (index <= 0) return; const periods = [...(this.settlementRule.customPeriods || [])]; periods.splice(index, 1); this.settlementRule.customPeriods = normalizeCustomPeriods(periods); }, 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; }, 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; }, 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> </script>
+290 -26
View File
@@ -1357,19 +1357,29 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="110" fixed="right"> <el-table-column label="操作" width="110" fixed="right">
<template #default="{ $index }"> <template #default="{ row, $index }">
<el-link <el-link
v-if="!dialogReadonly" v-if="!dialogReadonly && !isSelectedWaybillRemoveLocked(row)"
type="danger" type="danger"
@click="removeSelectedWaybill($index)" @click="removeSelectedWaybill($index)"
>删除</el-link >删除</el-link
> >
<span
v-else-if="!dialogReadonly && isSelectedWaybillRemoveLocked(row)"
class="loading-manage-dialog__locked-tip"
>已打卡</span
>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
</section-card> </section-card>
<section-card title="线路编辑"> <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 class="loading-manage-dialog__route-list" v-if="routeNodes.length">
<div <div
v-for="(node, index) in routeNodes" v-for="(node, index) in routeNodes"
@@ -1436,7 +1446,7 @@
<el-segmented <el-segmented
v-model="dialogForm.carrierType" v-model="dialogForm.carrierType"
:options="carrierTypeOptions" :options="carrierTypeOptions"
:disabled="dialogReadonly" :disabled="dialogReadonly || isRunningTaskRequiredLocked"
@change="handleCarrierTypeChange" @change="handleCarrierTypeChange"
/> />
</div> </div>
@@ -1450,7 +1460,11 @@
" "
clearable clearable
filterable filterable
:disabled="dialogReadonly || !selectedWaybillRows.length" :disabled="
dialogReadonly ||
isRunningTaskRequiredLocked ||
!selectedWaybillRows.length
"
:loading="taskCarrierLoading" :loading="taskCarrierLoading"
:placeholder="taskCarrierPlaceholder" :placeholder="taskCarrierPlaceholder"
@change="handleTaskCarrierChange" @change="handleTaskCarrierChange"
@@ -1483,6 +1497,7 @@
:fetch-suggestions="fetchDriverSuggestions" :fetch-suggestions="fetchDriverSuggestions"
:loading="driverLoading" :loading="driverLoading"
placeholder="请输入" placeholder="请输入"
:disabled="dialogReadonly || isRunningSelfDriverLocked"
@select="item => handleDriverSuggestionSelect('dialog', item)" @select="item => handleDriverSuggestionSelect('dialog', item)"
/> />
</el-form-item> </el-form-item>
@@ -1490,13 +1505,28 @@
label="手机号" label="手机号"
:required="dialogForm.carrierType === '自运'" :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>
<el-form-item label="车牌号" required> <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>
<el-form-item v-if="dialogForm.carrierType !== '承运商'" label="挂车车牌号"> <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>
<el-form-item v-if="dialogForm.carrierType !== '承运商'" label="押运人"> <el-form-item v-if="dialogForm.carrierType !== '承运商'" label="押运人">
<el-autocomplete <el-autocomplete
@@ -1506,11 +1536,17 @@
:fetch-suggestions="fetchEscortSuggestions" :fetch-suggestions="fetchEscortSuggestions"
:loading="escortLoading" :loading="escortLoading"
placeholder="请输入" placeholder="请输入"
:disabled="dialogReadonly"
@select="item => handleEscortSuggestionSelect('dialog', item)" @select="item => handleEscortSuggestionSelect('dialog', item)"
/> />
</el-form-item> </el-form-item>
<el-form-item v-if="dialogForm.carrierType !== '承运商'" label="押运人手机号"> <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>
<el-form-item label="里程(km)"> <el-form-item label="里程(km)">
<el-input <el-input
@@ -1528,6 +1564,7 @@
format="YYYY-MM-DD" format="YYYY-MM-DD"
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD"
placeholder="请选择" placeholder="请选择"
:disabled="dialogReadonly"
/> />
</el-form-item> </el-form-item>
<el-form-item label="预计完成日期"> <el-form-item label="预计完成日期">
@@ -1537,6 +1574,7 @@
format="YYYY-MM-DD" format="YYYY-MM-DD"
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD"
placeholder="请选择" placeholder="请选择"
:disabled="dialogReadonly"
/> />
</el-form-item> </el-form-item>
<el-form-item label="备注" class="loading-manage-dialog__span-2"> <el-form-item label="备注" class="loading-manage-dialog__span-2">
@@ -1547,6 +1585,7 @@
maxlength="500" maxlength="500"
show-word-limit show-word-limit
placeholder="请输入,500字以内" placeholder="请输入,500字以内"
:disabled="dialogReadonly"
/> />
</el-form-item> </el-form-item>
</div> </div>
@@ -1561,11 +1600,17 @@
</template> </template>
<template v-else> <template v-else>
<el-button @click="closeLoadingDialog">关闭</el-button> <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>
<el-button type="primary" :loading="dialogSaving === 'submit'" @click="submitLoading"> <el-button type="primary" :loading="dialogSaving === 'submit'" @click="submitLoading">
{{ dialogMode === 'reassign' ? '确认派单' : '确认' }} {{ dialogMode === 'reassign' ? '确认派单' : isRunningEdit ? '保存' : '确认' }}
</el-button> </el-button>
</template> </template>
</div> </div>
@@ -2107,6 +2152,8 @@ export default {
routeNodes: [], routeNodes: [],
cargoRows: [], cargoRows: [],
routeDragIndex: -1, routeDragIndex: -1,
/** 进行中编辑:子运单打卡记录映射 waybillId -> records[] */
waybillPunchMap: {},
transportTypeOptions: [], transportTypeOptions: [],
projectOptions: [], projectOptions: [],
customerOptions: [], customerOptions: [],
@@ -2170,6 +2217,23 @@ export default {
dialogReadonly() { dialogReadonly() {
return this.dialogMode === 'view'; 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() { orderedPunchRecords() {
const records = Array.isArray(this.punchRecords) ? this.punchRecords : []; const records = Array.isArray(this.punchRecords) ? this.punchRecords : [];
@@ -2913,6 +2977,7 @@ export default {
if (status === 'running') { if (status === 'running') {
return [ return [
{ type: 'view', label: '详情' }, { type: 'view', label: '详情' },
{ type: 'edit', label: '编辑' },
{ type: 'complete', label: '完成' }, { type: 'complete', label: '完成' },
]; ];
} }
@@ -3014,7 +3079,12 @@ export default {
query: { query: {
mode, mode,
...(mode === 'edit' && row?.id ? { id: row.id } : {}), ...(mode === 'edit' && row?.id ? { id: row.id } : {}),
name: mode === 'edit' ? '编辑配载管理' : '新增配载管理', name:
mode === 'edit'
? row?.businessStatus === 'running'
? '编辑进行中配载'
: '编辑配载管理'
: '新增配载管理',
}, },
}); });
return; return;
@@ -3037,6 +3107,9 @@ export default {
this.dialogForm.loadingSubNos this.dialogForm.loadingSubNos
); );
this.syncProcessWaybillOptions(); this.syncProcessWaybillOptions();
if (this.isRunningEdit) {
await this.refreshWaybillPunchMap();
}
this.restoreRouteAndCargo(); this.restoreRouteAndCargo();
this.restoreRouteChangeRecords(); this.restoreRouteChangeRecords();
this.loadPunchRecords(); this.loadPunchRecords();
@@ -3071,6 +3144,8 @@ export default {
this.taskCarrierLoading = false; this.taskCarrierLoading = false;
this.routeNodes = []; this.routeNodes = [];
this.cargoRows = []; this.cargoRows = [];
this.routeDragIndex = -1;
this.waybillPunchMap = {};
this.candidatePage.currentPage = 1; this.candidatePage.currentPage = 1;
this.candidatePage.total = 0; this.candidatePage.total = 0;
this.candidateSearchVisible = false; this.candidateSearchVisible = false;
@@ -3435,20 +3510,144 @@ export default {
const statusName = String(row.businessStatusName || row.statusName || ''); const statusName = String(row.businessStatusName || row.statusName || '');
return statusName.includes('已完成'); 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 = {}) { isRouteChangeNodeLocked(node = {}) {
return Boolean(node?.locked); return Boolean(node?.locked);
}, },
syncRouteNodeLockedFlags(nodes = [], rows = this.selectedWaybillRows) { syncRouteNodeLockedFlags(nodes = [], rows = this.selectedWaybillRows) {
return (nodes || []).map(node => { return (nodes || []).map(node => {
const locked = (node.tags || []).some(tag => { const locked = (node.tags || []).some(tag => this.isRouteNodeTagLocked(tag, rows));
const match = String(tag).match(/^(装|卸)(\d+)$/);
if (!match) return false;
const row = rows[Number(match[2]) - 1];
return row ? this.isWaybillCompleted(row) : false;
});
return { ...node, locked }; 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) { openRouteChangeMapDialog(row, field) {
if (this.isWaybillCompleted(row)) return; if (this.isWaybillCompleted(row)) return;
this.routeChangeMapTarget = { row, field }; this.routeChangeMapTarget = { row, field };
@@ -3766,6 +3965,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( const exists = new Set(
this.selectedWaybillRows.map(row => String(row.id || row.waybillNo)) this.selectedWaybillRows.map(row => String(row.id || row.waybillNo))
); );
@@ -3777,8 +4003,12 @@ export default {
exists.add(key); exists.add(key);
} }
}); });
const prevRouteNodes = [...this.routeNodes];
this.selectedWaybillRows = nextRows; this.selectedWaybillRows = nextRows;
this.rebuildSummaryFromWaybills(); if (this.isRunningEdit) {
await this.refreshWaybillPunchMap();
}
this.rebuildSummaryFromWaybills(true, prevRouteNodes);
await this.loadTaskCarrierOptions(); await this.loadTaskCarrierOptions();
this.$refs.candidateTableRef?.clearSelection?.(); this.$refs.candidateTableRef?.clearSelection?.();
this.candidateSelection = []; this.candidateSelection = [];
@@ -3790,11 +4020,23 @@ export default {
await this.addWaybillRows(this.candidateSelection); await this.addWaybillRows(this.candidateSelection);
}, },
removeSelectedWaybill(index) { removeSelectedWaybill(index) {
const row = this.selectedWaybillRows[index];
if (this.isSelectedWaybillRemoveLocked(row)) {
ElMessage.warning('已打卡的子运单不可移除');
return;
}
this.selectedWaybillRows.splice(index, 1); this.selectedWaybillRows.splice(index, 1);
if (this.isRunningEdit) {
this.refreshWaybillPunchMap().then(() => {
this.rebuildSummaryFromWaybills();
this.loadTaskCarrierOptions();
});
return;
}
this.rebuildSummaryFromWaybills(); this.rebuildSummaryFromWaybills();
this.loadTaskCarrierOptions(); this.loadTaskCarrierOptions();
}, },
rebuildSummaryFromWaybills(rebuildRoute = true) { rebuildSummaryFromWaybills(rebuildRoute = true, prevRouteNodes = null) {
const rows = this.selectedWaybillRows; const rows = this.selectedWaybillRows;
this.dialogForm.loadingSubNos = rows this.dialogForm.loadingSubNos = rows
.map(row => row.waybillNo) .map(row => row.waybillNo)
@@ -3819,7 +4061,13 @@ export default {
this.dialogForm.transportType = rows.find(row => row.transportType)?.transportType || ''; this.dialogForm.transportType = rows.find(row => row.transportType)?.transportType || '';
} }
if (rebuildRoute) { 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); this.cargoRows = this.buildCargoRows(rows);
// 编辑回填阶段 routeNodes 尚未恢复时跳过,避免清空已有发到货地 // 编辑回填阶段 routeNodes 尚未恢复时跳过,避免清空已有发到货地
@@ -3839,7 +4087,9 @@ export default {
if (locked) node.locked = true; if (locked) node.locked = true;
}; };
rows.forEach((row, rowIndex) => { 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; const departure = row.departureAddress || row.departureName;
appendNode( appendNode(
departure, departure,
@@ -3849,7 +4099,9 @@ export default {
); );
}); });
rows.forEach((row, rowIndex) => { 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; const arrival = row.arrivalAddress || row.arrivalName;
appendNode( appendNode(
arrival, arrival,
@@ -3901,7 +4153,9 @@ export default {
if ( if (
originalLockedKeys.some((key, lockedIndex) => key && key !== nextLockedKeys[lockedIndex]) originalLockedKeys.some((key, lockedIndex) => key && key !== nextLockedKeys[lockedIndex])
) { ) {
ElMessage.warning('已完成运单对应的路线不可调整'); ElMessage.warning(
this.isRunningEdit ? '已打卡地点不可调整,新地址只能在未打卡节点间拖拽' : '已完成运单对应的路线不可调整'
);
this.routeDragIndex = -1; this.routeDragIndex = -1;
return; return;
} }
@@ -4145,15 +4399,15 @@ export default {
try { try {
const payload = { const payload = {
...this.normalizePayload(), ...this.normalizePayload(),
// 确认生成正式配载单(待执行),覆盖草稿状态 // 进行中编辑保持 running;其余确认生成正式配载单为待执行
businessStatus: 'pending', businessStatus: this.isRunningEdit ? 'running' : 'pending',
}; };
if (this.dialogMode === 'reassign') { if (this.dialogMode === 'reassign') {
await loadingApi.reassign(payload); await loadingApi.reassign(payload);
ElMessage.success('派单成功'); ElMessage.success('派单成功');
} else { } else {
await loadingApi.submit(payload); await loadingApi.submit(payload);
ElMessage.success('确认成功'); ElMessage.success(this.isRunningEdit ? '保存成功' : '确认成功');
} }
sessionStorage.setItem(loadingManageListRefreshKey, '1'); sessionStorage.setItem(loadingManageListRefreshKey, '1');
this.closeLoadingDialog(); this.closeLoadingDialog();
@@ -4787,6 +5041,16 @@ export default {
color: #909399; 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 { .loading-manage-dialog__route-type {
width: 28px; width: 28px;
height: 28px; height: 28px;
+91 -16
View File
@@ -921,7 +921,7 @@ import {
getList as getCustomerArchiveList, getList as getCustomerArchiveList,
getDetail as getCustomerArchiveDetail, getDetail as getCustomerArchiveDetail,
} from '@/api/vehicle/customer-archive'; } 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 getSystemDictionary } from '@/api/system/dict';
import { getDictionary as getBizDictionary } from '@/api/system/dictbiz'; import { getDictionary as getBizDictionary } from '@/api/system/dictbiz';
import { getList as getUserList } from '@/api/system/user'; import { getList as getUserList } from '@/api/system/user';
@@ -1559,6 +1559,7 @@ export default {
projectType: type === 'majorSupplement' ? '重大项目' : '普通项目', projectType: type === 'majorSupplement' ? '重大项目' : '普通项目',
}); });
this.fillDefaultUsers(); this.fillDefaultUsers();
this.fillDefaultBusinessDept();
return; return;
} }
if (!id) return; if (!id) return;
@@ -1891,6 +1892,7 @@ export default {
.filter(Boolean); .filter(Boolean);
this.syncSelectedCustomerOptions(); this.syncSelectedCustomerOptions();
this.ensureCurrentPlatformOption(); this.ensureCurrentPlatformOption();
this.ensureCurrentBusinessDeptOption();
}, },
fillDefaultUsers() { fillDefaultUsers() {
const userId = this.userInfo?.userId || this.userInfo?.id || ''; const userId = this.userInfo?.userId || this.userInfo?.id || '';
@@ -1909,6 +1911,25 @@ export default {
this.form.principalUserName = userName; 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) { resolveUserRealName(row = {}, role) {
return ( return (
row[`${role}RealName`] || row[`${role}RealName`] ||
@@ -2491,17 +2512,61 @@ export default {
this.$refs.projectForm?.validateField('projectType'); this.$refs.projectForm?.validateField('projectType');
}, },
loadDeptOptions() { loadDeptOptions() {
getDeptTree().then(res => { Promise.all([getDeptTree(), getBusinessDeptSelect()])
const deptTree = res.data.data || []; .then(([treeRes, bizRes]) => {
// 新增/编辑/重大补录/项目变更均可选择业务部门,排除外部组织 const deptTree = this.excludeExternalOrganization(treeRes.data.data || []);
const businessDeptTree = ['add', 'edit', 'majorSupplement', 'change'].includes(this.dialogType) const bizList = bizRes.data.data || [];
? this.excludeExternalOrganization(deptTree) const bizIds = new Set(
: deptTree; bizList
this.deptOptions = this.flattenDept(businessDeptTree); .map(item => (item.id == null || item.id === '' ? '' : String(item.id)))
this.businessDeptTreeOptions = this.toDeptCascaderOptions(businessDeptTree); .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(); 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() { loadPlatformCompanyOptions() {
getPlatformCompanySelect().then(res => { getPlatformCompanySelect().then(res => {
const list = res.data.data || []; const list = res.data.data || [];
@@ -2545,12 +2610,22 @@ export default {
return result; return result;
}, []); }, []);
}, },
toDeptCascaderOptions(list = []) { toDeptCascaderOptions(list = [], bizIds = null) {
return list.map(item => ({ return list.map(item => {
label: item.title || item.deptName || item.name, const value = item.id == null || item.id === '' ? item.value : item.id;
value: item.id, const option = {
children: item.children?.length ? this.toDeptCascaderOptions(item.children) : undefined, 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) { flattenDept(list = [], level = 0) {
return list.reduce((result, item) => { return list.reduce((result, item) => {
+1 -1
View File
@@ -271,7 +271,7 @@ export default {
this.unwrapData( this.unwrapData(
await billLedgerApi.getAvailableOptions( await billLedgerApi.getAvailableOptions(
this.billDialog.keyword, this.billDialog.keyword,
this.form.deptId, undefined,
this.form.billLedgerId this.form.billLedgerId
) )
) || []; ) || [];
+3 -20
View File
@@ -1229,14 +1229,6 @@ export default {
callback(); callback();
return; return;
} }
if (
selected.billType &&
selected.billType !== 'received' &&
!this.billMatchesSettlementParties(selected)
) {
callback(new Error('汇票付款仅可选择收票类型,或出票单位与收票单位均为当前结算收付款方的汇票'));
return;
}
if (selected.maturityDate) { if (selected.maturityDate) {
const maturity = this.$dayjs(selected.maturityDate).startOf('day'); const maturity = this.$dayjs(selected.maturityDate).startOf('day');
const today = this.$dayjs().startOf('day'); const today = this.$dayjs().startOf('day');
@@ -2444,8 +2436,7 @@ export default {
} }
}, },
payeeReceiptLabel(item) { payeeReceiptLabel(item) {
const name = item.accountName || item.accountHolderName || '未命名收款方'; return item.accountName || item.accountHolderName || '未命名收款方';
return item.bankAccount ? `${name}(${item.bankAccount})` : name;
}, },
receiptAccountLabel(item) { receiptAccountLabel(item) {
return [item.accountName || item.accountHolderName, item.bankName, item.bankAccount] return [item.accountName || item.accountHolderName, item.bankName, item.bankAccount]
@@ -2603,7 +2594,7 @@ export default {
this.unwrapData( this.unwrapData(
await billLedgerApi.getAvailableOptions( await billLedgerApi.getAvailableOptions(
keyword, keyword,
this.form.deptId, undefined,
selectedId, selectedId,
this.settlementPayerName, this.settlementPayerName,
this.settlementPayeeName this.settlementPayeeName
@@ -2628,7 +2619,7 @@ export default {
this.billPage.current, this.billPage.current,
this.billPage.size, this.billPage.size,
String(this.billQuery.keyword || '').trim() || undefined, String(this.billQuery.keyword || '').trim() || undefined,
this.form.deptId, undefined,
this.form.billLedgerId, this.form.billLedgerId,
this.settlementPayerName, this.settlementPayerName,
this.settlementPayeeName this.settlementPayeeName
@@ -2667,14 +2658,6 @@ export default {
); );
}, },
selectBill(row) { selectBill(row) {
if (
row?.billType &&
row.billType !== 'received' &&
!this.billMatchesSettlementParties(row)
) {
ElMessage.warning('汇票付款仅可选择收票类型,或出票单位与收票单位均为当前结算收付款方的汇票');
return;
}
if (row?.maturityDate) { if (row?.maturityDate) {
const maturity = this.$dayjs(row.maturityDate).startOf('day'); const maturity = this.$dayjs(row.maturityDate).startOf('day');
const today = this.$dayjs().startOf('day'); const today = this.$dayjs().startOf('day');
+24
View File
@@ -535,6 +535,27 @@ export default {
}, },
], ],
}, },
{
label: '是否业务部门',
prop: 'isBusinessDept',
type: 'select',
dicData: [
{ label: '否', value: 0 },
{ label: '是', value: 1 },
],
dataType: 'number',
value: 0,
width: 120,
order: 34,
formatter: row => (Number(row.isBusinessDept) === 1 ? '是' : '否'),
rules: [
{
required: true,
message: '请选择是否业务部门',
trigger: 'change',
},
],
},
{ {
label: '排序', label: '排序',
prop: 'sort', prop: 'sort',
@@ -1523,6 +1544,9 @@ export default {
if (this.form.isPlatformCompany === undefined || this.form.isPlatformCompany === null) { if (this.form.isPlatformCompany === undefined || this.form.isPlatformCompany === null) {
this.form.isPlatformCompany = 0; this.form.isPlatformCompany = 0;
} }
if (this.form.isBusinessDept === undefined || this.form.isBusinessDept === null) {
this.form.isBusinessDept = 0;
}
this.clearEmptyContractInitialSerial(this.form); this.clearEmptyContractInitialSerial(this.form);
this.initData(this.form.tenantId); this.initData(this.form.tenantId);
this.loadParentDeptCode(this.form.parentId); this.loadParentDeptCode(this.form.parentId);