You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1168 lines
52 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div class="p-2">
<!-- 审批按钮组件参考 contractInfo/edit.vue -->
<el-card shadow="never" style="margin-top: 0">
<approvalButton
@submitForm="submitForm"
@approvalVerifyOpen="approvalVerifyOpen"
@handleApprovalRecord="handleApprovalRecord"
:buttonLoading="buttonLoading"
:id="form.contractChangeId"
:status="form.flowStatus"
:pageType="routeParams.type"
:mode="false"
/>
</el-card>
<el-card shadow="never">
<template #header>
<span>{{ isEdit ? '编辑合同变更' : '合同变更申请' }}</span>
<el-button class="float-right" link @click="goBack"></el-button>
</template>
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
<el-form-item label="变更类型" prop="changeType">
<el-radio-group v-model="form.changeType" :disabled="isFormDisabled">
<el-radio v-for="dict in contract_change_type" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="选择合同" prop="contractId">
<el-input
v-model="selectedContractName"
placeholder="请选择已激活的合同(点击右侧图标选择)"
readonly
:disabled="isFormDisabled"
>
<template #suffix>
<el-icon style="cursor: pointer" @click="openContractSelect"><Search /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item label="变更原因" prop="changeReason">
<el-input
v-model="form.changeReason"
type="textarea"
:rows="3"
placeholder="请输入变更原因(解除合同时即解除原因)"
:disabled="isFormDisabled"
/>
</el-form-item>
<!-- 内容变更时:下方按合同编辑页样式展示变更后合同信息、付款方式、物料,可编辑可删 -->
<template v-if="form.changeType === '1' && form.changeInfo">
<!-- 变更后合同信息(与合同编辑页同结构) -->
<el-card shadow="never" style="margin-top: 16px">
<template #header>
<div style="text-align: left; font-weight: bold; font-size: 18px">变更后合同信息</div>
</template>
<el-form :model="form.changeInfo" label-width="120px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="有无合同">
<el-radio-group v-model="form.changeInfo.contractFlag" :disabled="isFormDisabled">
<el-radio v-for="dict in contract_flag" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="合同编号">
<el-input v-model="form.changeInfo.contractCode" placeholder="请输入合同编号" :disabled="isFormDisabled" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="合同名称">
<el-input v-model="form.changeInfo.contractName" placeholder="请输入合同名称" :disabled="isFormDisabled" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="合同大类">
<el-select v-model="form.changeInfo.contractCategory" placeholder="请选择合同大类" :disabled="isFormDisabled" style="width: 100%">
<el-option v-for="dict in contract_category" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="业务方向">
<el-select v-model="form.changeInfo.businessDirection" placeholder="请选择业务方向" :disabled="isFormDisabled" style="width: 100%">
<el-option v-for="dict in business_direction" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="合同负责人">
<el-select v-model="form.changeInfo.contractManagerId" placeholder="请选择签订人" :disabled="isFormDisabled" filterable style="width: 100%">
<el-option v-for="item in userInfoList" :key="item.userId" :label="item.nickName" :value="item.userId" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="合同签订日期">
<el-date-picker v-model="form.changeInfo.contractDate" type="date" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择合同时间" :disabled="isFormDisabled" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="付款账户">
<el-select v-model="form.changeInfo.paymentAccountId" placeholder="请选择付款账户" :disabled="isFormDisabled" clearable style="width: 100%">
<el-option v-for="item in paymentAccountList" :key="item.paymentAccountId" :label="`${item.accountType || ''} - ${item.accountNumber || ''}`" :value="item.paymentAccountId" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="甲方公司">
<el-select v-model="form.changeInfo.oneCustomerId" placeholder="请选择甲方公司" :disabled="isFormDisabled" filterable style="width: 100%">
<el-option v-for="item in customerInfoList" :key="item.customerId" :label="item.customerName" :value="item.customerId" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="乙方公司">
<el-select v-model="form.changeInfo.twoCustomerId" placeholder="请选择乙方公司" :disabled="isFormDisabled" filterable style="width: 100%">
<el-option v-for="item in customerInfoList" :key="item.customerId" :label="item.customerName" :value="item.customerId" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="甲方授权代表">
<el-input v-model="form.changeInfo.oneRepresent" placeholder="甲方法人或授权代表" :disabled="isFormDisabled" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="乙方授权代表">
<el-input v-model="form.changeInfo.twoRepresent" placeholder="乙方法人或授权代表" :disabled="isFormDisabled" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="甲方签字日期">
<el-date-picker v-model="form.changeInfo.oneDate" type="date" value-format="YYYY-MM-DD HH:mm:ss" placeholder="甲方签字日期" :disabled="isFormDisabled" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="乙方签字日期">
<el-date-picker v-model="form.changeInfo.twoDate" type="date" value-format="YYYY-MM-DD HH:mm:ss" placeholder="乙方签字日期" :disabled="isFormDisabled" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="合同模板标识">
<el-radio-group v-model="form.changeInfo.contractTemplateFlag" :disabled="isFormDisabled">
<el-radio v-for="dict in contract_template_flag" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12" v-show="form.changeInfo.contractTemplateFlag !== '1'">
<el-form-item label="合同模板">
<el-select v-model="form.changeInfo.templateId" placeholder="请选择合同模板" :disabled="isFormDisabled" filterable style="width: 100%">
<el-option v-for="item in printTemplateList" :key="item.templateId" :label="item.templateName + '-' + (item.version || '')" :value="item.templateId" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="合同总价">
<el-input v-model="form.changeInfo.totalPrice" placeholder="根据物料自动计算" disabled>
<template #append>元</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="质保期描述">
<el-input v-model="form.changeInfo.warrantyPeriodDescription" placeholder="如验收合格后12个月" :disabled="isFormDisabled" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="交货地点">
<el-input v-model="form.changeInfo.deliveryLocation" placeholder="如:甲方指定仓库" :disabled="isFormDisabled" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="运输方式">
<el-input v-model="form.changeInfo.shipMethod" placeholder="如:汽运、空运" :disabled="isFormDisabled" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="交付启动期限(天)">
<el-input-number v-model="form.changeInfo.deliveryStart" placeholder="天" :disabled="isFormDisabled" :min="0" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="签订地点">
<el-input v-model="form.changeInfo.signingPlace" placeholder="签订地点" :disabled="isFormDisabled" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="付款方式">
<el-select v-model="form.changeInfo.paymentMethod" placeholder="请选择付款方式" :disabled="isFormDisabled" clearable style="width: 100%">
<el-option v-for="item in paymentMethodOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="备注">
<el-input v-model="form.changeInfo.remark" type="textarea" placeholder="备注" :disabled="isFormDisabled" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="附件">
<FileUpload
v-model="changeInfoOssId"
:limit="5"
:fileSize="20"
:fileType="['doc', 'docx', 'pdf', 'xls', 'xlsx']"
:disabled="isFormDisabled"
:isShowTip="true"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- 变更后合同付款方式 -->
<el-card shadow="never" style="margin-top: 0">
<template #header>
<div style="text-align: left; font-weight: bold; font-size: 18px">合同付款方式</div>
</template>
<div style="margin-bottom: 16px">
<el-button type="primary" icon="Plus" v-if="!isFormDisabled" @click="handleAddPaymentMethod">新增付款方式</el-button>
</div>
<el-table :data="changePaymentMethodList" border>
<el-table-column label="序号" align="center" prop="sortOrder" width="80" />
<el-table-column label="付款条款" align="center" prop="paymentDescription" min-width="200" show-overflow-tooltip />
<el-table-column label="支付金额" align="center" prop="paymentAmount" width="100" />
<el-table-column label="支付比例(%)" align="center" prop="paymentPercentage" width="100" />
<el-table-column label="操作" align="center" fixed="right" width="150" v-if="!isFormDisabled">
<template #default="scope">
<el-button link type="primary" icon="Edit" @click="handleEditPaymentMethod(scope.row, scope.$index)">编辑</el-button>
<el-button link type="danger" icon="Delete" @click="handleDeletePaymentMethod(scope.$index)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 变更后合同物料 -->
<el-card shadow="never" style="margin-top: 0">
<template #header>
<div style="text-align: left; font-weight: bold; font-size: 18px">合同物料</div>
</template>
<div style="margin-bottom: 16px">
<el-button type="primary" icon="Plus" v-if="!isFormDisabled" @click="handleAddMaterial">新增合同物料</el-button>
</div>
<el-table :data="changeMaterialList" border>
<el-table-column label="物料编号" align="center" prop="materialCode" width="120" />
<el-table-column label="物料名称" align="center" prop="materialName" width="120" />
<el-table-column label="销售物料名称" align="center" prop="saleMaterialName" width="120" />
<el-table-column label="产品名称(合同显示)" align="center" prop="productName" min-width="140" />
<el-table-column label="规格描述" align="center" prop="specificationDescription" min-width="100" />
<el-table-column label="数量" align="center" prop="amount" width="80">
<template #default="scope">{{ scope.row.amount != null ? Number(scope.row.amount).toFixed(2) : '0.00' }}</template>
</el-table-column>
<el-table-column label="单位" align="center" prop="unitName" width="60" />
<el-table-column label="未税单价" align="center" prop="beforePrice" width="100">
<template #default="scope">{{ scope.row.beforePrice != null ? Number(scope.row.beforePrice).toFixed(2) : '0.00' }}</template>
</el-table-column>
<el-table-column label="税率(%)" align="center" prop="taxRate" width="80">
<template #default="scope">{{ scope.row.taxRate != null ? Number(scope.row.taxRate).toFixed(2) : '0.00' }}</template>
</el-table-column>
<el-table-column label="含税单价" align="center" prop="includingPrice" width="100">
<template #default="scope">{{ scope.row.includingPrice != null ? Number(scope.row.includingPrice).toFixed(2) : '0.00' }}</template>
</el-table-column>
<el-table-column label="小计" align="center" prop="subtotal" width="90">
<template #default="scope">{{ scope.row.subtotal != null ? Number(scope.row.subtotal).toFixed(2) : '0.00' }}</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" min-width="80" />
<el-table-column label="操作" align="center" fixed="right" width="150" v-if="!isFormDisabled">
<template #default="scope">
<el-button link type="primary" icon="Edit" @click="handleEditMaterial(scope.row)">编辑</el-button>
<el-button link type="danger" icon="Delete" @click="handleDeleteMaterial(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-form-item label="合同物料备注" style="margin-bottom: 16px; margin-top: 16px">
<el-input v-model="form.changeInfo.materialRemark" type="textarea" :rows="3" placeholder="合同物料备注" :disabled="isFormDisabled" />
</el-form-item>
</el-card>
</template>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="选填" :disabled="isFormDisabled" />
</el-form-item>
</el-form>
</el-card>
<!-- 合同物料编辑对话框 -->
<el-dialog v-model="materialDialog.visible" :title="materialDialog.title" width="800px" append-to-body>
<el-form ref="materialFormRef" :model="materialForm" :rules="materialRules" label-width="120px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="标准物料标识" prop="materialFlag">
<el-radio-group v-model="materialForm.materialFlag">
<el-radio v-for="dict in material_flag" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12" v-if="materialForm.materialFlag === '1'">
<el-form-item label="物料名称" prop="materialName">
<el-input v-model="materialForm.materialName" placeholder="点击右侧图标检索">
<template #suffix>
<el-icon style="cursor: pointer" @click="openSaleMaterialSelect"><Search /></el-icon>
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12" v-if="materialForm.materialFlag === '1'">
<el-form-item label="物料编号"><el-input v-model="materialForm.materialCode" disabled /></el-form-item>
</el-col>
<el-col :span="12" v-if="materialForm.materialFlag === '1'">
<el-form-item label="客户名称"><el-input v-model="materialForm.customerName" disabled /></el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="产品名称" prop="productName">
<el-input v-model="materialForm.productName" placeholder="产品名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="规格描述" prop="specificationDescription">
<el-input v-model="materialForm.specificationDescription" placeholder="规格描述" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="数量" prop="amount">
<el-input-number v-model="materialForm.amount" placeholder="数量" style="width: 100%" :precision="2" @change="calculateSubtotal" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="物料单位" prop="unitId">
<el-select v-model="materialForm.unitId" placeholder="请选择单位" style="width: 100%">
<el-option v-for="item in unitInfoList" :key="item.unitId" :label="item.unitName" :value="item.unitId" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="税率(%)" prop="taxRate">
<el-select v-model="materialForm.taxRate" placeholder="税率" style="width: 100%" filterable allow-create @change="calculateBeforePrice">
<el-option label="0" :value="0" /><el-option label="6" :value="6" /><el-option label="9" :value="9" /><el-option label="13" :value="13" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="未税单价"><el-input-number v-model="materialForm.beforePrice" style="width: 100%" :precision="2" disabled :controls="false" /></el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="含税单价" prop="includingPrice">
<el-input-number v-model="materialForm.includingPrice" placeholder="含税单价" style="width: 100%" :precision="2" @change="calculateBeforePrice" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="小计"><el-input-number v-model="materialForm.subtotal" style="width: 100%" :precision="2" readonly disabled :controls="false" /></el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注" prop="remark"><el-input v-model="materialForm.remark" type="textarea" placeholder="备注" /></el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="cancelMaterial">取 消</el-button>
<el-button type="primary" @click="submitMaterialForm">确 定</el-button>
</template>
</el-dialog>
<!-- 合同付款方式编辑对话框 -->
<el-dialog v-model="paymentMethodDialog.visible" :title="paymentMethodDialog.title" width="500px" append-to-body>
<el-form ref="paymentMethodFormRef" :model="paymentMethodForm" :rules="paymentMethodRules" label-width="120px">
<el-form-item label="序号" prop="sortOrder">
<el-input-number v-model="paymentMethodForm.sortOrder" :min="1" readonly :controls="false" style="width: 100%" />
</el-form-item>
<el-form-item label="付款节点" prop="paymentStageId">
<el-select v-model="paymentMethodForm.paymentStageId" placeholder="请选择付款节点" style="width: 100%" filterable @change="onPaymentStageChange">
<el-option v-for="item in paymentStageList" :key="item.paymentStageId" :label="item.paymentMethod" :value="item.paymentStageId" />
</el-select>
</el-form-item>
<el-form-item label="支付期限(天)" prop="paymentDeadline">
<el-input-number v-model="paymentMethodForm.paymentDeadline" :min="0" style="width: 100%" @change="refreshPaymentDescription" />
</el-form-item>
<el-form-item label="支付比例(%" prop="paymentPercentage">
<el-input-number v-model="paymentMethodForm.paymentPercentage" :min="0" :max="100" style="width: 100%" @change="refreshPaymentDescription" />
</el-form-item>
<el-form-item label="发票比例(%" prop="invoicePercentage">
<el-input-number v-model="paymentMethodForm.invoicePercentage" :min="0" :max="100" style="width: 100%" @change="refreshPaymentDescription" />
</el-form-item>
<el-form-item label="付款条款" prop="paymentDescription">
<el-input v-model="paymentMethodForm.paymentDescription" type="textarea" :rows="5" placeholder="选择付款节点后自动带出,可修改" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="cancelPaymentMethod">取 消</el-button>
<el-button type="primary" @click="submitPaymentMethodForm">确 定</el-button>
</template>
</el-dialog>
<SaleMaterialSelect ref="saleMaterialSelectRef" :multiple="false" @confirm-call-back="saleMaterialSelectCallBack" />
<!-- 选择合同弹框:列表 + 合同详情 -->
<el-dialog v-model="contractDialog.visible" title="选择合同" width="960px" append-to-body>
<el-form :model="contractQueryParams" :inline="true" label-width="90px">
<el-form-item label="合同编号">
<el-input
v-model="contractQueryParams.contractCode"
placeholder="请输入合同编号"
clearable
style="width: 160px"
@keyup.enter="getContractList"
/>
</el-form-item>
<el-form-item label="合同名称">
<el-input
v-model="contractQueryParams.contractName"
placeholder="请输入合同名称"
clearable
style="width: 180px"
@keyup.enter="getContractList"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="getContractList">搜索</el-button>
<el-button icon="Refresh" @click="resetContractQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="16">
<el-col :span="14">
<el-table
v-loading="contractLoading"
:data="contractList"
border
max-height="360"
highlight-current-row
@current-change="handleContractRowChange"
>
<el-table-column type="index" label="序号" width="55" />
<el-table-column label="合同编号" align="center" prop="contractCode" min-width="120" />
<el-table-column label="合同名称" align="center" prop="contractName" min-width="160" show-overflow-tooltip />
<el-table-column label="合同总价" align="center" prop="totalPrice" width="100" />
</el-table>
<pagination
v-show="contractTotal > 0"
:total="contractTotal"
v-model:page="contractQueryParams.pageNum"
v-model:limit="contractQueryParams.pageSize"
@pagination="getContractList"
/>
</el-col>
<el-col :span="10">
<div class="contract-detail-panel">
<div class="panel-title">合同详情</div>
<template v-if="contractDetailPreview">
<el-descriptions :column="1" border size="small">
<el-descriptions-item label="合同编号">{{ contractDetailPreview.contractCode }}</el-descriptions-item>
<el-descriptions-item label="合同名称">{{ contractDetailPreview.contractName }}</el-descriptions-item>
<el-descriptions-item label="合同总价">{{ contractDetailPreview.totalPrice }}</el-descriptions-item>
<el-descriptions-item label="客户">{{ (contractDetailPreview as any).oneCustomerName || (contractDetailPreview as any).twoCustomerName || '-' }}</el-descriptions-item>
<el-descriptions-item label="签订日期">{{ contractDetailPreview.contractDate || '-' }}</el-descriptions-item>
</el-descriptions>
</template>
<div v-else class="panel-placeholder">点击左侧列表行可查看合同详情</div>
</div>
</el-col>
</el-row>
<template #footer>
<el-button @click="contractDialog.visible = false"> </el-button>
<el-button type="primary" @click="submitContractSelect"> </el-button>
</template>
</el-dialog>
<!-- 审批记录 -->
<approvalRecord ref="approvalRecordRef" />
<submitVerify ref="submitVerifyRef" :task-variables="taskVariables" @submit-callback="submitCallback" />
</div>
</template>
<script setup name="ContractChangeApply" lang="ts">
import { Search } from '@element-plus/icons-vue';
import { listContractInfo, getContractInfo } from '@/api/oa/erp/contractInfo';
import type { ContractInfoVO, ContractInfoQuery } from '@/api/oa/erp/contractInfo/types';
import { saveContractChange, getContractChangeDetail } from '@/api/oa/erp/contractChange';
import type { ContractChangeSaveForm } from '@/api/oa/erp/contractChange/types';
import type { ContractMaterialForm } from '@/api/oa/erp/contractMaterial/types';
import type { ContractPaymentMethodForm } from '@/api/oa/erp/contractPaymentMethod/types';
import { getBaseUnitInfoList } from '@/api/oa/base/unitInfo';
import { listPaymentStage } from '@/api/oa/base/paymentStage';
import type { PaymentStageVO } from '@/api/oa/base/paymentStage/types';
import { getCrmCustomerInfoList } from '@/api/oa/crm/customerInfo';
import { getCrmPaymentAccountList } from '@/api/oa/crm/paymentAccount';
import { getUserList } from '@/api/system/user';
import { getBasePrintTemplateList } from '@/api/oa/base/printTemplate';
import { startWorkFlow } from '@/api/workflow/task';
import ApprovalButton from '@/components/Process/approvalButton.vue';
import ApprovalRecord from '@/components/Process/approvalRecord.vue';
import SubmitVerify from '@/components/Process/submitVerify.vue';
import FileUpload from '@/components/FileUpload/index.vue';
import SaleMaterialSelect from '@/components/SaleMaterialSelect/index.vue';
import { FlowCodeEnum } from '@/enums/OAEnum';
const route = useRoute();
const router = useRouter();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const {
contract_change_type,
contract_category,
business_direction,
contract_flag,
contract_template_flag,
material_flag
} = toRefs<any>(
proxy?.useDict('contract_change_type', 'contract_category', 'business_direction', 'contract_flag', 'contract_template_flag', 'material_flag')
);
const paymentMethodOptions = ['电汇', '银行承兑6个月内', '电汇/银行承兑6个月内', '商业承兑'];
const formRef = ref<ElFormInstance>();
const buttonLoading = ref(false);
const routeParams = ref<Record<string, any>>({ ...(route.query as Record<string, any>) });
// 审批相关
const approvalRecordRef = ref<InstanceType<typeof ApprovalRecord>>();
const submitVerifyRef = ref<InstanceType<typeof SubmitVerify>>();
const taskVariables = ref<Record<string, any>>({});
// 下拉与选项
const unitInfoList = ref<any[]>([]);
const paymentStageList = ref<PaymentStageVO[]>([]);
const customerInfoList = ref<any[]>([]);
const userInfoList = ref<any[]>([]);
const paymentAccountList = ref<any[]>([]);
const printTemplateList = ref<any[]>([]);
// 变更后物料/付款方式列表(仅内容变更时使用)
const changeMaterialList = computed(() => form.value.changeMaterialList || []);
const changePaymentMethodList = computed(() => form.value.changePaymentMethodList || []);
// 变更后合同附件(绑定到 changeInfo.ossId
const changeInfoOssId = computed({
get() {
const v = form.value.changeInfo?.ossId;
return v === undefined || v === null ? '' : String(v);
},
set(val: string) {
if (form.value.changeInfo) form.value.changeInfo.ossId = val || undefined;
}
});
// 合同物料弹框
const materialDialog = reactive({ visible: false, title: '' });
const materialFormRef = ref<ElFormInstance>();
const initMaterialFormData: ContractMaterialForm & { materialCode?: string; customerName?: string; saleMaterialName?: string } = {
contractMaterialId: undefined,
materialFlag: '2',
contractId: undefined,
productName: undefined,
specificationDescription: undefined,
materialId: undefined,
relationMaterialId: undefined,
materialCode: undefined,
materialName: undefined,
saleMaterialName: undefined,
customerName: undefined,
amount: undefined,
unitId: undefined,
beforePrice: undefined,
taxRate: undefined,
includingPrice: undefined,
subtotal: undefined,
remark: undefined,
activeFlag: '1'
};
const materialForm = ref<ContractMaterialForm & { materialCode?: string; customerName?: string; saleMaterialName?: string }>({ ...initMaterialFormData });
const materialRules = {
productName: [{ required: true, message: '产品名称不能为空', trigger: 'blur' }],
amount: [{ required: true, message: '数量不能为空', trigger: 'blur' }],
taxRate: [{ required: true, message: '税率不能为空', trigger: 'blur' }]
};
// 合同付款方式弹框
const paymentMethodDialog = reactive({ visible: false, title: '' });
const editingPaymentMethodIndex = ref<number | null>(null);
const paymentMethodFormRef = ref<ElFormInstance>();
const initPaymentMethodFormData: ContractPaymentMethodForm = {
paymentMethodId: undefined,
contractId: undefined,
sortOrder: undefined,
paymentStageId: undefined,
paymentDeadline: undefined,
paymentPercentage: undefined,
invoicePercentage: 0,
paymentAmount: undefined,
paymentDescription: undefined,
remark: undefined,
activeFlag: '1'
};
const paymentMethodForm = ref<ContractPaymentMethodForm>({ ...initPaymentMethodFormData });
const paymentMethodRules = {
paymentStageId: [{ required: true, message: '请选择付款节点', trigger: 'change' }],
paymentDeadline: [{ required: true, message: '支付期限(天)不能为空', trigger: 'blur' }],
paymentPercentage: [{ required: true, message: '支付比例不能为空', trigger: 'blur' }],
invoicePercentage: [{ required: true, message: '发票比例不能为空', trigger: 'blur' }]
};
const PAYMENT_TEMPLATE_PLACEHOLDERS: Record<string, string> = {
'{序号}': 'sortOrder',
'{支付期限}': 'paymentDeadline',
'{支付比例}': 'paymentPercentage',
'{发票比例}': 'invoicePercentage'
};
const replacePaymentTemplate = (template: string | undefined, data: Record<string, any>): string => {
if (template == null || template === '') return '';
let result = template;
for (const [placeholder, fieldKey] of Object.entries(PAYMENT_TEMPLATE_PLACEHOLDERS)) {
const value = data[fieldKey];
result = result.replaceAll(placeholder, value !== undefined && value !== null ? String(value) : '');
}
return result;
};
const removeSegmentWhenInvoiceZero = (text: string, invoicePercentage: any): string => {
if (text == null || text === '') return text;
const isZero = invoicePercentage === 0 || invoicePercentage === '0' || Number(invoicePercentage) === 0;
if (!isZero) return text;
const startChar = '且';
const endChar = '后';
const idxStart = text.indexOf(startChar);
if (idxStart === -1) return text;
const idxEnd = text.indexOf(endChar, idxStart);
if (idxEnd === -1) return text;
return text.slice(0, idxStart) + text.slice(idxEnd + endChar.length);
};
const applyPaymentDescription = (template: string | undefined, data: Record<string, any>): string => {
const replaced = replacePaymentTemplate(template, data);
return removeSegmentWhenInvoiceZero(replaced, data.invoicePercentage);
};
const paymentMethodTemplateRaw = ref<string | null>(null);
const refreshPaymentDescription = () => {
if (paymentMethodTemplateRaw.value) {
paymentMethodForm.value.paymentDescription = applyPaymentDescription(
paymentMethodTemplateRaw.value,
paymentMethodForm.value as Record<string, any>
);
}
};
const onPaymentStageChange = (paymentStageId: string | number | undefined) => {
if (paymentStageId == null) return;
const stage = paymentStageList.value.find((s) => s.paymentStageId === paymentStageId || String(s.paymentStageId) === String(paymentStageId));
if (stage?.paymentTemplate) {
paymentMethodTemplateRaw.value = stage.paymentTemplate;
paymentMethodForm.value.paymentDescription = applyPaymentDescription(
stage.paymentTemplate,
paymentMethodForm.value as Record<string, any>
);
}
};
const saleMaterialSelectRef = ref<InstanceType<typeof SaleMaterialSelect>>();
// 合同选择弹框
const contractDialog = reactive({ visible: false });
const contractLoading = ref(false);
const contractList = ref<ContractInfoVO[]>([]);
const contractTotal = ref(0);
const contractQueryParams = ref<ContractInfoQuery>({
pageNum: 1,
pageSize: 10,
contractStatus: '3',
activeFlag: '1'
});
const selectedContract = ref<ContractInfoVO | null>(null);
const contractDetailPreview = ref<ContractInfoVO | null>(null);
const selectedContractName = ref<string>('');
const isEdit = computed(() => !!(routeParams.value.id && (routeParams.value.type === 'update' || routeParams.value.type === 'view' || routeParams.value.type === 'approval')));
const isFormDisabled = computed(() => routeParams.value.type === 'view' || routeParams.value.type === 'approval');
const form = ref<
ContractChangeSaveForm & {
changeInfo?: any;
changeMaterialList?: any[];
changePaymentMethodList?: any[];
flowStatus?: string;
changeCode?: string;
}
>({
contractChangeId: undefined,
contractId: undefined as any,
changeType: '1',
changeReason: '',
applyTime: undefined,
remark: undefined,
changeStatus: '1',
flowStatus: 'draft',
activeFlag: '1',
contractCode: undefined,
contractName: undefined,
originalCustomerName: undefined,
originalContractAmount: undefined,
changeContractCode: undefined,
changeContractName: undefined,
customerName: undefined,
changeContractAmount: undefined,
changeInfo: undefined,
changeMaterialList: undefined,
changePaymentMethodList: undefined,
changeCode: undefined
});
const rules = {
changeType: [{ required: true, message: '请选择变更类型', trigger: 'change' }],
contractId: [{ required: true, message: '请选择合同', trigger: 'change' }],
changeReason: [{ required: true, message: '请输入变更原因', trigger: 'blur' }]
};
function goBack() {
proxy?.$tab.closePage(route);
router.push({ path: '/contract/contractChange' });
}
/** 打开合同选择弹框 */
function openContractSelect() {
if (isFormDisabled.value) return;
contractDialog.visible = true;
selectedContract.value = null;
contractDetailPreview.value = null;
contractQueryParams.value.pageNum = 1;
getContractList();
}
/** 查询合同列表(仅已激活) */
async function getContractList() {
contractLoading.value = true;
try {
const res = await listContractInfo(contractQueryParams.value);
contractList.value = res.rows || [];
contractTotal.value = res.total || 0;
} finally {
contractLoading.value = false;
}
}
function resetContractQuery() {
contractQueryParams.value.contractCode = undefined;
contractQueryParams.value.contractName = undefined;
contractQueryParams.value.pageNum = 1;
getContractList();
}
/** 表格当前行变化:选中合同并加载详情 */
async function handleContractRowChange(row: ContractInfoVO | null) {
selectedContract.value = row || null;
contractDetailPreview.value = null;
if (row?.contractId) {
try {
const res = await getContractInfo(row.contractId);
contractDetailPreview.value = res.data || null;
} catch (e) {
console.error(e);
}
}
}
/** 确定选择合同 */
function submitContractSelect() {
if (selectedContract.value) {
const c = selectedContract.value;
form.value.contractId = c.contractId as any;
selectedContractName.value = c.contractName || '';
contractDialog.visible = false;
onContractChange(c.contractId);
} else {
proxy?.$modal.msgWarning('请先选择一条合同');
}
}
async function onContractChange(contractId: string | number) {
if (!contractId || form.value.changeType !== '1') return;
const res = await getContractInfo(contractId);
const c = res.data;
if (!c) return;
form.value.contractCode = c.contractCode;
form.value.contractName = c.contractName;
const cAny = c as any;
form.value.originalCustomerName = cAny.oneCustomerName || cAny.twoCustomerName;
form.value.originalContractAmount = c.totalPrice as any;
form.value.changeContractCode = c.contractCode;
form.value.changeContractName = c.contractName;
form.value.customerName = cAny.oneCustomerName || cAny.twoCustomerName;
form.value.changeContractAmount = c.totalPrice as any;
form.value.changeInfo = c ? mapContractToChangeInfo(c) : undefined;
form.value.changeMaterialList = (cAny.contractMaterialList || []).map((m: any) => ({
...m,
changeMaterialId: undefined,
contractChangeId: undefined
}));
form.value.changePaymentMethodList = (cAny.contractPaymentMethodList || []).map((p: any) => ({
...p,
changePaymentId: undefined,
contractChangeId: undefined
}));
}
function mapContractToChangeInfo(c: any) {
const info: any = {};
const keys = [
'contractFlag',
'customerContractCode',
'contractCode',
'contractName',
'contractCategory',
'contractType',
'businessDirection',
'contractDeptId',
'contractDate',
'totalPrice',
'oneCustomerId',
'oneRepresent',
'oneDate',
'twoCustomerId',
'twoRepresent',
'twoDate',
'contractManagerId',
'templateId',
'ossId',
'paymentAccountId',
'paymentMethod',
'signatureAppendix',
'warrantyPeriod',
'internalContractCode',
'externalContractCode',
'orderContractCode',
'projectContractCode',
'deliveryStart',
'warrantyPeriodDescription',
'deliveryLocation',
'shipMethod',
'taxRate',
'signingPlace',
'materialRemark',
'contractTemplateFlag',
'capitalizedAmount',
'remark'
];
keys.forEach((k) => {
if (c[k] !== undefined) info[k] = c[k];
});
info.changeInfoId = undefined;
info.contractChangeId = undefined;
if (info.contractTemplateFlag == null) info.contractTemplateFlag = '2';
return info;
}
async function loadDetail() {
const id = routeParams.value.id;
if (!id) return;
const res = await getContractChangeDetail(id);
const d = res.data;
if (!d?.main) return;
const m = d.main;
form.value.contractChangeId = m.contractChangeId;
form.value.contractId = m.contractId;
form.value.changeType = m.changeType;
form.value.changeReason = m.changeReason;
form.value.changeCode = m.changeCode;
form.value.applyTime = m.applyTime;
form.value.remark = m.remark;
form.value.changeStatus = m.changeStatus;
form.value.flowStatus = m.flowStatus ?? (m.changeStatus === '1' ? 'draft' : 'waiting');
form.value.contractCode = m.contractCode;
form.value.contractName = m.contractName;
form.value.originalCustomerName = m.originalCustomerName;
form.value.originalContractAmount = m.originalContractAmount;
form.value.changeContractCode = m.changeContractCode;
form.value.changeContractName = m.changeContractName;
form.value.customerName = m.customerName;
form.value.changeContractAmount = m.changeContractAmount;
form.value.changeInfo = d.changeInfo ? { ...d.changeInfo } : undefined;
form.value.changeMaterialList = (d.changeMaterialList || []).map((x: any) => ({ ...x }));
form.value.changePaymentMethodList = (d.changePaymentMethodList || []).map((x: any) => ({ ...x }));
selectedContractName.value = m.contractName || '';
}
function loadSelectOptions() {
getBaseUnitInfoList(null).then((res) => (unitInfoList.value = res.data || []));
listPaymentStage({ pageNum: 1, pageSize: 500 }).then((res) => (paymentStageList.value = res.rows || []));
getCrmCustomerInfoList(null).then((res) => (customerInfoList.value = res.data || []));
getUserList({ pageNum: 1, pageSize: 1000 }).then((res) => (userInfoList.value = res.data || []));
getCrmPaymentAccountList({}).then((res) => (paymentAccountList.value = res.data || []));
getBasePrintTemplateList({ templateType: '1' }).then((res) => (printTemplateList.value = res.data || []));
}
// ----- 变更后物料:增删改 -----
function handleAddMaterial() {
if (!form.value.changeInfo) return;
resetMaterialForm();
materialForm.value.contractId = form.value.contractId as any;
materialDialog.visible = true;
materialDialog.title = '新增合同物料';
}
function handleEditMaterial(row: any) {
resetMaterialForm();
materialForm.value = { ...row };
materialDialog.visible = true;
materialDialog.title = '编辑合同物料';
}
async function handleDeleteMaterial(row: any) {
await proxy?.$modal.confirm('是否确认删除该合同物料?');
const list = form.value.changeMaterialList || [];
const idx = list.findIndex((item: any) => (item.changeMaterialId != null && item.changeMaterialId === row.changeMaterialId) || (item.contractMaterialId != null && item.contractMaterialId === row.contractMaterialId) || item === row);
if (idx !== -1) {
list.splice(idx, 1);
calculateTotalPrice();
}
proxy?.$modal.msgSuccess('删除成功');
}
function resetMaterialForm() {
materialForm.value = { ...initMaterialFormData };
materialFormRef.value?.resetFields();
}
function cancelMaterial() {
resetMaterialForm();
materialDialog.visible = false;
}
function calculateBeforePrice() {
const tax = Number(materialForm.value.taxRate);
const including = Number(materialForm.value.includingPrice);
if (!isNaN(tax) && !isNaN(including)) {
const divisor = 1 + tax / 100;
if (divisor > 0) materialForm.value.beforePrice = Number((including / divisor).toFixed(2));
}
calculateSubtotal();
}
function calculateSubtotal() {
const amount = Number(materialForm.value.amount);
const including = Number(materialForm.value.includingPrice);
if (!isNaN(amount) && !isNaN(including)) materialForm.value.subtotal = Number((amount * including).toFixed(2));
calculateTotalPrice();
}
function calculateTotalPrice() {
const list = form.value.changeMaterialList || [];
const total = list.reduce((sum: number, m: any) => sum + (Number(m.subtotal) || 0), 0);
if (form.value.changeInfo) form.value.changeInfo.totalPrice = Number(total.toFixed(2));
}
function submitMaterialForm() {
materialFormRef.value?.validate((valid: boolean) => {
if (!valid) return;
if (!form.value.changeMaterialList) form.value.changeMaterialList = [];
const list = form.value.changeMaterialList;
const unitInfo = unitInfoList.value.find((item: any) => item.unitId === materialForm.value.unitId);
const unitName = unitInfo ? unitInfo.unitName : '';
const data = { ...materialForm.value, unitName };
const id = materialForm.value.contractMaterialId ?? (materialForm.value as any).changeMaterialId;
const idx = id != null ? list.findIndex((item: any) => item.contractMaterialId === id || item.changeMaterialId === id) : -1;
if (idx !== -1) {
list[idx] = data;
} else {
list.push({ ...data, contractMaterialId: undefined, changeMaterialId: Date.now() });
}
calculateTotalPrice();
proxy?.$modal.msgSuccess('操作成功');
materialDialog.visible = false;
});
}
// ----- 变更后付款方式:增删改 -----
function handleAddPaymentMethod() {
resetPaymentMethodForm();
editingPaymentMethodIndex.value = null;
const list = form.value.changePaymentMethodList || [];
paymentMethodForm.value.sortOrder = list.length + 1;
paymentMethodDialog.visible = true;
paymentMethodDialog.title = '新增付款方式';
}
function handleEditPaymentMethod(row: any, rowIndex: number) {
resetPaymentMethodForm();
editingPaymentMethodIndex.value = rowIndex;
paymentMethodForm.value = { ...row };
const stage = paymentStageList.value.find((s) => s.paymentStageId === row.paymentStageId || String(s.paymentStageId) === String(row.paymentStageId));
if (stage?.paymentTemplate) paymentMethodTemplateRaw.value = stage.paymentTemplate;
paymentMethodDialog.visible = true;
paymentMethodDialog.title = '编辑付款方式';
}
async function handleDeletePaymentMethod(rowIndex: number) {
await proxy?.$modal.confirm('是否确认删除该付款方式?');
const list = form.value.changePaymentMethodList || [];
if (rowIndex >= 0 && rowIndex < list.length) {
list.splice(rowIndex, 1);
list.forEach((item: any, i: number) => (item.sortOrder = i + 1));
}
proxy?.$modal.msgSuccess('删除成功');
}
function resetPaymentMethodForm() {
paymentMethodForm.value = { ...initPaymentMethodFormData };
paymentMethodTemplateRaw.value = null;
paymentMethodFormRef.value?.resetFields();
}
function cancelPaymentMethod() {
resetPaymentMethodForm();
editingPaymentMethodIndex.value = null;
paymentMethodDialog.visible = false;
}
function submitPaymentMethodForm() {
paymentMethodFormRef.value?.validate((valid: boolean) => {
if (!valid) return;
if (!form.value.changePaymentMethodList) form.value.changePaymentMethodList = [];
const list = form.value.changePaymentMethodList;
const data = { ...paymentMethodForm.value };
data.paymentDescription = applyPaymentDescription(data.paymentDescription || '', data as Record<string, any>);
const editIndex = editingPaymentMethodIndex.value;
if (editIndex !== null && editIndex >= 0 && editIndex < list.length) {
list[editIndex] = data;
} else {
list.push({ ...data, paymentMethodId: undefined, changePaymentId: undefined });
}
editingPaymentMethodIndex.value = null;
list.forEach((item: any, i: number) => {
item.sortOrder = i + 1;
item.paymentDescription = applyPaymentDescription(item.paymentDescription || '', item);
});
proxy?.$modal.msgSuccess('操作成功');
paymentMethodDialog.visible = false;
});
}
function openSaleMaterialSelect() {
saleMaterialSelectRef.value?.open();
}
function saleMaterialSelectCallBack(data: any) {
const arr = Array.isArray(data) ? data : [data];
if (arr.length > 0) {
const m = arr[0];
materialForm.value.materialId = m.materialId;
materialForm.value.materialCode = m.materialCode;
materialForm.value.materialName = m.materialName;
materialForm.value.productName = m.saleMaterialName;
materialForm.value.saleMaterialName = m.saleMaterialName;
materialForm.value.relationMaterialId = m.relationMaterialId;
materialForm.value.customerName = m.customerName;
}
}
function buildPayload(changeStatus: '1' | '2'): ContractChangeSaveForm {
const f = form.value;
const payload: ContractChangeSaveForm = {
contractChangeId: f.contractChangeId,
contractId: f.contractId,
changeType: f.changeType,
changeReason: f.changeReason,
applyTime: f.applyTime || new Date().toISOString().slice(0, 19).replace('T', ' '),
remark: f.remark,
changeStatus,
activeFlag: '1',
contractCode: f.contractCode,
contractName: f.contractName,
originalCustomerName: f.originalCustomerName,
originalContractAmount: f.originalContractAmount,
changeContractCode: f.changeContractCode,
changeContractName: f.changeContractName,
customerName: f.customerName,
changeContractAmount: f.changeContractAmount
};
if (f.changeType === '1') {
payload.changeInfo = f.changeInfo;
payload.changeMaterialList = f.changeMaterialList;
payload.changePaymentMethodList = f.changePaymentMethodList;
}
return payload;
}
/** 审批栏:提交(暂存 / 提交审批) */
function submitForm(status: string, _mode: boolean) {
formRef.value?.validate((valid: boolean) => {
if (!valid) return;
if (form.value.changeType === '1' && !form.value.changeInfo) {
proxy?.$modal.msgError('内容变更请先选择合同,以加载变更后合同信息');
return;
}
buttonLoading.value = true;
if (status !== 'draft') {
// 提交审批:先保存再发起流程
const payload = buildPayload('2');
saveContractChange(payload)
.then((res) => {
const id = res.data;
if (id) form.value.contractChangeId = id;
form.value.changeStatus = '2';
form.value.flowStatus = 'waiting';
const variables = {
contractChangeId: id,
changeCode: form.value.changeCode || '',
contractName: form.value.contractName,
changeReason: form.value.changeReason
};
taskVariables.value = variables;
return startWorkFlow({
businessId: String(id),
flowCode: FlowCodeEnum.CONTRACT_CHANGE_CODE,
variables,
bizExt: { businessTitle: '合同变更', businessCode: form.value.changeCode || '合同变更' }
});
})
.then((resp) => {
if (resp?.data?.taskId && submitVerifyRef.value) {
return submitVerifyRef.value.openDialog(resp.data.taskId);
}
})
.then(() => {
proxy?.$modal.msgSuccess('提交成功');
goBack();
})
.finally(() => {
buttonLoading.value = false;
});
} else {
// 暂存
const payload = buildPayload('1');
saveContractChange(payload)
.then((res) => {
const id = res.data;
if (id) form.value.contractChangeId = id;
form.value.changeStatus = '1';
form.value.flowStatus = 'draft';
proxy?.$modal.msgSuccess('暂存成功');
goBack();
})
.finally(() => {
buttonLoading.value = false;
});
}
});
}
function submitCallback() {
goBack();
}
function handleApprovalRecord() {
approvalRecordRef.value?.init(form.value.contractChangeId);
}
async function approvalVerifyOpen() {
if (routeParams.value.taskId) await submitVerifyRef.value?.openDialog(routeParams.value.taskId);
}
onMounted(async () => {
routeParams.value = { ...(route.query as Record<string, any>) };
loadSelectOptions();
if (isEdit.value) {
await loadDetail();
} else {
form.value.changeType = '1';
form.value.flowStatus = 'draft';
}
});
</script>
<style scoped>
.contract-detail-panel {
border: 1px solid var(--el-border-color);
border-radius: 4px;
padding: 12px;
min-height: 200px;
}
.panel-title {
font-weight: 600;
margin-bottom: 8px;
}
.panel-placeholder {
color: var(--el-text-color-secondary);
font-size: 12px;
padding: 16px 0;
}
</style>