调整 合同

This commit is contained in:
2026-09-14 15:01:13 +08:00
parent acbc9d3322
commit f1e2313e4c
8 changed files with 1321 additions and 357 deletions
+6
View File
@@ -130,12 +130,18 @@ export const contractApprovalStatusOptions = [
{ label: '草稿', value: 'draft' }, { label: '草稿', value: 'draft' },
{ label: '审批中', value: 'reviewing' }, { label: '审批中', value: 'reviewing' },
{ label: '已驳回', value: 'rejected' }, { label: '已驳回', value: 'rejected' },
{ label: '已撤回', value: 'withdrawn' },
{ label: '审批通过', value: 'approved' }, { label: '审批通过', value: 'approved' },
{ label: '变更审批中', value: 'change_reviewing' }, { label: '变更审批中', value: 'change_reviewing' },
{ label: '变更驳回', value: 'change_rejected' }, { label: '变更驳回', value: 'change_rejected' },
{ label: '变更审批通过', value: 'change_approved' }, { label: '变更审批通过', value: 'change_approved' },
]; ];
export const contractArchiveStatusOptions = [
{ label: '未归档', value: '未归档' },
{ label: '已归档', value: '已归档' },
];
export const nodeOptions = [ export const nodeOptions = [
{ label: '接单', value: '接单' }, { label: '接单', value: '接单' },
{ label: '到场', value: '到场' }, { label: '到场', value: '到场' },
+63 -3
View File
@@ -1,6 +1,7 @@
import { import {
auditColumns, auditColumns,
contractApprovalStatusOptions, contractApprovalStatusOptions,
contractArchiveStatusOptions,
contractCategoryOptions, contractCategoryOptions,
contractStageOptions, contractStageOptions,
createCrudOption, createCrudOption,
@@ -48,7 +49,7 @@ export const config = {
batchDelete: false, batchDelete: false,
enableProjectSelect: true, enableProjectSelect: true,
projectQueryParams: { projectQueryParams: {
approvalStatuses: 'approved,change_approved', contractSelectable: true,
}, },
actions: ['copy'], actions: ['copy'],
statusProp: 'approvalStatus', statusProp: 'approvalStatus',
@@ -71,6 +72,10 @@ export const config = {
['legalSealFlag', '是否需要加盖法人章'], ['legalSealFlag', '是否需要加盖法人章'],
['copyCount', '一式(份)'], ['copyCount', '一式(份)'],
['paymentDays', '回款账期(天)'], ['paymentDays', '回款账期(天)'],
['contractAmount', '合同金额'],
['templateFlag', '是否范本'],
['originalContractNo', '原件合同编号'],
['electronicSealFlag', '是否电子章'],
['remark', '备注', 2], ['remark', '备注', 2],
], ],
}, },
@@ -84,6 +89,7 @@ export const config = {
['effectiveType', '生效类型'], ['effectiveType', '生效类型'],
['contractStage', '合同阶段'], ['contractStage', '合同阶段'],
['approvalStatus', '审核状态'], ['approvalStatus', '审核状态'],
['archiveStatus', '归档状态'],
['currentNode', '当前节点'], ['currentNode', '当前节点'],
['currentProcessor', '当前处理人'], ['currentProcessor', '当前处理人'],
], ],
@@ -104,7 +110,7 @@ export const config = {
}, },
deleteStatus: ['draft'], deleteStatus: ['draft'],
deleteStage: ['draft'], deleteStage: ['draft'],
editStatus: ['draft', 'rejected', 'change_rejected'], editStatus: ['draft', 'withdrawn', 'rejected', 'change_rejected'],
operations: [ operations: [
{ {
action: 'flow', action: 'flow',
@@ -271,7 +277,7 @@ export const option = createCrudOption([
formslot: true, formslot: true,
order: 165, order: 165,
dicUrl: dicUrl:
'/blade-transport/project-apply/list?current=1&size=9999&approvalStatuses=approved,change_approved', '/blade-transport/project-apply/list?current=1&size=9999&contractSelectable=true',
dicFormatter: projectNameDicFormatter, dicFormatter: projectNameDicFormatter,
props: { props: {
label: 'projectName', label: 'projectName',
@@ -485,6 +491,47 @@ export const option = createCrudOption([
order: 50, order: 50,
minWidth: 130, minWidth: 130,
}, },
{
label: '合同金额',
prop: 'contractAmount',
type: 'number',
min: 0,
precision: 2,
hide: true,
order: 45,
minWidth: 130,
},
{
label: '是否范本',
prop: 'templateFlag',
type: 'select',
dicData: [
{ label: '否', value: 0 },
{ label: '是', value: 1 },
],
hide: true,
order: 40,
minWidth: 100,
},
{
label: '原件合同编号',
prop: 'originalContractNo',
hide: true,
order: 35,
minWidth: 160,
},
{
label: '是否电子章',
prop: 'electronicSealFlag',
type: 'select',
dicData: [
{ label: '否', value: 0 },
{ label: '是', value: 1 },
],
hide: true,
order: 30,
minWidth: 110,
},
{ {
label: '创建时间', label: '创建时间',
prop: 'createTime', prop: 'createTime',
@@ -522,6 +569,19 @@ export const option = createCrudOption([
addDisplay: false, addDisplay: false,
editDisplay: false, editDisplay: false,
}, },
{
label: '归档状态',
prop: 'archiveStatus',
type: 'select',
search: true,
searchOrder: 5,
searchPlaceholder: '请选择',
slot: true,
dicData: contractArchiveStatusOptions,
minWidth: 110,
addDisplay: false,
editDisplay: false,
},
{ {
label: '当前节点', label: '当前节点',
prop: 'currentNode', prop: 'currentNode',
@@ -229,32 +229,38 @@
</div></template </div></template
></el-table-column ></el-table-column
> >
<el-table-column label="保底计费重量" width="240" <el-table-column width="240"
><template #header
><span class="billing-plan-editor__column-title"
>保底计费重量<el-tooltip content="实际计量值小于保底数值时按保底计费" placement="top"
><el-icon class="billing-plan-editor__default-tip"><InfoFilled /></el-icon></el-tooltip></span></template
><template #default="{ row }" ><template #default="{ row }"
><div v-if="readonly && usesRangeMinimum(row)" class="limit-list"> ><div v-if="readonly && usesRangeMinimum(row)" class="limit-list">
<span v-for="(item, rangeIndex) in getRanges(row)" :key="rangeIndex">{{ <span v-for="(item, rangeIndex) in getRanges(row)" :key="rangeIndex">{{
displayValue(item.minimumBillingWeight) rangeIndex === 0
? displayValue(item.minimumBillingWeight || row.minimumBillingWeight)
: ''
}}</span> }}</span>
</div> </div>
<span v-else-if="readonly">{{ displayValue(row.minimumBillingWeight) }}</span> <span v-else-if="readonly">{{ displayValue(row.minimumBillingWeight) }}</span>
<div v-else-if="usesRangeMinimum(row)" class="limit-list"> <div v-else-if="usesRangeMinimum(row)" class="limit-list">
<el-input <template v-for="(item, rangeIndex) in getRanges(row)" :key="rangeIndex">
v-for="(item, rangeIndex) in getRanges(row)" <el-input
:key="rangeIndex" v-if="rangeIndex === 0"
v-model="item.minimumBillingWeight" v-model="item.minimumBillingWeight"
:disabled="!canEditMinimum(row)" :disabled="!canEditMinimum(row)"
:placeholder="canEditMinimum(row) ? '请输入保底计费重量' : '无需配置'" :placeholder="canEditMinimum(row) ? '请输入' : '无需配置'"
@input="value => rangeMinimumInput(row, rangeIndex, value)" @input="value => rangeMinimumInput(row, rangeIndex, value)"
/> />
<div v-else class="limit-placeholder" />
</template>
</div> </div>
<el-input <el-input
v-else v-else
v-model="row.minimumBillingWeight" v-model="row.minimumBillingWeight"
:disabled="!canEditMinimum(row)" :disabled="!canEditMinimum(row)"
:placeholder=" :placeholder="
canEditMinimum(row) canEditMinimum(row) ? '请输入' : '仅按重量、按吨·公里可填写'
? '实际计量值小于保底数值时按保底计费'
: '仅按重量、按吨·公里可填写'
" "
@input="value => decimalInput(row, 'minimumBillingWeight', value)" /></template @input="value => decimalInput(row, 'minimumBillingWeight', value)" /></template
></el-table-column> ></el-table-column>
@@ -297,9 +303,6 @@
<el-descriptions-item label="目的地">{{ <el-descriptions-item label="目的地">{{
displayValue(matchForm.destination) displayValue(matchForm.destination)
}}</el-descriptions-item> }}</el-descriptions-item>
<el-descriptions-item label="运输方式">{{
transportModeLabel(matchForm.transportMode)
}}</el-descriptions-item>
<el-descriptions-item label="货物类型">{{ <el-descriptions-item label="货物类型">{{
displayValue(matchForm.cargoType) displayValue(matchForm.cargoType)
}}</el-descriptions-item> }}</el-descriptions-item>
@@ -334,21 +337,6 @@
@visible-change="v => v && ensureRegions()" @visible-change="v => v && ensureRegions()"
@change="v => matchRegionChange('destination', v)" /></el-form-item @change="v => matchRegionChange('destination', v)" /></el-form-item
></el-col> ></el-col>
<el-col :span="12"
><el-form-item label="运输方式"
><el-select
v-model="matchForm.transportMode"
:loading="transportModeLoading"
:disabled="readonly"
placeholder="请选择运输方式"
clearable
filterable
><el-option
v-for="item in transportModeOptions"
:key="item.value"
:label="item.label"
:value="item.value" /></el-select></el-form-item
></el-col>
<el-col :span="12" <el-col :span="12"
><el-form-item label="货物类型" ><el-form-item label="货物类型"
><el-cascader ><el-cascader
@@ -560,30 +548,24 @@ export default {
return next; return next;
}, },
normalizeRanges(row) { normalizeRanges(row) {
const ranges = Array.isArray(row.limitRanges) const rawRanges = Array.isArray(row.limitRanges) ? row.limitRanges : [];
? row.limitRanges const ranges = rawRanges
.map(item => ({ .map((item, index) => ({
lowerLimit: item?.lowerLimit ?? '', lowerLimit: item?.lowerLimit ?? '',
upperLimit: item?.upperLimit ?? '', upperLimit: item?.upperLimit ?? '',
unitPrice: unitPrice:
item?.unitPrice === undefined || item?.unitPrice === '' item?.unitPrice === undefined || item?.unitPrice === ''
? row.unitPrice ?? '' ? row.unitPrice ?? ''
: item.unitPrice, : item.unitPrice,
minimumBillingWeight: minimumBillingWeight: this.resolveRangeMinimum(row, item, index),
item?.minimumBillingWeight === undefined || item?.minimumBillingWeight === '' }))
? this.usesRangeMinimum(row) .filter(
? row.minimumBillingWeight ?? '' item =>
: '' item.lowerLimit !== '' ||
: item.minimumBillingWeight, item.upperLimit !== '' ||
})) item.unitPrice !== '' ||
.filter( item.minimumBillingWeight !== ''
item => );
item.lowerLimit !== '' ||
item.upperLimit !== '' ||
item.unitPrice !== '' ||
item.minimumBillingWeight !== ''
)
: [];
if ( if (
!ranges.length && !ranges.length &&
(row.lowerLimit !== '' || (row.lowerLimit !== '' ||
@@ -598,9 +580,23 @@ export default {
minimumBillingWeight: this.usesRangeMinimum(row) ? row.minimumBillingWeight ?? '' : '', minimumBillingWeight: this.usesRangeMinimum(row) ? row.minimumBillingWeight ?? '' : '',
}); });
return ranges.length return ranges.length
? ranges ? ranges.map((item, index) => ({
...item,
minimumBillingWeight: this.usesRangeMinimum(row)
? index === 0
? item.minimumBillingWeight
: ''
: '',
}))
: [{ lowerLimit: '', upperLimit: '', unitPrice: '', minimumBillingWeight: '' }]; : [{ lowerLimit: '', upperLimit: '', unitPrice: '', minimumBillingWeight: '' }];
}, },
resolveRangeMinimum(row, item, index) {
if (!this.usesRangeMinimum(row) || index !== 0) return '';
if (item?.minimumBillingWeight !== undefined && item?.minimumBillingWeight !== '') {
return item.minimumBillingWeight;
}
return row.minimumBillingWeight ?? '';
},
syncLegacyLimit(row) { syncLegacyLimit(row) {
const first = row.limitRanges?.[0] || {}; const first = row.limitRanges?.[0] || {};
row.lowerLimit = first.lowerLimit ?? ''; row.lowerLimit = first.lowerLimit ?? '';
@@ -803,11 +799,16 @@ export default {
if (index === 0) row.unitPrice = target.value; if (index === 0) row.unitPrice = target.value;
}, },
rangeMinimumInput(row, index, value) { rangeMinimumInput(row, index, value) {
if (index !== 0) return;
row.limitRanges = this.getRanges(row); row.limitRanges = this.getRanges(row);
const target = { value }; const target = { value };
this.decimalInput(target, 'value', value); this.decimalInput(target, 'value', value);
row.limitRanges[index].minimumBillingWeight = target.value; row.limitRanges[0].minimumBillingWeight = target.value;
if (index === 0) row.minimumBillingWeight = target.value; row.minimumBillingWeight = target.value;
row.limitRanges = row.limitRanges.map((item, rangeIndex) => ({
...item,
minimumBillingWeight: rangeIndex === 0 ? target.value : '',
}));
}, },
addRule() { addRule() {
this.draft.rules.push(defaultRule()); this.draft.rules.push(defaultRule());
@@ -929,15 +930,22 @@ export default {
plan.transportModeLabel = transportMode?.label || plan.transportMode; plan.transportModeLabel = transportMode?.label || plan.transportMode;
plan.rules = plan.rules.map(rule => { plan.rules = plan.rules.map(rule => {
const ranges = this.canEditLimit(rule) ? this.getRanges(rule) : []; const ranges = this.canEditLimit(rule) ? this.getRanges(rule) : [];
const normalizedRanges = ranges.map((item, rangeIndex) => ({
...item,
minimumBillingWeight:
this.usesRangeMinimum(rule) && rangeIndex === 0 ? item.minimumBillingWeight || '' : '',
}));
return { return {
...rule, ...rule,
unitPrice: this.usesRangeUnitPrice(rule) ? ranges[0]?.unitPrice || '' : rule.unitPrice, unitPrice: this.usesRangeUnitPrice(rule)
limitRanges: ranges, ? normalizedRanges[0]?.unitPrice || ''
lowerLimit: ranges[0]?.lowerLimit || '', : rule.unitPrice,
upperLimit: ranges[0]?.upperLimit || '', limitRanges: normalizedRanges,
lowerLimit: normalizedRanges[0]?.lowerLimit || '',
upperLimit: normalizedRanges[0]?.upperLimit || '',
minimumBillingWeight: this.canEditMinimum(rule) minimumBillingWeight: this.canEditMinimum(rule)
? this.usesRangeMinimum(rule) ? this.usesRangeMinimum(rule)
? ranges[0]?.minimumBillingWeight || '' ? normalizedRanges[0]?.minimumBillingWeight || ''
: rule.minimumBillingWeight : rule.minimumBillingWeight
: '', : '',
}; };
@@ -950,9 +958,6 @@ export default {
this.matchIndex = index; this.matchIndex = index;
this.matchForm = { ...defaultRule().matchCondition, ...(row.matchCondition || {}) }; this.matchForm = { ...defaultRule().matchCondition, ...(row.matchCondition || {}) };
this.matchVisible = true; this.matchVisible = true;
this.ensureTransportModes().then(() => {
this.matchForm.transportMode = this.normalizeTransportMode(this.matchForm.transportMode);
});
this.ensureRegions(); this.ensureRegions();
this.ensureCargo().then(() => { this.ensureCargo().then(() => {
const path = this.resolveCargoPath(this.matchForm); const path = this.resolveCargoPath(this.matchForm);
@@ -977,21 +982,6 @@ export default {
}); });
return this.transportModeRequest; return this.transportModeRequest;
}, },
normalizeTransportMode(value) {
const transportMode = String(value || '').trim();
if (!transportMode) return '';
const option = this.transportModeOptions.find(item => {
const optionValue = String(item.value || '').trim();
const optionLabel = String(item.label || '').trim();
return (
optionValue === transportMode ||
optionLabel === transportMode ||
optionLabel.includes(transportMode) ||
transportMode.includes(optionLabel)
);
});
return option?.value || transportMode;
},
ensureRegions() { ensureRegions() {
if (this.regionOptions.length) return Promise.resolve(this.regionOptions); if (this.regionOptions.length) return Promise.resolve(this.regionOptions);
if (this.regionRequest) return this.regionRequest; if (this.regionRequest) return this.regionRequest;
@@ -1214,6 +1204,11 @@ export default {
cursor: pointer; cursor: pointer;
} }
.billing-plan-editor__column-title {
display: inline-flex;
align-items: center;
}
.limit-list { .limit-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -1229,4 +1224,8 @@ export default {
.limit-row .el-input { .limit-row .el-input {
flex: 1; flex: 1;
} }
.limit-placeholder {
height: 32px;
}
</style> </style>
@@ -1927,13 +1927,13 @@
</el-form-item> </el-form-item>
<el-form-item <el-form-item
v-if="showSettlementBillCycleType" v-if="showSettlementBillCycleType"
label="账单周期类型" label="结算周期"
required required
:error="settlementRuleErrors.billCycleType" :error="settlementRuleErrors.billCycleType"
> >
<el-select <el-select
v-model="settlementRuleForm.billCycleType" v-model="settlementRuleForm.billCycleType"
placeholder="请选择账单周期类型" placeholder="请选择结算周期"
:disabled="dialogReadonly" :disabled="dialogReadonly"
@change="handleSettlementBillCycleTypeChange" @change="handleSettlementBillCycleTypeChange"
> >
@@ -1986,6 +1986,59 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-form> </el-form>
<div
v-if="settlementRuleEnabled && showSettlementCustomPeriods"
class="business-crud-page__custom-periods"
>
<el-table :data="settlementRuleForm.customPeriods || []" border>
<el-table-column type="index" label="序号" width="80" align="center" />
<el-table-column label="运单区间-开始日" min-width="180" align="center">
<template #default="{ row, $index }">
<el-select
:model-value="row.startDay"
placeholder="请选择"
clearable
:disabled="dialogReadonly"
@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" align="center">
<template #default="{ row, $index }">
<el-select
:model-value="row.endDay"
placeholder="请选择"
clearable
:disabled="dialogReadonly"
@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 v-if="!dialogReadonly" label="操作" width="100" align="center">
<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 v-if="!dialogReadonly" #empty>
<el-link type="primary" @click="addCustomPeriodRow">添加</el-link>
</template>
</el-table>
</div>
</div> </div>
</template> </template>
@@ -5412,13 +5465,19 @@ const defaultBillingPlan = () => ({
}); });
const defaultSettlementRule = () => ({ const defaultSettlementRule = () => ({
autoGenerate: 1, autoGenerate: '',
billStartDate: '', billStartDate: '',
settlementType: '月结', settlementType: '',
billCycleType: '固定截单日', billCycleType: '',
billCutoffDay: 25, billCutoffDay: '',
cycleDays: '', cycleDays: '',
customPeriods: [],
}); });
const normalizeCustomPeriods = (periods = []) =>
(periods || []).map(item => ({
startDay: Number(item?.startDay) > 0 ? Number(item.startDay) : '',
endDay: Number(item?.endDay) > 0 ? Number(item.endDay) : '',
}));
const defaultReconciliation = () => ({ const defaultReconciliation = () => ({
skipReconciliation: 1, skipReconciliation: 1,
@@ -5837,7 +5896,7 @@ export default {
按数量: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'], 按数量: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
}, },
settlementTypeOptions: ['月结', '日结', '周结', '半月结', '固定天数周期结算'], settlementTypeOptions: ['月结', '日结', '周结', '半月结', '固定天数周期结算'],
billCycleTypeOptions: ['固定截单日', '自然月'], billCycleTypeOptions: ['固定截单日', '自然月', '自定义多周期'],
reconciliationModeOptions: ['明细逐行核对', '按账单汇总核对', '跳过自动核对'], reconciliationModeOptions: ['明细逐行核对', '按账单汇总核对', '跳过自动核对'],
packageOptions, packageOptions,
quantityUnitOptions: ['吨', '千克', '立方米', '件', '车', '箱', '托盘'], quantityUnitOptions: ['吨', '千克', '立方米', '件', '车', '箱', '托盘'],
@@ -6448,6 +6507,11 @@ export default {
this.showSettlementBillCycleType && this.settlementRuleForm.billCycleType === '固定截单日' this.showSettlementBillCycleType && this.settlementRuleForm.billCycleType === '固定截单日'
); );
}, },
showSettlementCustomPeriods() {
return (
this.showSettlementBillCycleType && this.settlementRuleForm.billCycleType === '自定义多周期'
);
},
showSettlementCycleDays() { showSettlementCycleDays() {
return this.settlementRuleForm.settlementType === '固定天数周期结算'; return this.settlementRuleForm.settlementType === '固定天数周期结算';
}, },
@@ -7025,7 +7089,7 @@ export default {
if (!row) return '-'; if (!row) return '-';
if (prop === 'businessStatus') return this.displayStatus(row, prop); if (prop === 'businessStatus') return this.displayStatus(row, prop);
if (prop === 'feeGenerationMode') { if (prop === 'feeGenerationMode') {
return row[prop] === 'manual' ? '手动生成' : '系统生成'; return row[prop] === 'manual' ? '账单导入生成' : '系统生成';
} }
const column = this.findColumn(this.option.column, prop); const column = this.findColumn(this.option.column, prop);
if (column?.formatter) return column.formatter(row, {}, row[prop]); if (column?.formatter) return column.formatter(row, {}, row[prop]);
@@ -7566,23 +7630,32 @@ export default {
isFixedBillCutoffSettlement(rule = this.settlementRuleForm) { isFixedBillCutoffSettlement(rule = this.settlementRuleForm) {
return this.isMonthlySettlement(rule) && rule.billCycleType === '固定截单日'; return this.isMonthlySettlement(rule) && rule.billCycleType === '固定截单日';
}, },
isCustomMultiPeriodSettlement(rule = this.settlementRuleForm) {
return this.isMonthlySettlement(rule) && rule.billCycleType === '自定义多周期';
},
isFixedCycleSettlement(rule = this.settlementRuleForm) { isFixedCycleSettlement(rule = this.settlementRuleForm) {
return rule.settlementType === '固定天数周期结算'; return rule.settlementType === '固定天数周期结算';
}, },
handleSettlementTypeChange(value) { handleSettlementTypeChange(value) {
this.clearSettlementRuleError('settlementType'); this.clearSettlementRuleError('settlementType');
if (value === '月结') { if (value === '月结') {
if (!this.settlementRuleForm.billCycleType) {
this.settlementRuleForm.billCycleType = '固定截单日';
}
if (this.settlementRuleForm.billCycleType === '固定截单日') { if (this.settlementRuleForm.billCycleType === '固定截单日') {
this.settlementRuleForm.billCutoffDay = this.settlementRuleForm.billCutoffDay || 25; this.settlementRuleForm.customPeriods = [];
} else if (this.settlementRuleForm.billCycleType === '自定义多周期') {
this.settlementRuleForm.billCutoffDay = '';
this.settlementRuleForm.customPeriods = normalizeCustomPeriods(
this.settlementRuleForm.customPeriods || []
);
} else {
this.settlementRuleForm.billCutoffDay = '';
this.settlementRuleForm.customPeriods = [];
} }
this.settlementRuleForm.cycleDays = ''; this.settlementRuleForm.cycleDays = '';
return; return;
} }
this.settlementRuleForm.billCycleType = ''; this.settlementRuleForm.billCycleType = '';
this.settlementRuleForm.billCutoffDay = ''; this.settlementRuleForm.billCutoffDay = '';
this.settlementRuleForm.customPeriods = [];
if (value !== '固定天数周期结算') { if (value !== '固定天数周期结算') {
this.settlementRuleForm.cycleDays = ''; this.settlementRuleForm.cycleDays = '';
} }
@@ -7590,10 +7663,17 @@ export default {
handleSettlementBillCycleTypeChange(value) { handleSettlementBillCycleTypeChange(value) {
this.clearSettlementRuleError('billCycleType'); this.clearSettlementRuleError('billCycleType');
if (value === '固定截单日') { if (value === '固定截单日') {
this.settlementRuleForm.billCutoffDay = this.settlementRuleForm.billCutoffDay || 25; this.settlementRuleForm.customPeriods = [];
return; return;
} }
this.settlementRuleForm.billCutoffDay = ''; this.settlementRuleForm.billCutoffDay = '';
if (value === '自定义多周期') {
this.settlementRuleForm.customPeriods = normalizeCustomPeriods(
this.settlementRuleForm.customPeriods || []
);
return;
}
this.settlementRuleForm.customPeriods = [];
}, },
normalizeSettlementRule(rule = this.settlementRuleForm) { normalizeSettlementRule(rule = this.settlementRuleForm) {
const nextRule = { const nextRule = {
@@ -7603,14 +7683,94 @@ export default {
if (!this.isMonthlySettlement(nextRule)) { if (!this.isMonthlySettlement(nextRule)) {
nextRule.billCycleType = ''; nextRule.billCycleType = '';
nextRule.billCutoffDay = ''; nextRule.billCutoffDay = '';
nextRule.customPeriods = [];
} else if (!this.isFixedBillCutoffSettlement(nextRule)) { } else if (!this.isFixedBillCutoffSettlement(nextRule)) {
nextRule.billCutoffDay = ''; nextRule.billCutoffDay = '';
} }
if (this.isCustomMultiPeriodSettlement(nextRule)) {
nextRule.customPeriods = normalizeCustomPeriods(
Array.isArray(nextRule.customPeriods) ? nextRule.customPeriods : []
);
} else {
nextRule.customPeriods = [];
}
if (!this.isFixedCycleSettlement(nextRule)) { if (!this.isFixedCycleSettlement(nextRule)) {
nextRule.cycleDays = ''; nextRule.cycleDays = '';
} }
return nextRule; return nextRule;
}, },
customPeriodEndDayOptions(row) {
const startDay = Number(row?.startDay);
if (!Number.isFinite(startDay) || startDay < 1) return this.billCutoffDayOptions;
return Array.from({ length: 31 - startDay + 1 }, (_, index) => {
const value = startDay + index;
return { label: `${value}`, value };
});
},
handleCustomPeriodStartDayChange(index, value) {
const periods = normalizeCustomPeriods(this.settlementRuleForm.customPeriods || []);
const row = periods[index];
if (!row) return;
row.startDay = value || '';
const startDay = Number(row.startDay);
const endDay = Number(row.endDay);
if (
Number.isFinite(startDay) &&
Number.isFinite(endDay) &&
(endDay < startDay || endDay > 31)
) {
row.endDay = '';
}
this.settlementRuleForm.customPeriods = periods;
},
handleCustomPeriodEndDayChange(index, value) {
const periods = normalizeCustomPeriods(this.settlementRuleForm.customPeriods || []);
const row = periods[index];
if (!row) return;
row.endDay = value || '';
this.settlementRuleForm.customPeriods = periods;
},
addCustomPeriodRow() {
const periods = normalizeCustomPeriods(this.settlementRuleForm.customPeriods || []);
periods.push({ startDay: '', endDay: '' });
this.settlementRuleForm.customPeriods = periods;
},
removeCustomPeriodRow(index) {
if (index <= 0) return;
const periods = [...(this.settlementRuleForm.customPeriods || [])];
periods.splice(index, 1);
this.settlementRuleForm.customPeriods = normalizeCustomPeriods(periods);
},
validateCustomPeriods(periods = [], label = '') {
const rows = normalizeCustomPeriods(periods);
const prefix = label ? `${label}` : '';
if (!rows.length) {
this.$message.warning(`${prefix}请至少配置一段自定义周期`);
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(`${prefix}请选择第${index + 1}行运单区间开始日`);
return false;
}
if (!Number.isFinite(endDay) || endDay < 1 || endDay > 31) {
this.$message.warning(`${prefix}请选择第${index + 1}行运单区间结束日`);
return false;
}
if (endDay < startDay) {
this.$message.warning(`${prefix}${index + 1}行结束日不能早于开始日`);
return false;
}
if (index > 0 && startDay !== Number(rows[index - 1].endDay) + 1) {
this.$message.warning(`${prefix}自定义多周期区间必须连续,不允许重叠或存在日期缺口`);
return false;
}
}
return true;
},
clearSettlementRuleError(field) { clearSettlementRuleError(field) {
if (!this.settlementRuleErrors[field]) return; if (!this.settlementRuleErrors[field]) return;
this.settlementRuleErrors = { this.settlementRuleErrors = {
@@ -7632,8 +7792,8 @@ export default {
return false; return false;
} }
if (this.isMonthlySettlement(rule) && this.isBillingFieldEmpty(rule.billCycleType)) { if (this.isMonthlySettlement(rule) && this.isBillingFieldEmpty(rule.billCycleType)) {
this.settlementRuleErrors = { billCycleType: '请选择账单周期类型' }; this.settlementRuleErrors = { billCycleType: '请选择结算周期' };
this.$message.warning(`${label}${label ? '' : ''}请选择账单周期类型`); this.$message.warning(`${label}${label ? '' : ''}请选择结算周期`);
return false; return false;
} }
if (this.isFixedBillCutoffSettlement(rule)) { if (this.isFixedBillCutoffSettlement(rule)) {
@@ -7649,6 +7809,9 @@ export default {
return false; return false;
} }
} }
if (this.isCustomMultiPeriodSettlement(rule)) {
return this.validateCustomPeriods(rule.customPeriods, label);
}
if (this.isFixedCycleSettlement(rule)) { if (this.isFixedCycleSettlement(rule)) {
const cycleDays = Number(rule.cycleDays); const cycleDays = Number(rule.cycleDays);
if (!Number.isInteger(cycleDays) || cycleDays < 1 || cycleDays > 365) { if (!Number.isInteger(cycleDays) || cycleDays < 1 || cycleDays > 365) {
@@ -11239,8 +11402,10 @@ export default {
if (!this.canEditBillingLimit(row)) { if (!this.canEditBillingLimit(row)) {
continue; continue;
} }
const key = row.billingElement || ''; const feeItem = String(row.feeItem || '').trim();
groups[key] = groups[key] || []; const billingElement = String(row.billingElement || '').trim();
const key = JSON.stringify([feeItem, billingElement]);
groups[key] = groups[key] || { feeItem, billingElement, ranges: [] };
const ranges = this.getBillingLimitRanges(row); const ranges = this.getBillingLimitRanges(row);
for (const range of ranges) { for (const range of ranges) {
const lower = Number(range.lowerLimit); const lower = Number(range.lowerLimit);
@@ -11258,14 +11423,14 @@ export default {
this.$message.warning('计费要素下限不能大于上限'); this.$message.warning('计费要素下限不能大于上限');
return false; return false;
} }
groups[key].push({ lower, upper }); groups[key].ranges.push({ lower, upper });
} }
} }
return this.validateBillingLimitRangeGroups(groups); return this.validateBillingLimitRangeGroups(groups);
}, },
validateBillingLimitRangeGroups(groups) { validateBillingLimitRangeGroups(groups) {
const precision = 0.000001; const precision = 0.000001;
for (const [billingElement, ranges] of Object.entries(groups)) { for (const { feeItem, billingElement, ranges } of Object.values(groups)) {
if (ranges.length <= 1) continue; if (ranges.length <= 1) continue;
const sortedRanges = [...ranges].sort((prev, next) => { const sortedRanges = [...ranges].sort((prev, next) => {
if (prev.lower !== next.lower) return prev.lower - next.lower; if (prev.lower !== next.lower) return prev.lower - next.lower;
@@ -11275,11 +11440,13 @@ export default {
const prevRange = sortedRanges[index - 1]; const prevRange = sortedRanges[index - 1];
const currentRange = sortedRanges[index]; const currentRange = sortedRanges[index];
if (currentRange.lower < prevRange.upper - precision) { if (currentRange.lower < prevRange.upper - precision) {
this.$message.warning(`${billingElement}的计费要素区间不能重叠`); this.$message.warning(`费用项“${feeItem}${billingElement}的计费要素区间不能重叠`);
return false; return false;
} }
if (currentRange.lower > prevRange.upper + precision) { if (currentRange.lower > prevRange.upper + precision) {
this.$message.warning(`${billingElement}的计费要素区间必须连续,不能存在间隙`); this.$message.warning(
`费用项“${feeItem}${billingElement}的计费要素区间必须连续,不能存在间隙`
);
return false; return false;
} }
} }
@@ -0,0 +1,493 @@
<template>
<section
class="contract-manage-form__section contract-manage-form__section--panel contract-manage-form__section--attachment"
>
<div class="contract-manage-form__attachment-head">
<div class="dialog-section-title">{{ title }}</div>
<el-button type="primary" :disabled="!rows.length" @click="batchDownload">批量下载</el-button>
</div>
<el-table
:data="rows"
border
class="contract-manage-form__attachment-table"
@selection-change="selected = $event"
>
<el-table-column v-if="!readonly" type="selection" width="55" align="center" />
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column v-if="attachmentType" label="附件类型" min-width="160" align="center">
<template #default="{ row }">
<span v-if="readonly || isRowLocked(row)">{{ row.fileType || '-' }}</span>
<el-select
v-else
:model-value="row.fileType"
placeholder="请选择"
:teleported="false"
:fit-input-width="true"
@update:model-value="value => updateRowFileType(row, value)"
>
<el-option
v-for="item in contractAttachmentTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</el-table-column>
<el-table-column
label="文件名"
min-width="240"
align="left"
header-align="left"
show-overflow-tooltip
>
<template #default="{ row }">
<el-link type="primary" @click="preview?.(row, rows)">{{ attachmentName(row) }}</el-link>
</template>
</el-table-column>
<el-table-column v-if="description" label="附件描述" min-width="220">
<template #default="{ row }">
<span v-if="readonly || isRowLocked(row)">{{ row.description || '-' }}</span>
<el-input
v-else
:model-value="row.description"
maxlength="200"
@update:model-value="value => updateRowDescription(row, value)"
/>
</template>
</el-table-column>
<el-table-column label="文件大小" width="120" align="center">
<template #default="{ row }">{{ formatSize(row.size) }}</template>
</el-table-column>
<el-table-column prop="uploadUserName" label="上传人" width="140" align="center" />
<el-table-column prop="uploadTime" label="上传时间" width="180" align="center" sortable />
<el-table-column v-if="!readonly" label="操作" width="100" align="center" fixed="right">
<template #default="{ row, $index }">
<span v-if="isRowLocked(row)">-</span>
<el-link v-else type="danger" @click="remove($index)">删除</el-link>
</template>
</el-table-column>
</el-table>
<div v-if="!readonly" class="contract-manage-form__attachment-upload">
<template v-if="useUploadDialog">
<el-button type="primary" plain @click="openUploadDialog">上传附件</el-button>
<span class="contract-manage-form__attachment-tip">
支持pdf、bmp、jpeg、png、jpg、doc、docx、ppt、pptx、xlsx、xls、eml、msg、zip的文件格式,单个文件不超过50M
</span>
</template>
<vehicle-attachment-upload
v-else
:model-value="rows"
:file-types="attachmentFileTypes"
:max-size="50"
show-tip
tip="支持pdfbmpjpegpngjpgdocdocxpptpptxxlsxxlsemlmsgzip的文件格式单个文件不超过50M"
:show-file-list="false"
button-text="上传附件"
@update:model-value="handleChange"
@change="handleChange"
/>
</div>
<el-dialog
v-model="uploadDialogVisible"
title="附件上传"
width="560px"
append-to-body
destroy-on-close
:close-on-click-modal="false"
class="contract-attachment-upload-dialog"
@closed="resetUploadDialog"
>
<div class="contract-attachment-upload-dialog__body">
<div class="contract-attachment-upload-dialog__field">
<span class="contract-attachment-upload-dialog__label">附件位置</span>
<el-select model-value="合同文件" disabled style="width: 100%">
<el-option label="合同文件" value="合同文件" />
</el-select>
</div>
<div class="contract-attachment-upload-dialog__field">
<span class="contract-attachment-upload-dialog__label">附件类型</span>
<el-select v-model="uploadDialogType" placeholder="请选择附件类型" style="width: 100%">
<el-option
v-for="item in contractAttachmentTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
<div class="contract-attachment-upload-dialog__uploader">
<el-upload
drag
multiple
:action="uploadAction"
:headers="uploadHeaders"
:accept="acceptText"
:show-file-list="true"
:before-upload="beforeUpload"
:on-success="handleDialogSuccess"
:on-error="handleDialogError"
:on-remove="handleDialogRemove"
:file-list="uploadDialogFileList"
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
</el-upload>
</div>
</div>
<template #footer>
<div class="contract-attachment-upload-dialog__footer">
<el-button type="primary" @click="confirmUploadDialog">确定</el-button>
<el-button type="primary" @click="uploadDialogVisible = false">取消</el-button>
</div>
</template>
</el-dialog>
</section>
</template>
<script>
import { UploadFilled } from '@element-plus/icons-vue';
import { baseUrl } from '@/config/env';
import { getUploadHeaders } from '@/utils/upload';
import { downloadFileByUrl } from '@/utils/util';
const CONTRACT_ATTACHMENT_TYPE_OTHER = '其它文件';
export const contractAttachmentTypeOptions = [
{ label: '合同文件', value: '合同文件' },
{ label: '双章归档文件', value: '双章归档文件' },
{ label: '其它文件', value: CONTRACT_ATTACHMENT_TYPE_OTHER },
];
export const attachmentFileTypes = [
'pdf',
'bmp',
'jpeg',
'png',
'jpg',
'doc',
'docx',
'ppt',
'pptx',
'xlsx',
'xls',
'eml',
'msg',
'zip',
];
export const attachmentName = row => row?.originalName || row?.name || row?.fileName || '附件';
export const attachmentUrl = row =>
row?.url || row?.link || row?.fileUrl || row?.downloadUrl || row?.domain || '';
export const isAttachmentApproved = row =>
row?.approved === true ||
row?.approved === 1 ||
row?.approved === '1' ||
row?.approvalStatus === 'approved';
const normalizeAttachmentTypeText = value => {
let text = String(value || '').split(/[?#]/)[0];
try {
text = decodeURIComponent(text);
} catch (error) {
// ignore decode error
}
return text
.replace(/\.[^.\\/]+$/, '')
.toLowerCase()
.replace(/[\s_\-—–·•()()\[\]【】{}《》<>“”"'、,,。.]/g, '');
};
const attachmentTypeKeywords = fileType => {
if (fileType === '双章归档文件') return ['双章归档文件', '双章归档', '双章'];
if (fileType === '合同文件') return ['合同文件', '合同'];
return [fileType];
};
export const resolveContractAttachmentType = (row = {}) => {
const values = contractAttachmentTypeOptions.map(item => item.value);
if (values.includes(row.fileType)) return row.fileType;
const fileName = normalizeAttachmentTypeText(attachmentName(row));
if (!fileName) return CONTRACT_ATTACHMENT_TYPE_OTHER;
const matched = contractAttachmentTypeOptions
.filter(item => item.value !== CONTRACT_ATTACHMENT_TYPE_OTHER)
.flatMap(item =>
attachmentTypeKeywords(item.value).map(keyword => ({
value: item.value,
keyword: normalizeAttachmentTypeText(keyword),
}))
)
.filter(item => item.keyword)
.sort((a, b) => b.keyword.length - a.keyword.length)
.find(item => fileName.includes(item.keyword));
return matched?.value || CONTRACT_ATTACHMENT_TYPE_OTHER;
};
export const normalizeContractFileRows = rows =>
(rows || []).map(row => ({
...row,
fileType: resolveContractAttachmentType(row),
description: row.description || '',
}));
export default {
name: 'ContractAttachmentSection',
components: { UploadFilled },
props: {
title: { type: String, required: true },
rows: { type: Array, default: () => [] },
description: Boolean,
attachmentType: Boolean,
readonly: Boolean,
lockApproved: Boolean,
markApprovedOnUpload: Boolean,
useUploadDialog: Boolean,
preview: { type: Function, default: null },
},
emits: ['update:rows'],
data() {
return {
selected: [],
attachmentFileTypes,
contractAttachmentTypeOptions,
uploadDialogVisible: false,
uploadDialogType: CONTRACT_ATTACHMENT_TYPE_OTHER,
uploadDialogFiles: [],
uploadDialogFileList: [],
};
},
computed: {
uploadAction() {
return `${baseUrl}/blade-resource/oss/endpoint/put-file`;
},
uploadHeaders() {
return getUploadHeaders();
},
acceptText() {
return this.attachmentFileTypes.map(item => `.${item}`).join(',');
},
},
methods: {
attachmentName,
isRowLocked(row) {
return this.lockApproved && isAttachmentApproved(row);
},
update(rows) {
this.$emit('update:rows', rows);
},
updateRowFileType(row, value) {
if (this.isRowLocked(row)) return;
row.fileType = value;
this.update([...this.rows]);
},
updateRowDescription(row, value) {
if (this.isRowLocked(row)) return;
row.description = value;
this.update([...this.rows]);
},
openUploadDialog() {
this.uploadDialogType = CONTRACT_ATTACHMENT_TYPE_OTHER;
this.uploadDialogFiles = [];
this.uploadDialogFileList = [];
this.uploadDialogVisible = true;
},
resetUploadDialog() {
this.uploadDialogFiles = [];
this.uploadDialogFileList = [];
this.uploadDialogType = CONTRACT_ATTACHMENT_TYPE_OTHER;
},
beforeUpload(file) {
const extension = String(file.name || '')
.split('.')
.pop()
?.toLowerCase();
if (!this.attachmentFileTypes.includes(extension)) {
this.$message.warning('文件类型不符合上传要求');
return false;
}
if (file.size / 1024 / 1024 > 50) {
this.$message.warning('单文件大小不能超过 50MB');
return false;
}
return true;
},
normalizeUploadFile(file, response) {
const data = response?.data || file.response?.data || {};
const url = data.link || data.url || data.domain || file.url || '';
const originalName =
data.originalName || file.name || data.name || attachmentName({ url }) || '附件';
return {
uid: file.uid,
originalName,
name: originalName,
url,
link: data.link || url,
size: data.size || data.attachSize || file.size || '',
};
},
handleDialogSuccess(response, file, fileList) {
if (!response || response.success === false || (response.code && response.code !== 200)) {
this.$message.error(response?.msg || '上传失败');
this.uploadDialogFileList = (fileList || []).filter(item => item.uid !== file.uid);
return;
}
const normalized = this.normalizeUploadFile(file, response);
this.uploadDialogFileList = fileList || [];
this.uploadDialogFiles = [
...this.uploadDialogFiles.filter(item => String(item.uid) !== String(file.uid)),
normalized,
];
this.uploadDialogType = resolveContractAttachmentType({
...normalized,
fileType: '',
});
this.$message.success('上传成功');
},
handleDialogError() {
this.$message.error('上传失败');
},
handleDialogRemove(file) {
this.uploadDialogFiles = this.uploadDialogFiles.filter(
item => String(item.uid) !== String(file.uid)
);
this.uploadDialogFileList = this.uploadDialogFileList.filter(
item => String(item.uid) !== String(file.uid)
);
},
confirmUploadDialog() {
if (!this.uploadDialogFiles.length) {
this.$message.warning('请先上传附件');
return;
}
if (this.attachmentType && !this.uploadDialogType) {
this.$message.warning('请选择附件类型');
return;
}
const uploadUserName = this.$store.getters.userInfo?.realName || '';
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
const appended = this.uploadDialogFiles.map(row => ({
...row,
fileType: this.attachmentType ? this.uploadDialogType : row.fileType,
description: row.description || '',
uploadUserName: row.uploadUserName || uploadUserName,
uploadTime: row.uploadTime || uploadTime,
...(this.markApprovedOnUpload ? { approved: true } : {}),
}));
this.update([...(this.rows || []), ...appended]);
this.uploadDialogVisible = false;
},
handleChange(rows) {
const uploadUserName = this.$store.getters.userInfo?.realName || '';
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
const existingRows = this.rows || [];
this.update(
(rows || []).map(row => {
const fileUrl = attachmentUrl(row);
const existing = existingRows.find(item => {
const sameUid = row.uid && item.uid && String(row.uid) === String(item.uid);
const sameUrl = fileUrl && attachmentUrl(item) === fileUrl;
return sameUid || sameUrl;
});
if (existing && this.isRowLocked(existing)) return { ...existing };
const next = {
...existing,
...row,
description: row.description || existing?.description || '',
uploadUserName: existing?.uploadUserName || row.uploadUserName || uploadUserName,
uploadTime: existing?.uploadTime || row.uploadTime || uploadTime,
approved: existing?.approved || row.approved || false,
};
if (this.attachmentType) {
next.fileType = resolveContractAttachmentType({
...next,
fileType: existing?.fileType || row.fileType,
});
}
if (this.markApprovedOnUpload && !existing) next.approved = true;
return next;
})
);
},
remove(index) {
const row = this.rows[index];
if (this.isRowLocked(row)) {
this.$message.warning('已审核通过的附件不允许删除');
return;
}
const rows = [...this.rows];
rows.splice(index, 1);
this.update(rows);
},
download(row) {
const url = attachmentUrl(row);
if (!url) {
this.$message.warning('附件地址为空无法下载');
return;
}
downloadFileByUrl(url, attachmentName(row));
},
batchDownload() {
(this.selected.length ? this.selected : this.rows).forEach(this.download);
},
formatSize(size) {
const value = Number(size);
if (!value) return '';
if (value < 1024) return `${value}B`;
if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)}KB`;
return `${(value / 1024 / 1024).toFixed(1)}MB`;
},
},
};
</script>
<style lang="scss" scoped>
.contract-manage-form__attachment-tip {
margin-left: 30px;
color: #909399;
font-size: 13px;
line-height: 1.4;
}
.contract-attachment-upload-dialog {
&__body {
padding: 4px 8px 0;
}
&__field {
display: flex;
align-items: center;
margin-bottom: 16px;
}
&__label {
flex: none;
width: 72px;
color: #606266;
text-align: right;
margin-right: 12px;
}
&__uploader {
margin-top: 8px;
:deep(.el-upload) {
width: 100%;
}
:deep(.el-upload-dragger) {
width: 100%;
height: 180px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
}
&__footer {
display: flex;
justify-content: center;
gap: 12px;
}
}
</style>
@@ -4287,7 +4287,7 @@ export default {
if (!row) return '-'; if (!row) return '-';
if (prop === 'businessStatus') return this.displayStatus(row, prop); if (prop === 'businessStatus') return this.displayStatus(row, prop);
if (prop === 'feeGenerationMode') { if (prop === 'feeGenerationMode') {
return row[prop] === 'manual' ? '手动生成' : '系统生成'; return row[prop] === 'manual' ? '账单导入生成' : '系统生成';
} }
const column = this.findColumn(this.option.column, prop); const column = this.findColumn(this.option.column, prop);
if (column?.formatter) return column.formatter(row, {}, row[prop]); if (column?.formatter) return column.formatter(row, {}, row[prop]);
+39 -9
View File
@@ -25,6 +25,10 @@
<el-form-item label="是否需要加盖法人章"><el-select v-model="form.legalSealFlag"><el-option label="是" :value="1" /><el-option label="否" :value="0" /></el-select></el-form-item> <el-form-item label="是否需要加盖法人章"><el-select v-model="form.legalSealFlag"><el-option label="是" :value="1" /><el-option label="否" :value="0" /></el-select></el-form-item>
<el-form-item label="一式(份)"><el-input v-model="form.copyCount" inputmode="numeric" maxlength="9" placeholder="请输入" @input="value => positiveIntegerInput('copyCount', value)" /></el-form-item> <el-form-item label="一式(份)"><el-input v-model="form.copyCount" inputmode="numeric" maxlength="9" placeholder="请输入" @input="value => positiveIntegerInput('copyCount', value)" /></el-form-item>
<el-form-item label="回款账期"><el-input v-model="form.paymentDays" inputmode="numeric" maxlength="9" placeholder="请输入" @input="value => positiveIntegerInput('paymentDays', value)" /></el-form-item> <el-form-item label="回款账期"><el-input v-model="form.paymentDays" inputmode="numeric" maxlength="9" placeholder="请输入" @input="value => positiveIntegerInput('paymentDays', value)" /></el-form-item>
<el-form-item label="合同金额"><el-input-number v-model="form.contractAmount" :min="0" :precision="2" :controls="false" placeholder="请输入" /></el-form-item>
<el-form-item label="是否范本"><el-select v-model="form.templateFlag" placeholder="请选择"><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-form-item>
<el-form-item label="是否电子章"><el-select v-model="form.electronicSealFlag" placeholder="请选择"><el-option label="否" :value="0" /><el-option label="是" :value="1" /></el-select></el-form-item>
</div> </div>
<el-form-item label="备注" class="contract-basic-section__remark"><el-input v-model="form.remark" type="textarea" :rows="2" maxlength="2000" show-word-limit placeholder="请输入备注" /></el-form-item> <el-form-item label="备注" class="contract-basic-section__remark"><el-input v-model="form.remark" type="textarea" :rows="2" maxlength="2000" show-word-limit placeholder="请输入备注" /></el-form-item>
</section> </section>
@@ -42,7 +46,7 @@
<section class="change-section"> <section class="change-section">
<div class="section-head"><div class="dialog-section-title">计费信息</div><el-button type="primary" plain @click="addPlan">添加</el-button></div> <div class="section-head"><div class="dialog-section-title">计费信息</div><el-button 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-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-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>
@@ -65,10 +69,17 @@
<el-form v-if="settlementRule.autoGenerate === 1" :model="settlementRule" label-position="right" label-width="auto" class="settlement-form"> <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-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 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="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="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-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-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>
</section> </section>
<section class="change-section"> <section class="change-section">
@@ -144,15 +155,27 @@ const normalizeOptionalPositiveInteger = value => {
const number = Number(value); const number = Number(value);
return Number.isInteger(number) && number > 0 ? number : null; return Number.isInteger(number) && number > 0 ? number : null;
}; };
const normalizeOptionalAmount = value => {
if (value === undefined || value === null || value === '') return null;
const number = Number(value);
return Number.isFinite(number) && number >= 0 ? Number(number.toFixed(2)) : null;
};
const normalizeCustomPeriods = (periods = []) =>
(periods || []).map(item => ({
startDay: Number(item?.startDay) > 0 ? Number(item.startDay) : '',
endDay: Number(item?.endDay) > 0 ? Number(item.endDay) : '',
}));
export default { export default {
components: { BillingPlanEditor, ElImageViewer, OpenFileViewer }, components: { BillingPlanEditor, ElImageViewer, OpenFileViewer },
data() { return { form: {}, period: [], plans: [], attachments: [], contractFiles: [], contractFileRows: [], selectedContractFiles: [], paymentRatioRows: [], settlementConfigTab: 'pre', changeMaterials: [], imagePreviewVisible: false, imagePreviewUrls: [], imagePreviewIndex: 0, documentPreviewVisible: false, previewFile: {}, viewerPlugins, viewerToolbar: { zoom: true, rotate: true, download: true, fullscreen: true }, feeGenerationMode: 'system', settlementRule: { autoGenerate: 1, settlementType: '月结', billCycleType: '固定截单日', billCutoffDay: 25, cycleDays: '' }, preSettlementConfig: {}, formalSettlementConfig: {}, settlementTypeOptions: ['月结','日结','周结','半月结','固定天数周期结算'], billCycleTypeOptions: ['固定截单日','自然月'], billCutoffDayOptions: Array.from({ length: 31 }, (_, index) => ({ label: `${index + 1}`, value: index + 1 })), cycleDayOptions: [7,15,30,60].map(value => ({ label: `${value}`, value })), planDialogVisible: false, planEditor: { planName: '', defaultPlan: false, remark: '', rules: [] }, planEditorIndex: -1, billingElements: ['按重量','按体积','按车辆','按里程','按吨·公里','固定金额(整单一口价)','按数量'], attachmentFileTypes: ['pdf','bmp','jpeg','png','jpg','doc','docx','ppt','pptx','xlsx','xls','eml','msg','zip'], rules: { contractName: [{ required: true, message: '请输入合同名称', trigger: 'blur' }], changeReason: [{ required: true, message: '请输入变更原因', trigger: 'blur' }] } }; }, data() { return { form: {}, period: [], plans: [], attachments: [], contractFiles: [], contractFileRows: [], selectedContractFiles: [], paymentRatioRows: [], settlementConfigTab: 'pre', changeMaterials: [], imagePreviewVisible: false, imagePreviewUrls: [], imagePreviewIndex: 0, documentPreviewVisible: false, previewFile: {}, viewerPlugins, viewerToolbar: { zoom: true, rotate: true, download: true, fullscreen: true }, feeGenerationMode: 'system', settlementRule: { autoGenerate: '', billStartDate: '', settlementType: '', billCycleType: '', billCutoffDay: '', cycleDays: '', customPeriods: [] }, preSettlementConfig: {}, formalSettlementConfig: {}, settlementTypeOptions: ['月结','日结','周结','半月结','固定天数周期结算'], billCycleTypeOptions: ['固定截单日','自然月','自定义多周期'], billCutoffDayOptions: Array.from({ length: 31 }, (_, index) => ({ label: `${index + 1}日`, value: index + 1 })), cycleDayOptions: [7,15,30,60].map(value => ({ label: `${value}天`, value })), planDialogVisible: false, planEditor: { planName: '', defaultPlan: false, remark: '', rules: [] }, planEditorIndex: -1, billingElements: ['按重量','按体积','按车辆','按里程','按吨·公里','固定金额(整单一口价)','按数量'], attachmentFileTypes: ['pdf','bmp','jpeg','png','jpg','doc','docx','ppt','pptx','xlsx','xls','eml','msg','zip'], rules: { contractName: [{ required: true, message: '请输入合同名称', trigger: 'blur' }], changeReason: [{ required: true, message: '请输入变更原因', trigger: 'blur' }] } }; },
computed: { showSettlementBillCycleType() { return this.settlementRule.settlementType === '月结'; }, showSettlementBillCutoffDay() { return this.showSettlementBillCycleType && this.settlementRule.billCycleType === '固定截单日'; }, showSettlementCycleDays() { return this.settlementRule.settlementType === '固定天数周期结算'; } }, computed: { showSettlementBillCycleType() { return this.settlementRule.settlementType === '月结'; }, showSettlementBillCutoffDay() { return this.showSettlementBillCycleType && this.settlementRule.billCycleType === '固定截单日'; }, showSettlementCustomPeriods() { return this.showSettlementBillCycleType && this.settlementRule.billCycleType === '自定义多周期'; }, showSettlementCycleDays() { return this.settlementRule.settlementType === '固定天数周期结算'; } },
mounted() { this.load(); }, mounted() { this.load(); },
watch: { settlementConfigTab(tab, oldTab) { if (tab === oldTab) return; if (oldTab === 'pre') this.preSettlementConfig = { ...this.settlementRule }; else this.formalSettlementConfig = { ...this.settlementRule }; this.settlementRule = { ...(tab === 'pre' ? this.preSettlementConfig : this.formalSettlementConfig) }; } }, watch: { settlementConfigTab(tab, oldTab) { if (tab === oldTab) return; if (oldTab === 'pre') this.preSettlementConfig = { ...this.settlementRule }; else this.formalSettlementConfig = { ...this.settlementRule }; this.settlementRule = { ...(tab === 'pre' ? this.preSettlementConfig : this.formalSettlementConfig) }; } },
methods: { methods: {
async load() { const id = this.$route.query.id; if (!id) return; const res = await api.getDetail(id); const data = res.data?.data || res.data || {}; this.form = { ...data, changeContent: '', changeReason: '', changeAttachmentsJson: '', copyCount: normalizeOptionalPositiveInteger(data.copyCount), paymentDays: normalizeOptionalPositiveInteger(data.paymentDays), changeType: '合同信息变更' }; this.changeMaterials = []; this.period = data.startDate && data.endDate ? [data.startDate, data.endDate] : []; this.plans = this.parse(data.billingPlanJson); this.attachments = this.mergeChangeAttachments(this.parse(data.attachmentsJson), this.parse(data.changeAttachmentsJson)); this.contractFileRows = this.parse(data.contractFileJson); const rules = this.parseObject(data.settlementRuleJson); const pre = this.parseObject(data.preSettlementConfigJson); const formal = this.parseObject(data.formalSettlementConfigJson); const legacy = Object.keys(rules).some(key => !['preSettlementConfig', 'formalSettlementConfig'].includes(key)) ? rules : {}; this.preSettlementConfig = rules.preSettlementConfig || (Object.keys(pre).length ? pre : legacy); this.formalSettlementConfig = rules.formalSettlementConfig || (Object.keys(formal).length ? formal : legacy); this.settlementRule = { ...this.preSettlementConfig }; this.feeGenerationMode = data.feeGenerationMode || (Number(data.billingEnabled) === 0 ? 'manual' : 'system'); this.paymentRatioRows = this.parse(data.paymentRatioJson); }, async load() { const id = this.$route.query.id; if (!id) return; const res = await api.getDetail(id); const data = res.data?.data || res.data || {}; this.form = { ...data, changeContent: '', changeReason: '', changeAttachmentsJson: '', copyCount: normalizeOptionalPositiveInteger(data.copyCount), paymentDays: normalizeOptionalPositiveInteger(data.paymentDays), contractAmount: normalizeOptionalAmount(data.contractAmount), templateFlag: data.templateFlag ?? 0, electronicSealFlag: data.electronicSealFlag ?? 0, changeType: '合同信息变更' }; this.changeMaterials = []; this.period = data.startDate && data.endDate ? [data.startDate, data.endDate] : []; this.plans = this.parse(data.billingPlanJson); this.attachments = this.mergeChangeAttachments(this.parse(data.attachmentsJson), this.parse(data.changeAttachmentsJson)); this.contractFileRows = this.parse(data.contractFileJson); const rules = this.parseObject(data.settlementRuleJson); const pre = this.parseObject(data.preSettlementConfigJson); const formal = this.parseObject(data.formalSettlementConfigJson); const legacy = Object.keys(rules).some(key => !['preSettlementConfig', 'formalSettlementConfig'].includes(key)) ? rules : {}; const normalizeRule = rule => { const next = { autoGenerate: '', billStartDate: '', settlementType: '', billCycleType: '', billCutoffDay: '', cycleDays: '', customPeriods: [], ...(rule || {}) }; if (next.settlementType === '月结' && next.billCycleType === '自定义多周期') next.customPeriods = normalizeCustomPeriods(Array.isArray(next.customPeriods) ? next.customPeriods : []); else next.customPeriods = []; return next; }; this.preSettlementConfig = normalizeRule(rules.preSettlementConfig || (Object.keys(pre).length ? pre : legacy)); this.formalSettlementConfig = normalizeRule(rules.formalSettlementConfig || (Object.keys(formal).length ? formal : legacy)); this.settlementRule = { ...this.preSettlementConfig }; this.feeGenerationMode = data.feeGenerationMode || (Number(data.billingEnabled) === 0 ? 'manual' : 'system'); this.paymentRatioRows = this.parse(data.paymentRatioJson); },
parse(value) { try { const result = JSON.parse(value || '[]'); return Array.isArray(result) ? result : []; } catch { return []; } }, parse(value) { try { const result = JSON.parse(value || '[]'); return Array.isArray(result) ? result : []; } catch { return []; } },
// 审核通过后上一次的变更材料归集到「其它附件」,按文件地址/文件名去重,避免重复展示 // 审核通过后上一次的变更材料归集到「其它附件」,按文件地址/文件名去重,避免重复展示
mergeChangeAttachments(attachments = [], changeAttachments = []) { mergeChangeAttachments(attachments = [], changeAttachments = []) {
@@ -163,14 +186,14 @@ export default {
.map(item => ({ ...item, size: /^\d+$/.test(String(item.size ?? '')) ? this.formatFileSize(item.size) : item.size })); .map(item => ({ ...item, size: /^\d+$/.test(String(item.size ?? '')) ? this.formatFileSize(item.size) : item.size }));
return [...attachments, ...merged]; return [...attachments, ...merged];
}, },
parseObject(value) { try { return { autoGenerate: 1, settlementType: '月结', billCycleType: '固定截单日', billCutoffDay: 25, cycleDays: 15, ...(JSON.parse(value || '{}') || {}) }; } catch { return { autoGenerate: 1, settlementType: '月结', billCycleType: '固定截单日', billCutoffDay: 25, cycleDays: 15 }; } }, parseObject(value) { try { return { autoGenerate: '', billStartDate: '', settlementType: '', billCycleType: '', billCutoffDay: '', cycleDays: '', customPeriods: [], ...(JSON.parse(value || '{}') || {}) }; } catch { return { autoGenerate: '', billStartDate: '', settlementType: '', billCycleType: '', billCutoffDay: '', cycleDays: '', customPeriods: [] }; } },
positiveIntegerInput(prop, value) { this.form[prop] = String(value ?? '').replace(/\D/g, '').replace(/^0+/, ''); }, positiveIntegerInput(prop, value) { this.form[prop] = String(value ?? '').replace(/\D/g, '').replace(/^0+/, ''); },
addPlan() { this.planEditorIndex = -1; this.planEditor = { planName: `计费方案${this.plans.length + 1}`, defaultPlan: !this.plans.length, remark: '', rules: [{}] }; this.planDialogVisible = true; }, addPlan() { this.planEditorIndex = -1; this.planEditor = { planName: `计费方案${this.plans.length + 1}`, defaultPlan: !this.plans.length, remark: '', rules: [{}] }; this.planDialogVisible = true; },
editPlan(row, index) { this.planEditorIndex = index; this.planEditor = JSON.parse(JSON.stringify(row)); this.planDialogVisible = true; }, editPlan(row, index) { this.planEditorIndex = index; this.planEditor = JSON.parse(JSON.stringify(row)); this.planDialogVisible = true; },
toggleDefaultPlan(value) { if (value) this.plans.forEach(item => { item.defaultPlan = false; }); }, toggleDefaultPlan(value) { if (value) this.plans.forEach(item => { item.defaultPlan = false; }); },
savePlan(value, index) { if (index < 0) this.plans.push(value); else this.plans.splice(index, 1, value); if (value.defaultPlan) this.plans.forEach((item, current) => { if (current !== (index < 0 ? this.plans.length - 1 : index)) item.defaultPlan = false; }); }, savePlan(value, index) { if (index < 0) this.plans.push(value); else this.plans.splice(index, 1, value); if (value.defaultPlan) this.plans.forEach((item, current) => { if (current !== (index < 0 ? this.plans.length - 1 : index)) item.defaultPlan = false; }); },
handleSettlementTypeChange(value) { if (value !== '月结') { this.settlementRule.billCycleType = ''; this.settlementRule.billCutoffDay = ''; } else if (!this.settlementRule.billCycleType) this.settlementRule.billCycleType = '固定截单日'; if (value !== '固定天数周期结算') this.settlementRule.cycleDays = ''; }, handleSettlementTypeChange(value) { if (value !== '月结') { this.settlementRule.billCycleType = ''; this.settlementRule.billCutoffDay = ''; this.settlementRule.customPeriods = []; } else if (this.settlementRule.billCycleType === '自定义多周期') this.settlementRule.customPeriods = normalizeCustomPeriods(this.settlementRule.customPeriods || []); else this.settlementRule.customPeriods = []; if (value !== '固定天数周期结算') this.settlementRule.cycleDays = ''; },
handleCycleTypeChange(value) { if (value === '固定截单日' && !this.settlementRule.billCutoffDay) this.settlementRule.billCutoffDay = 25; if (value !== '固定截单日') this.settlementRule.billCutoffDay = ''; }, handleCycleTypeChange(value) { if (value !== '固定截单日') this.settlementRule.billCutoffDay = ''; if (value === '自定义多周期') this.settlementRule.customPeriods = normalizeCustomPeriods(this.settlementRule.customPeriods || []); else this.settlementRule.customPeriods = []; },
currentUploadUserName() { currentUploadUserName() {
const userInfo = this.$store.getters.userInfo || {}; const userInfo = this.$store.getters.userInfo || {};
return userInfo.realName || userInfo.userName || ''; return userInfo.realName || userInfo.userName || '';
@@ -209,7 +232,14 @@ export default {
handleContractFileBatchDownload() { (this.selectedContractFiles.length ? this.selectedContractFiles : this.contractFileRows).forEach(row => { if (row.url) window.open(row.url, '_blank'); }); }, handleContractFileBatchDownload() { (this.selectedContractFiles.length ? this.selectedContractFiles : this.contractFileRows).forEach(row => { if (row.url) window.open(row.url, '_blank'); }); },
formatFileSize(value) { const size = Number(value || 0); return size > 1024 * 1024 ? `${(size / 1024 / 1024).toFixed(2)}MB` : `${Math.max(1, Math.ceil(size / 1024))}KB`; }, formatFileSize(value) { const size = Number(value || 0); return size > 1024 * 1024 ? `${(size / 1024 / 1024).toFixed(2)}MB` : `${Math.max(1, Math.ceil(size / 1024))}KB`; },
addPaymentRatioRow() { this.paymentRatioRows.push({ paymentTerm: `第${this.paymentRatioRows.length + 1}笔`, ratioLimit: '', remark: '' }); }, addPaymentRatioRow() { this.paymentRatioRows.push({ paymentTerm: `第${this.paymentRatioRows.length + 1}笔`, ratioLimit: '', remark: '' }); },
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; } 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), paymentDays: normalizeOptionalPositiveInteger(this.form.paymentDays), 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) }); this.$message.success('变更已提交'); this.$router.back(); }, customPeriodEndDayOptions(row) { const startDay = Number(row?.startDay); if (!Number.isFinite(startDay) || startDay < 1) return this.billCutoffDayOptions; return Array.from({ length: 31 - startDay + 1 }, (_, index) => ({ label: `${startDay + index}日`, value: startDay + index })); },
handleCustomPeriodStartDayChange(index, value) { const periods = normalizeCustomPeriods(this.settlementRule.customPeriods || []); const row = periods[index]; if (!row) return; row.startDay = value || ''; const startDay = Number(row.startDay); const endDay = Number(row.endDay); if (Number.isFinite(startDay) && Number.isFinite(endDay) && (endDay < startDay || endDay > 31)) row.endDay = ''; this.settlementRule.customPeriods = periods; },
handleCustomPeriodEndDayChange(index, value) { const periods = normalizeCustomPeriods(this.settlementRule.customPeriods || []); const row = periods[index]; if (!row) return; row.endDay = value || ''; this.settlementRule.customPeriods = periods; },
addCustomPeriodRow() { const periods = normalizeCustomPeriods(this.settlementRule.customPeriods || []); periods.push({ startDay: '', endDay: '' }); this.settlementRule.customPeriods = 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; },
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), 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) }); this.$message.success('变更已提交'); this.$router.back(); },
}, },
}; };
</script> </script>
File diff suppressed because it is too large Load Diff