b9c367e028
Reviewed-on: #9
1346 lines
57 KiB
Vue
1346 lines
57 KiB
Vue
<template>
|
||
<basic-container class="contract-change-page">
|
||
<div class="archive-page-form__title">{{ pageTitle }}</div>
|
||
<el-form
|
||
ref="formRef"
|
||
:model="form"
|
||
:rules="rules"
|
||
:disabled="pageReadonly"
|
||
label-position="right"
|
||
label-width="auto"
|
||
class="contract-change-form"
|
||
>
|
||
<section class="change-section contract-basic-section">
|
||
<div class="dialog-section-title">基本信息</div>
|
||
<div class="contract-basic-section__grid">
|
||
<el-form-item label="变更类型" class="contract-basic-section__change-type">
|
||
<el-radio-group v-model="form.changeType" :disabled="pageReadonly">
|
||
<el-radio label="合同信息变更" />
|
||
<el-radio label="结算规则变更" />
|
||
<el-radio label="终止合同" />
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
<el-form-item label="签约类型" prop="signType">
|
||
<el-select
|
||
v-model="form.signType"
|
||
placeholder="请选择签约类型"
|
||
clearable
|
||
:disabled="isSettlementRuleChange"
|
||
>
|
||
<el-option
|
||
v-for="item in signTypeOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="合同编号" prop="contractNo">
|
||
<el-input
|
||
v-model="form.contractNo"
|
||
maxlength="100"
|
||
clearable
|
||
placeholder="请输入合同编号"
|
||
:disabled="isSettlementRuleChange"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="合同名称" prop="contractName">
|
||
<el-input
|
||
v-model="form.contractName"
|
||
maxlength="100"
|
||
show-word-limit
|
||
placeholder="请输入合同名称"
|
||
:disabled="isSettlementRuleChange"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="合同类型">
|
||
<el-select v-model="form.contractCategory" disabled>
|
||
<el-option
|
||
v-for="item in contractCategoryOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="项目">
|
||
<el-input v-model="form.projectName" disabled />
|
||
</el-form-item>
|
||
<el-form-item label="合同负责部门">
|
||
<el-input v-model="form.organizationName" disabled />
|
||
</el-form-item>
|
||
<el-form-item label="甲方">
|
||
<el-input v-model="form.partyA" disabled />
|
||
</el-form-item>
|
||
<el-form-item label="乙方">
|
||
<el-input v-model="form.partyB" disabled />
|
||
</el-form-item>
|
||
<el-form-item label="签订日期">
|
||
<el-date-picker
|
||
v-model="form.signDate"
|
||
type="date"
|
||
format="YYYY-MM-DD"
|
||
value-format="YYYY-MM-DD"
|
||
disabled
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="申请日期">
|
||
<el-date-picker
|
||
v-model="form.applyDate"
|
||
type="date"
|
||
format="YYYY-MM-DD"
|
||
value-format="YYYY-MM-DD"
|
||
disabled
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="合同期限">
|
||
<div class="contract-basic-section__date-range">
|
||
<el-date-picker
|
||
v-model="period[0]"
|
||
type="date"
|
||
format="YYYY-MM-DD"
|
||
value-format="YYYY-MM-DD"
|
||
placeholder="请输入"
|
||
:disabled="isSettlementRuleChange"
|
||
/>
|
||
<span>至</span>
|
||
<el-date-picker
|
||
v-model="period[1]"
|
||
type="date"
|
||
format="YYYY-MM-DD"
|
||
value-format="YYYY-MM-DD"
|
||
placeholder="请输入"
|
||
:disabled="isSettlementRuleChange"
|
||
/>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="回款账期">
|
||
<el-input
|
||
v-model="form.paymentDays"
|
||
placeholder="请输入"
|
||
:disabled="isSettlementRuleChange"
|
||
@input="value => positiveIntegerInput('paymentDays', value)"
|
||
>
|
||
<template #suffix>天</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
<el-form-item label="合同金额" prop="contractAmount">
|
||
<el-input
|
||
:model-value="contractAmountDisplay"
|
||
placeholder="请输入"
|
||
clearable
|
||
:disabled="isSettlementRuleChange"
|
||
@input="handleContractAmountInput"
|
||
@blur="formatContractAmountField"
|
||
@clear="form.contractAmount = null"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="是否范本">
|
||
<el-select
|
||
v-model="form.templateFlag"
|
||
placeholder="请选择"
|
||
:disabled="isSettlementRuleChange"
|
||
>
|
||
<el-option label="否" :value="0" />
|
||
<el-option label="是" :value="1" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="原件合同编号">
|
||
<el-input
|
||
v-model="form.originalContractNo"
|
||
maxlength="100"
|
||
placeholder="请输入"
|
||
:disabled="isSettlementRuleChange"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="是否电子章">
|
||
<el-select
|
||
v-model="form.electronicSealFlag"
|
||
placeholder="请选择"
|
||
:disabled="isSettlementRuleChange"
|
||
>
|
||
<el-option label="否" :value="0" />
|
||
<el-option label="是" :value="1" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="开票周期">
|
||
<el-input
|
||
v-model="form.invoiceCycle"
|
||
placeholder="请输入"
|
||
:disabled="isSettlementRuleChange"
|
||
@input="value => positiveIntegerInput('invoiceCycle', value)"
|
||
>
|
||
<template #suffix>天</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
<el-form-item label="结算币种" prop="settlementCurrency">
|
||
<el-select
|
||
v-model="form.settlementCurrency"
|
||
placeholder="请选择结算币种"
|
||
clearable
|
||
:disabled="isSettlementRuleChange"
|
||
>
|
||
<el-option
|
||
v-for="item in settlementCurrencyOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="合同格式">
|
||
<el-select
|
||
v-model="form.contractFormat"
|
||
placeholder="请选择合同格式"
|
||
clearable
|
||
:disabled="isSettlementRuleChange"
|
||
>
|
||
<el-option
|
||
v-for="item in contractFormatOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="一式">
|
||
<el-input
|
||
v-model="form.copyCount"
|
||
placeholder="请输入"
|
||
:disabled="isSettlementRuleChange"
|
||
@input="value => positiveIntegerInput('copyCount', value)"
|
||
>
|
||
<template #suffix>份</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
<el-form-item label="是否需要加盖法人章">
|
||
<el-select
|
||
v-model="form.legalSealFlag"
|
||
placeholder="请选择"
|
||
clearable
|
||
:disabled="isSettlementRuleChange"
|
||
>
|
||
<el-option label="否" :value="0" />
|
||
<el-option label="是" :value="1" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="结算方式">
|
||
<el-select
|
||
v-model="form.settlementMode"
|
||
placeholder="请选择结算方式"
|
||
clearable
|
||
:disabled="isSettlementRuleChange"
|
||
>
|
||
<el-option
|
||
v-for="item in settlementModeOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="经办人">
|
||
<el-input v-model="form.handlerUserName" disabled />
|
||
</el-form-item>
|
||
</div>
|
||
<el-form-item label="合同主要内容" class="contract-basic-section__remark">
|
||
<el-input
|
||
v-model="form.mainContent"
|
||
type="textarea"
|
||
:rows="4"
|
||
maxlength="1000"
|
||
show-word-limit
|
||
placeholder="请输入合同主要内容"
|
||
:disabled="isSettlementRuleChange"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="其他事项说明" class="contract-basic-section__remark">
|
||
<el-input
|
||
v-model="form.remark"
|
||
type="textarea"
|
||
:rows="2"
|
||
maxlength="200"
|
||
show-word-limit
|
||
placeholder="请输入其他事项说明"
|
||
:disabled="isSettlementRuleChange"
|
||
/>
|
||
</el-form-item>
|
||
<!-- 风险客商提示:合同已推送合规并留痕高风险/黑名单时展示,变更提交同样要求特批附件 -->
|
||
<div v-if="riskTipText" class="contract-manage-form__risk-tip">
|
||
<div class="contract-manage-form__risk-tip-title">风险客商提示:</div>
|
||
<div class="contract-manage-form__risk-tip-content">{{ riskTipText }}</div>
|
||
</div>
|
||
</section>
|
||
|
||
<contract-attachment-section
|
||
title="用印材料"
|
||
description
|
||
attachment-type
|
||
:readonly="isSettlementRuleChange"
|
||
:attachment-location-options="changeAttachmentLocationOptions"
|
||
:rows="contractFileRows"
|
||
:preview="previewAttachment"
|
||
@update:rows="contractFileRows = $event"
|
||
@upload-to-location="handleAttachmentUploadToLocation"
|
||
/>
|
||
|
||
<section class="change-section">
|
||
<div class="section-head">
|
||
<div class="dialog-section-title">计费信息</div>
|
||
<el-button
|
||
v-if="!pageReadonly && feeGenerationMode !== 'manual'"
|
||
type="primary"
|
||
plain
|
||
@click="addPlan"
|
||
>添加</el-button
|
||
>
|
||
</div>
|
||
<el-radio-group v-model="feeGenerationMode"
|
||
><el-radio label="system">系统生成</el-radio
|
||
><el-radio label="manual">账单导入生成</el-radio></el-radio-group
|
||
>
|
||
<el-table :data="plans" border class="change-table">
|
||
<el-table-column type="index" label="序号" width="70" />
|
||
<el-table-column prop="planName" label="方案名称" min-width="180" />
|
||
<el-table-column label="适用客商" min-width="180">
|
||
<template #default="{ row }">
|
||
{{ row.customerName || billingPlanCustomerLabel(row) || '-' }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="remark" label="备注" min-width="200" />
|
||
<el-table-column v-if="!pageReadonly" 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-column v-else label="操作" width="100">
|
||
<template #default="{ row, $index }">
|
||
<el-link type="primary" @click="editPlan(row, $index)">查看</el-link>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</section>
|
||
|
||
<section class="change-section">
|
||
<div class="dialog-section-title">结算单规则</div>
|
||
<el-tabs v-model="settlementConfigTab">
|
||
<el-tab-pane label="预结算配置" name="pre" />
|
||
<el-tab-pane label="正式结算配置" name="formal" />
|
||
</el-tabs>
|
||
<div class="settlement-switch">
|
||
<span>自动生成结算单</span>
|
||
<el-radio-group v-model="settlementRule.autoGenerate">
|
||
<el-radio :label="1">开启</el-radio>
|
||
<el-tooltip content="开启时,系统根据配置规则归集运单,定时生成结算单" placement="top">
|
||
<el-icon class="settlement-switch-tip"><QuestionFilled /></el-icon>
|
||
</el-tooltip>
|
||
<el-radio :label="0">关闭</el-radio>
|
||
</el-radio-group>
|
||
</div>
|
||
<el-form
|
||
v-if="settlementRule.autoGenerate === 1"
|
||
:model="settlementRule"
|
||
label-position="right"
|
||
label-width="auto"
|
||
class="settlement-form"
|
||
>
|
||
<el-form-item label="账单起始日" required
|
||
><el-date-picker
|
||
v-model="settlementRule.billStartDate"
|
||
type="date"
|
||
placeholder="请选择账单起始日"
|
||
format="YYYY-MM-DD"
|
||
value-format="YYYY-MM-DD"
|
||
/></el-form-item>
|
||
<el-form-item label="结算类型" required
|
||
><el-select
|
||
v-model="settlementRule.settlementType"
|
||
placeholder="请选择结算类型"
|
||
@change="handleSettlementTypeChange"
|
||
><el-option
|
||
v-for="item in settlementTypeOptions"
|
||
:key="item"
|
||
:label="item"
|
||
:value="item"
|
||
/></el-select></el-form-item>
|
||
<el-form-item v-if="showSettlementBillCycleType" label="结算周期" required
|
||
><el-select
|
||
v-model="settlementRule.billCycleType"
|
||
placeholder="请选择结算周期"
|
||
@change="handleCycleTypeChange"
|
||
><el-option
|
||
v-for="item in billCycleTypeOptions"
|
||
:key="item"
|
||
:label="item"
|
||
:value="item"
|
||
/></el-select></el-form-item>
|
||
<el-form-item v-if="showSettlementBillCutoffDay" label="账单截单日" required
|
||
><el-select v-model="settlementRule.billCutoffDay" placeholder="请选择账单截单日"
|
||
><el-option
|
||
v-for="item in billCutoffDayOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/></el-select></el-form-item>
|
||
<el-form-item v-if="showSettlementCycleDays" label="周期天数" required
|
||
><el-select v-model="settlementRule.cycleDays" placeholder="请选择周期天数"
|
||
><el-option
|
||
v-for="item in cycleDayOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/></el-select></el-form-item>
|
||
</el-form>
|
||
<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 v-if="!pageReadonly" 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 v-if="!pageReadonly" type="primary" @click="addCustomPeriodRow">添加</el-link></template
|
||
>
|
||
</el-table>
|
||
</section>
|
||
|
||
<section class="change-section">
|
||
<div class="dialog-section-title">
|
||
付款比例设置
|
||
<el-tooltip content="非必填;配置付款比例时,合计必须等于100%。" placement="top">
|
||
<el-icon style="margin-left: 4px; color: #909399; cursor: pointer"
|
||
><el-icon-question-filled
|
||
/></el-icon>
|
||
</el-tooltip>
|
||
</div>
|
||
<el-table :data="paymentRatioRows" border class="change-table">
|
||
<el-table-column type="index" label="序号" width="70" />
|
||
<el-table-column prop="paymentTerm" label="付款笔数" min-width="180" />
|
||
<el-table-column label="付款比例上限(%)" min-width="220"
|
||
><template #default="{ row }"
|
||
><el-input-number
|
||
v-model="row.ratioLimit"
|
||
:min="0"
|
||
:max="100"
|
||
:precision="2"
|
||
controls-position="right"
|
||
: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="!pageReadonly && !isSettlementRuleChange" label="操作" width="100"
|
||
><template #default="{ $index }"
|
||
><el-link type="danger" @click="paymentRatioRows.splice($index, 1)"
|
||
>删除</el-link
|
||
></template
|
||
></el-table-column
|
||
>
|
||
</el-table>
|
||
<el-button
|
||
v-if="!pageReadonly && !isSettlementRuleChange"
|
||
type="primary"
|
||
plain
|
||
@click="addPaymentRatioRow"
|
||
>添加</el-button
|
||
>
|
||
</section>
|
||
|
||
<billing-plan-editor
|
||
v-model="planDialogVisible"
|
||
:value="planEditor"
|
||
:index="planEditorIndex"
|
||
:readonly="pageReadonly"
|
||
:settlement-currency="form.settlementCurrency"
|
||
:party-options="billingPartyOptions"
|
||
@save="savePlan"
|
||
/>
|
||
|
||
<contract-attachment-section
|
||
title="附件(佐证材料等)"
|
||
description
|
||
attachment-type
|
||
attachment-location="附件(佐证材料等)"
|
||
attachment-type-dict-code="contract_supporting_attachment_type"
|
||
:readonly="isSettlementRuleChange"
|
||
:attachment-location-options="changeAttachmentLocationOptions"
|
||
:rows="attachments"
|
||
:preview="previewAttachment"
|
||
@update:rows="attachments = $event"
|
||
@upload-to-location="handleAttachmentUploadToLocation"
|
||
/>
|
||
|
||
<section class="change-section change-reason-section">
|
||
<div class="dialog-section-title">变更原因</div>
|
||
<el-form-item prop="changeReason" class="change-reason-section__input">
|
||
<el-input
|
||
v-model="form.changeReason"
|
||
type="textarea"
|
||
:rows="3"
|
||
maxlength="500"
|
||
show-word-limit
|
||
placeholder="请输入变更原因"
|
||
/>
|
||
</el-form-item>
|
||
</section>
|
||
|
||
<contract-attachment-section
|
||
title="变更材料"
|
||
description
|
||
attachment-type
|
||
attachment-location="变更材料"
|
||
:attachment-location-options="changeAttachmentLocationOptions"
|
||
:rows="changeMaterials"
|
||
:preview="previewAttachment"
|
||
@update:rows="changeMaterials = $event"
|
||
@upload-to-location="handleAttachmentUploadToLocation"
|
||
/>
|
||
<div v-if="!pageReadonly" class="page-footer">
|
||
<el-button @click="goBack">取消</el-button>
|
||
<el-button v-if="isContractChangeModule" @click="saveDraft">保存草稿</el-button>
|
||
<el-button type="primary" @click="submit">提交</el-button>
|
||
</div>
|
||
</el-form>
|
||
|
||
<el-dialog v-model="documentPreviewVisible" :title="previewFile.name || '附件预览'" append-to-body destroy-on-close width="90%" top="4vh">
|
||
<open-file-viewer v-if="documentPreviewVisible && previewFile.url" :file="previewFile.url" :file-name="previewFile.name" :mime-type="previewFile.mimeType" width="100%" height="72vh" fit="contain" theme="auto" locale="zh-CN" :toolbar="viewerToolbar" :plugins="viewerPlugins" @unsupported="handlePreviewUnsupported" @error="handlePreviewError" />
|
||
</el-dialog>
|
||
<el-image-viewer v-if="imagePreviewVisible" :url-list="imagePreviewUrls" :initial-index="imagePreviewIndex" @close="imagePreviewVisible = false" />
|
||
</basic-container>
|
||
</template>
|
||
|
||
<script>
|
||
import * as contractApi from '@/api/business/contract-manage';
|
||
import * as changeApi from '@/api/business/contract-change';
|
||
import { getMkPublicDetail } from '@/api/mk-process';
|
||
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
|
||
import BillingPlanEditor from './components/billing-plan-editor.vue';
|
||
import ContractAttachmentSection, {
|
||
isContractSealMaterialLocation,
|
||
isContractSupportingAttachmentLocation,
|
||
normalizeContractFileRows,
|
||
} from './components/contract-attachment-section.vue';
|
||
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
|
||
import { getDictionary as getBizDictionary } from '@/api/system/dictbiz';
|
||
import {
|
||
contractCategoryOptions as defaultContractCategoryOptions,
|
||
signTypeOptions as defaultSignTypeOptions,
|
||
} from '@/option/business/common';
|
||
import { ElImageViewer } from 'element-plus';
|
||
import { OpenFileViewer } from '@open-file-viewer/vue';
|
||
import { fallbackPlugin, imagePlugin, officePlugin, pdfPlugin, textPlugin } from '@open-file-viewer/core';
|
||
import '@open-file-viewer/core/style.css';
|
||
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
|
||
|
||
const viewerPlugins = [
|
||
imagePlugin(),
|
||
pdfPlugin({ workerSrc: pdfWorkerSrc, useFetchData: true }),
|
||
officePlugin({ pdf: { workerSrc: pdfWorkerSrc, useFetchData: true } }),
|
||
textPlugin(),
|
||
fallbackPlugin(),
|
||
];
|
||
const defaultSettlementModeOptions = [
|
||
{ label: '现结', value: '现结' },
|
||
{ label: '日结', value: '日结' },
|
||
{ label: '周结', value: '周结' },
|
||
{ label: '月结', value: '月结' },
|
||
{ label: '按固定天数周期', value: '按固定天数周期' },
|
||
];
|
||
const defaultContractFormatOptions = [
|
||
{ label: '电子合同', value: '电子合同' },
|
||
{ label: '纸质合同', value: '纸质合同' },
|
||
];
|
||
const contractChangeListRoute = '/business/contract-change';
|
||
const contractChangeListRefreshKey = 'contract-change-list-refresh';
|
||
const normalizeOptionalPositiveInteger = value => {
|
||
if (value === undefined || value === null || value === '') return null;
|
||
const number = Number(value);
|
||
return Number.isInteger(number) && number > 0 ? number : null;
|
||
};
|
||
const normalizeOptionalAmount = value => {
|
||
if (
|
||
value === undefined ||
|
||
value === null ||
|
||
value === '' ||
|
||
Number(value) === -1 ||
|
||
String(value) === '-1'
|
||
) {
|
||
return null;
|
||
}
|
||
const number = Number(value);
|
||
return Number.isFinite(number) && number >= 0 ? Number(number.toFixed(2)) : null;
|
||
};
|
||
const formatContractAmountInput = (value, emptyValue = null) => {
|
||
if (
|
||
value === undefined ||
|
||
value === null ||
|
||
value === '' ||
|
||
Number(value) === -1 ||
|
||
String(value) === '-1'
|
||
) {
|
||
return emptyValue;
|
||
}
|
||
const number = Number(value);
|
||
return Number.isFinite(number) && number >= 0 ? number.toFixed(2) : emptyValue;
|
||
};
|
||
|
||
|
||
const normalizeCustomPeriods = (periods = []) =>
|
||
(periods || []).map(item => ({
|
||
startDay: Number(item?.startDay) > 0 ? Number(item.startDay) : '',
|
||
endDay: Number(item?.endDay) > 0 ? Number(item.endDay) : '',
|
||
}));
|
||
|
||
const createPartyRow = (row = {}) => ({
|
||
id: row.id || `${Date.now()}${Math.floor(Math.random() * 1000)}`,
|
||
customerId: row.customerId || '',
|
||
customerName: row.customerName || '',
|
||
creditCode: row.creditCode || row.unifiedCreditCode || '',
|
||
contactId: row.contactId || '',
|
||
contactName: row.contactName || '',
|
||
contactPhone: row.contactPhone || '',
|
||
});
|
||
|
||
const partyRoleLabel = role => {
|
||
if (role === 'A') return '甲方';
|
||
if (role === 'B') return '乙方';
|
||
if (role === 'C') return '丙方';
|
||
return role || '';
|
||
};
|
||
|
||
const normalizePartyRows = (partyJson, fallbackName, requireOne) => {
|
||
const rows = (() => {
|
||
if (Array.isArray(partyJson)) return partyJson;
|
||
if (!partyJson) return [];
|
||
try {
|
||
const data = JSON.parse(partyJson);
|
||
return Array.isArray(data) ? data : [];
|
||
} catch (error) {
|
||
return [];
|
||
}
|
||
})().map(item => createPartyRow(item));
|
||
if (rows.length) return rows;
|
||
if (fallbackName) {
|
||
return String(fallbackName)
|
||
.split(/[、,]/)
|
||
.map(name => name.trim())
|
||
.filter(Boolean)
|
||
.map(name => createPartyRow({ customerName: name }));
|
||
}
|
||
return requireOne ? [createPartyRow()] : [];
|
||
};
|
||
|
||
export default {
|
||
components: { BillingPlanEditor, ContractAttachmentSection, ElImageViewer, OpenFileViewer },
|
||
data() {
|
||
return {
|
||
form: {},
|
||
period: [],
|
||
plans: [],
|
||
partyARows: [],
|
||
partyBRows: [],
|
||
partyCRows: [],
|
||
attachments: [],
|
||
contractFiles: [],
|
||
contractFileRows: [],
|
||
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 })),
|
||
signTypeOptions: defaultSignTypeOptions,
|
||
contractCategoryDictOptions: [],
|
||
settlementCurrencyOptions: [],
|
||
settlementModeDictOptions: [],
|
||
contractFormatDictOptions: [],
|
||
planDialogVisible: false,
|
||
planEditor: { planName: '', remark: '', rules: [] },
|
||
planEditorIndex: -1,
|
||
billingElements: [
|
||
'按重量',
|
||
'按体积',
|
||
'按车辆',
|
||
'按里程',
|
||
'按吨·公里',
|
||
'固定金额(整单一口价)',
|
||
'按数量',
|
||
],
|
||
rules: {
|
||
contractNo: [
|
||
{
|
||
validator: (rule, value, callback) => {
|
||
if (this.isSettlementRuleChange) {
|
||
return callback();
|
||
}
|
||
const contractNo = String(value || '').trim();
|
||
if (!contractNo) {
|
||
return callback(new Error('请输入合同编号'));
|
||
}
|
||
if (contractNo.length > 100) {
|
||
return callback(new Error('合同编号不能超过100个字符'));
|
||
}
|
||
callback();
|
||
},
|
||
trigger: 'blur',
|
||
},
|
||
],
|
||
contractName: [{ required: true, message: '请输入合同名称', trigger: 'blur' }],
|
||
contractAmount: [
|
||
{ required: true, message: '请输入合同金额', trigger: 'blur' },
|
||
{
|
||
validator: (rule, value, callback) => {
|
||
if (value === undefined || value === null || value === '') {
|
||
return callback(new Error('请输入合同金额'));
|
||
}
|
||
if (Number(value) < 0) {
|
||
return callback(new Error('合同金额不能小于0'));
|
||
}
|
||
callback();
|
||
},
|
||
trigger: 'blur',
|
||
},
|
||
],
|
||
changeReason: [{ max: 500, message: '变更原因不能超过500个字符', trigger: 'blur' }],
|
||
},
|
||
};
|
||
},
|
||
computed: {
|
||
pageTitle() {
|
||
if (this.pageReadonly) return this.$route.query.name || '查看合同变更';
|
||
return this.$route.query.name || '合同变更';
|
||
},
|
||
isContractChangeModule() {
|
||
return (
|
||
this.$route.path === '/business/contract-change/form' ||
|
||
this.$route.path === '/business/contract-change/public-view'
|
||
);
|
||
},
|
||
isPublicViewPage() {
|
||
return this.$route.path === '/business/contract-change/public-view';
|
||
},
|
||
pageReadonly() {
|
||
return this.isPublicViewPage || this.$route.query.mode === 'view';
|
||
},
|
||
// 高风险/黑名单客商提示:按合同推送合规留痕 hcs_risk_level/hcs_black_status 判定(与合同详情口径一致)
|
||
riskTipText() {
|
||
const riskLevel = String(this.form.hcsRiskLevel || '').trim();
|
||
const blacklist = String(this.form.kingdeeBlackStatus || '').toLowerCase() === 'yes';
|
||
if (riskLevel !== '2' && !blacklist) return '';
|
||
const parties = [this.form.partyA, this.form.partyB]
|
||
.map(name => String(name || '').trim())
|
||
.filter(Boolean)
|
||
.join('、');
|
||
return `【${parties || '相对方'}】:被列入外部黑名单;涉贪污受贿裁判相关提及方(3年以上及其他);被执行人信息;买卖合同纠纷;劳动纠纷;重大纠纷。提交变更前须上传「特批附件」类型附件`;
|
||
},
|
||
isSettlementRuleChange() {
|
||
return this.form.changeType === '结算规则变更';
|
||
},
|
||
/**
|
||
* 计费方案适用客商:客户合同仅甲方;承运商合同仅乙方、丙方
|
||
*/
|
||
billingPartyOptions() {
|
||
const category = String(this.form.contractCategory || '').trim();
|
||
const allowedRoles =
|
||
category === '承运商合同' ? ['B', 'C'] : category === '客户合同' ? ['A'] : ['A', 'B', 'C'];
|
||
return [
|
||
...this.partyARows.map(row => ({ ...row, partyRole: 'A' })),
|
||
...this.partyBRows.map(row => ({ ...row, partyRole: 'B' })),
|
||
...this.partyCRows.map(row => ({ ...row, partyRole: 'C' })),
|
||
]
|
||
.filter(row => allowedRoles.includes(row.partyRole))
|
||
.filter(row => row.customerName || row.customerId)
|
||
.map(row => ({
|
||
value: row.id,
|
||
label: `${partyRoleLabel(row.partyRole)}-${row.customerName || row.customerId}`,
|
||
partyRole: row.partyRole,
|
||
partyRowId: row.id,
|
||
customerId: row.customerId,
|
||
customerName: row.customerName,
|
||
}));
|
||
},
|
||
changeAttachmentLocationOptions() {
|
||
if (this.isSettlementRuleChange) {
|
||
return ['变更材料'];
|
||
}
|
||
return ['用印材料', '附件(佐证材料等)', '变更材料'];
|
||
},
|
||
contractAmountDisplay() {
|
||
const value = this.form.contractAmount;
|
||
if (
|
||
value === undefined ||
|
||
value === null ||
|
||
value === '' ||
|
||
Number(value) === -1 ||
|
||
String(value) === '-1'
|
||
) {
|
||
return '';
|
||
}
|
||
return value;
|
||
},
|
||
contractCategoryOptions() {
|
||
return this.contractCategoryDictOptions.length
|
||
? this.contractCategoryDictOptions
|
||
: defaultContractCategoryOptions;
|
||
},
|
||
settlementModeOptions() {
|
||
return this.settlementModeDictOptions.length
|
||
? this.settlementModeDictOptions
|
||
: defaultSettlementModeOptions;
|
||
},
|
||
contractFormatOptions() {
|
||
return this.contractFormatDictOptions.length
|
||
? this.contractFormatDictOptions
|
||
: defaultContractFormatOptions;
|
||
},
|
||
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.loadDictionaries();
|
||
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),
|
||
};
|
||
},
|
||
},
|
||
methods: {
|
||
handleContractAmountInput(value) {
|
||
const text = String(value || '').replace(/[^\d.]/g, '');
|
||
const parts = text.split('.');
|
||
const normalized =
|
||
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0];
|
||
this.form.contractAmount = normalized === '' ? null : normalized;
|
||
},
|
||
formatContractAmountField() {
|
||
this.form.contractAmount = formatContractAmountInput(this.form.contractAmount);
|
||
},
|
||
loadDictionaries() {
|
||
Promise.all([
|
||
getSystemDictionary({ code: 'currency_type' }),
|
||
getBizDictionary({ code: 'settle_method' }),
|
||
getBizDictionary({ code: 'contractFormat' }),
|
||
getBizDictionary({ code: 'contractCategory' }),
|
||
]).then(([currencyRes, methodRes, formatRes, categoryRes]) => {
|
||
const records = response => {
|
||
const data = response?.data?.data || response?.data || [];
|
||
return Array.isArray(data) ? data : data.records || [];
|
||
};
|
||
this.settlementCurrencyOptions = records(currencyRes).map(item => ({
|
||
label: item.dictValue,
|
||
value: item.dictKey,
|
||
}));
|
||
this.settlementModeDictOptions = records(methodRes).map(item => ({
|
||
label: item.dictValue,
|
||
value: item.dictKey,
|
||
}));
|
||
this.contractFormatDictOptions = records(formatRes).map(item => ({
|
||
label: item.dictValue,
|
||
value: item.dictKey,
|
||
}));
|
||
this.contractCategoryDictOptions = records(categoryRes).map(item => ({
|
||
label: item.dictValue,
|
||
value: item.dictKey,
|
||
}));
|
||
});
|
||
},
|
||
async load() {
|
||
const id = this.$route.query.id;
|
||
if (!id) return;
|
||
let data = {};
|
||
if (this.isPublicViewPage) {
|
||
const res = await getMkPublicDetail('contract-change', id);
|
||
data = res.data?.data || res.data || {};
|
||
} else if (this.isContractChangeModule) {
|
||
const res = await changeApi.getDetail(id);
|
||
data = res.data?.data || res.data || {};
|
||
} else {
|
||
const res = await contractApi.getDetail(id);
|
||
data = res.data?.data || res.data || {};
|
||
}
|
||
const isChangeRecord = this.isContractChangeModule || this.isPublicViewPage;
|
||
this.form = {
|
||
...data,
|
||
// 独立变更单保留 contractId;旧路由以合同 id 作为主键
|
||
contractId: data.contractId || (isChangeRecord ? data.contractId : data.id),
|
||
changeContent: isChangeRecord ? data.changeContent || '' : '',
|
||
changeReason: isChangeRecord ? data.changeReason || '' : '',
|
||
changeAttachmentsJson: data.changeAttachmentsJson || '',
|
||
copyCount: normalizeOptionalPositiveInteger(data.copyCount),
|
||
invoiceCycle: normalizeOptionalPositiveInteger(data.invoiceCycle),
|
||
paymentDays: normalizeOptionalPositiveInteger(data.paymentDays),
|
||
contractAmount: formatContractAmountInput(data.contractAmount),
|
||
templateFlag: data.templateFlag ?? 0,
|
||
electronicSealFlag: data.electronicSealFlag ?? 0,
|
||
archiveStatus: data.archiveStatus || '未归档',
|
||
changeType: data.changeType || '合同信息变更',
|
||
};
|
||
this.changeMaterials = this.parse(data.changeAttachmentsJson);
|
||
this.period = data.startDate && data.endDate ? [data.startDate, data.endDate] : [];
|
||
this.partyARows = normalizePartyRows(data.partyAJson, data.partyA, true);
|
||
this.partyBRows = normalizePartyRows(data.partyBJson, data.partyB, true);
|
||
this.partyCRows = normalizePartyRows(data.partyCJson, data.partyC, false);
|
||
this.plans = this.parse(data.billingPlanJson);
|
||
this.attachments = this.mergeChangeAttachments(
|
||
this.parse(data.attachmentsJson),
|
||
isChangeRecord ? [] : this.parse(data.changeAttachmentsJson)
|
||
);
|
||
this.contractFileRows = normalizeContractFileRows(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);
|
||
},
|
||
goBack() {
|
||
if (this.isContractChangeModule) {
|
||
this.$router.push(contractChangeListRoute);
|
||
return;
|
||
}
|
||
this.$router.back();
|
||
},
|
||
buildChangePayload() {
|
||
if (this.settlementConfigTab === 'pre') this.preSettlementConfig = { ...this.settlementRule };
|
||
else this.formalSettlementConfig = { ...this.settlementRule };
|
||
const settlementRule = {
|
||
preSettlementConfig: this.preSettlementConfig,
|
||
formalSettlementConfig: this.formalSettlementConfig,
|
||
};
|
||
if (this.isSettlementRuleChange) {
|
||
return {
|
||
id: this.form.id,
|
||
contractId: this.form.contractId,
|
||
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),
|
||
};
|
||
}
|
||
return {
|
||
...this.form,
|
||
contractId: this.form.contractId,
|
||
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),
|
||
};
|
||
},
|
||
async saveDraft() {
|
||
if (this.form.changeType === '终止合同') {
|
||
this.$message.warning('终止合同无需保存草稿,请直接提交');
|
||
return;
|
||
}
|
||
const payload = this.buildChangePayload();
|
||
await changeApi.saveDraft(payload);
|
||
this.$message.success('草稿已保存');
|
||
},
|
||
parse(value) {
|
||
try {
|
||
const result = JSON.parse(value || '[]');
|
||
return Array.isArray(result) ? result : [];
|
||
} catch {
|
||
return [];
|
||
}
|
||
},
|
||
// 审核通过后上一次的变更材料归集到「附件(佐证材料等)」,按文件地址/文件名去重,避免重复展示
|
||
mergeChangeAttachments(attachments = [], changeAttachments = []) {
|
||
const key = item => this.attachmentUrl(item) || this.attachmentName(item);
|
||
const exists = new Set(attachments.map(key));
|
||
const merged = changeAttachments
|
||
.filter(item => !exists.has(key(item)))
|
||
.map(item => ({
|
||
...item,
|
||
size: /^\d+$/.test(String(item.size ?? '')) ? this.formatFileSize(item.size) : item.size,
|
||
}));
|
||
return [...attachments, ...merged];
|
||
},
|
||
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+/, '');
|
||
},
|
||
addPlan() {
|
||
this.planEditorIndex = -1;
|
||
this.planEditor = {
|
||
planName: `计费方案${this.plans.length + 1}`,
|
||
remark: '',
|
||
rules: [{}],
|
||
};
|
||
this.planDialogVisible = true;
|
||
},
|
||
editPlan(row, index) {
|
||
this.planEditorIndex = index;
|
||
this.planEditor = JSON.parse(JSON.stringify(row));
|
||
this.planDialogVisible = true;
|
||
},
|
||
billingPlanCustomerLabel(row) {
|
||
const matched = this.billingPartyOptions.find(
|
||
item => String(item.partyRowId) === String(row.partyRowId)
|
||
);
|
||
return matched?.label || row.customerName || '';
|
||
},
|
||
savePlan(value, index) {
|
||
const nextPlan = { ...value };
|
||
delete nextPlan.defaultPlan;
|
||
if (index < 0) this.plans.push(nextPlan);
|
||
else this.plans.splice(index, 1, nextPlan);
|
||
},
|
||
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 = ''; if (value === '自定义多周期') this.settlementRule.customPeriods = normalizeCustomPeriods(this.settlementRule.customPeriods || []); else this.settlementRule.customPeriods = []; },
|
||
attachmentUrl(row = {}) { return row.url || row.link || row.fileUrl || row.downloadUrl || row.domain || ''; },
|
||
attachmentName(row = {}) { return row.originalName || row.name || row.fileName || '附件'; },
|
||
attachmentExtension(row = {}) { const source = String(this.attachmentName(row) || this.attachmentUrl(row)).split('?')[0]; const index = source.lastIndexOf('.'); return index > -1 ? source.slice(index + 1).toLowerCase() : ''; },
|
||
isAttachmentImage(row) { return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(this.attachmentExtension(row)); },
|
||
previewAttachment(row, rows = this.attachments) { const url = this.attachmentUrl(row); if (!url) { this.$message.warning('附件地址为空,无法预览'); return; } if (this.isAttachmentImage(row)) { this.imagePreviewUrls = (rows || []).filter(item => this.isAttachmentImage(item) && this.attachmentUrl(item)).map(item => this.attachmentUrl(item)); this.imagePreviewIndex = Math.max(this.imagePreviewUrls.indexOf(url), 0); this.imagePreviewVisible = true; return; } this.previewFile = { name: this.attachmentName(row), url, mimeType: row.mimeType || row.contentType || '' }; this.documentPreviewVisible = true; },
|
||
handleAttachmentUploadToLocation({ location, rows = [] }) {
|
||
if (!rows.length) return;
|
||
if (this.isSettlementRuleChange) {
|
||
if (location === '变更材料') {
|
||
this.changeMaterials = [...(this.changeMaterials || []), ...rows];
|
||
}
|
||
return;
|
||
}
|
||
if (isContractSealMaterialLocation(location)) {
|
||
this.contractFileRows = [...(this.contractFileRows || []), ...rows];
|
||
return;
|
||
}
|
||
if (isContractSupportingAttachmentLocation(location)) {
|
||
this.attachments = [...(this.attachments || []), ...rows];
|
||
return;
|
||
}
|
||
if (location === '变更材料') {
|
||
this.changeMaterials = [...(this.changeMaterials || []), ...rows];
|
||
}
|
||
},
|
||
handlePreviewUnsupported() { this.$message.warning('当前文件暂不支持在线预览'); },
|
||
handlePreviewError() { this.$message.error('附件预览失败'); },
|
||
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: '' }); },
|
||
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() {
|
||
if (this.form.changeType === '终止合同') {
|
||
const contractId = this.form.contractId || this.form.id;
|
||
await contractApi.terminate(contractId, this.form.changeReason);
|
||
this.$message.success('合同已终止');
|
||
this.goBack();
|
||
return;
|
||
}
|
||
await this.$refs.formRef.validate();
|
||
if (!this.isSettlementRuleChange) {
|
||
const contractNo = String(this.form.contractNo || '').trim();
|
||
if (!contractNo) {
|
||
this.$message.warning('请输入合同编号');
|
||
return;
|
||
}
|
||
if (contractNo.length > 100) {
|
||
this.$message.warning('合同编号不能超过100个字符');
|
||
return;
|
||
}
|
||
this.form.contractNo = contractNo;
|
||
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 payload = this.buildChangePayload();
|
||
if (this.isContractChangeModule) {
|
||
await changeApi.submit(payload);
|
||
await submitMkApprovalFlow({
|
||
bizType: 'contract-change',
|
||
bizId: this.form.id,
|
||
subjectName: this.form.contractName || '',
|
||
approvalStatus: this.form.approvalStatus || '',
|
||
mkEnabled: await loadMkEnabled().catch(() => false),
|
||
});
|
||
sessionStorage.setItem(contractChangeListRefreshKey, '1');
|
||
this.$message.success('变更已提交');
|
||
this.$router.push(contractChangeListRoute);
|
||
return;
|
||
}
|
||
await contractApi.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>
|
||
|
||
<style scoped lang="scss">
|
||
.contract-change-page { min-height: 100%; padding-bottom: 72px; background: #f5f6fa; }
|
||
.contract-change-page :deep(.basic-container__card) { border: 0; background: transparent; box-shadow: none; }
|
||
.contract-change-form { background: #f5f6fa; }
|
||
.change-section { padding: 20px 24px; background: #fff; border-bottom: 1px solid #eff1f7; margin-top: 12px; }
|
||
.contract-basic-section { margin-bottom: 12px; padding: 14px 16px 16px; overflow: hidden; background: #fff; border-bottom: 0; border-radius: 6px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); }
|
||
.contract-basic-section > .dialog-section-title { margin-bottom: 20px; color: #303133; }
|
||
.contract-basic-section__grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); column-gap: 48px; }
|
||
.contract-basic-section__change-type { grid-column: 1 / -1; }
|
||
.contract-basic-section__remark {
|
||
width: 100%;
|
||
margin-bottom: 23px !important;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0 !important;
|
||
}
|
||
}
|
||
.contract-basic-section__date-range { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center; gap: 8px; width: 360px; max-width: 100%; }
|
||
.contract-basic-section__date-range span { color: #606266; }
|
||
// 风险客商提示条(基本信息备注后):橙黄背景 + 红色文字,与合同新增页一致
|
||
.contract-manage-form__risk-tip { margin: 0 0 18px; padding: 10px 16px; background: #faecd8; border-radius: 4px; }
|
||
.contract-manage-form__risk-tip-title { color: #f56c6c; font-weight: 600; line-height: 22px; }
|
||
.contract-manage-form__risk-tip-content { color: #f56c6c; line-height: 22px; }
|
||
.contract-basic-section__date-range :deep(.el-date-editor) { width: auto; min-width: 0; }
|
||
.contract-basic-section :deep(.el-form-item) { align-items: center; }
|
||
.contract-basic-section :deep(.el-form-item__label) { flex: 0 0 96px !important; width: 96px !important; height: auto; padding-right: 12px; white-space: normal; word-break: break-all; line-height: 18px; }
|
||
.contract-basic-section :deep(.el-form-item__content) { min-width: 0; }
|
||
.contract-basic-section :deep(.el-input),
|
||
.contract-basic-section :deep(.el-select),
|
||
.contract-basic-section :deep(.el-date-editor) { width: 360px; max-width: 100%; }
|
||
.contract-basic-section :deep(.el-input-number) {
|
||
width: 360px;
|
||
max-width: 100%;
|
||
|
||
.el-input__inner {
|
||
text-align: left;
|
||
}
|
||
}
|
||
.dialog-section-title { margin-bottom: 18px; font-size: 16px; font-weight: 600; }
|
||
.dialog-section-title::before { display: inline-block; width: 4px; height: 16px; margin-right: 8px; vertical-align: -2px; background: #409eff; content: ''; }
|
||
.section-head { display: flex; align-items: center; justify-content: space-between; }
|
||
.change-table { margin: 16px 0; }
|
||
.ratio-tip { margin-bottom: 8px; color: #f56c6c; }
|
||
.unit { margin-left: 8px; }
|
||
.attachment-upload { display: flex; justify-content: flex-start; margin-top: 12px; }
|
||
.attachment-upload .vehicle-attachment-upload { width: auto; }
|
||
.attachment-upload .el-upload { display: inline-flex; }
|
||
.settlement-switch { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
|
||
// 问号提示图标紧靠「开启」一侧:清掉开启 radio 自带的右间距,图标与「关闭」之间留大间距
|
||
.settlement-switch :deep(.el-radio:first-child) { margin-right: 0; }
|
||
.settlement-switch-tip { margin: 0 32px 0 8px; color: #a8abb2; cursor: help; font-size: 14px; }
|
||
.settlement-form { display: grid; grid-template-columns: repeat(3, minmax(240px, 1fr)); gap: 8px 28px; }
|
||
.change-reason-section { margin: 20px 0; }
|
||
.change-reason-section__input {
|
||
width: 100%;
|
||
:deep(.el-textarea__inner) {
|
||
border-color: #f56c6c;
|
||
box-shadow: 0 0 0 1px #f56c6c inset;
|
||
}
|
||
:deep(.el-textarea__inner:hover),
|
||
:deep(.el-textarea__inner:focus) {
|
||
border-color: #f56c6c;
|
||
box-shadow: 0 0 0 1px #f56c6c inset;
|
||
}
|
||
}
|
||
.page-footer {
|
||
position: fixed;
|
||
right: 0;
|
||
bottom: 0;
|
||
left: 230px;
|
||
z-index: 20;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
min-height: 64px;
|
||
box-sizing: border-box;
|
||
padding: 12px 24px;
|
||
background: #fff;
|
||
border-top: 1px solid #eff1f7;
|
||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
|
||
|
||
// 统一 12px 间距,与全站底部操作栏(去 gap,由相邻按钮 margin 提供)一致
|
||
.el-button + .el-button {
|
||
margin-left: 12px;
|
||
}
|
||
}
|
||
:global(.avue--collapse .page-footer) {
|
||
left: 60px;
|
||
}
|
||
:global(.avue-layout--horizontal .page-footer) {
|
||
left: 0;
|
||
}
|
||
@media (max-width: 1200px) { .contract-basic-section__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 32px; } }
|
||
@media (max-width: 768px) { .contract-basic-section__grid { grid-template-columns: 1fr; } }
|
||
</style>
|