2991 lines
97 KiB
Vue
2991 lines
97 KiB
Vue
<template>
|
||
<a-modal
|
||
v-model:open="visible"
|
||
title="维护合同方信息"
|
||
width="96vw"
|
||
:footer="null"
|
||
:mask-closable="false"
|
||
:style="{ top: '12px' }"
|
||
:body-style="{
|
||
padding: 0,
|
||
height: 'calc(100vh - 120px)',
|
||
overflow: 'hidden',
|
||
}"
|
||
>
|
||
<div class="contract-main-editor">
|
||
<div class="editor-header">
|
||
<div><h3>合同主信息</h3></div>
|
||
<a-space>
|
||
<a-tag :color="creationMode === 'manual' ? 'orange' : 'blue'">
|
||
{{ creationMode === "manual" ? "手动创建" : "模板创建" }}
|
||
</a-tag>
|
||
<a-tag v-if="templateName" color="geekblue">
|
||
{{ templateName }}
|
||
</a-tag>
|
||
</a-space>
|
||
</div>
|
||
|
||
<div class="editor-body">
|
||
<a-form layout="vertical" :disabled="isViewMode" class="editor-form">
|
||
<section class="main-grid">
|
||
<div class="left-column">
|
||
<div class="section-card">
|
||
<div class="section-title-row">
|
||
<div class="section-title">合同主信息</div>
|
||
</div>
|
||
|
||
<div class="section-title-row counterparty-toolbar">
|
||
<div class="section-title">我方主体</div>
|
||
<a-button
|
||
v-if="!isViewMode && !isOurPartyLocked"
|
||
type="link"
|
||
class="text-action-btn"
|
||
@click="handleAddOurParty"
|
||
>
|
||
添加
|
||
</a-button>
|
||
</div>
|
||
<div class="party-empty-state" style="margin-bottom: 12px">
|
||
{{
|
||
isSingleAgreementChecked
|
||
? "单方协议默认使用当前登录主体,且不可修改"
|
||
: "当前登录主体必须包含在我方主体中"
|
||
}}
|
||
</div>
|
||
|
||
<div
|
||
v-for="party in ourParties"
|
||
:key="party.localId"
|
||
class="party-block"
|
||
>
|
||
<div class="party-block-header">
|
||
<div class="field-label required">
|
||
{{ party.displayName }}
|
||
</div>
|
||
<a-button
|
||
v-if="
|
||
!isViewMode &&
|
||
!isOurPartyLocked &&
|
||
ourParties.length > 1
|
||
"
|
||
type="link"
|
||
danger
|
||
class="text-action-btn"
|
||
@click="handleRemoveOurParty(party.localId)"
|
||
>
|
||
删除
|
||
</a-button>
|
||
</div>
|
||
<div class="search-input-row">
|
||
<a-select
|
||
v-model:value="party.companyName"
|
||
:options="getPartyOptions(party.roleCode)"
|
||
show-search
|
||
:disabled="isViewMode || isOurPartyLocked"
|
||
:placeholder="`请选择${party.displayName}`"
|
||
:filter-option="filterPartnerOption"
|
||
@change="
|
||
(value, option) =>
|
||
handleCompanySelect(party, value, option)
|
||
"
|
||
/>
|
||
</div>
|
||
<div class="party-info-box">
|
||
<div class="info-line">
|
||
<span>{{ party.displayName }}</span
|
||
><strong>{{ party.companyName || "/" }}</strong>
|
||
</div>
|
||
<div class="info-line">
|
||
<span>主体编码</span
|
||
><strong>{{ party.subjectCode || "/" }}</strong>
|
||
</div>
|
||
<div class="info-line">
|
||
<span>电话</span
|
||
><strong>{{ party.contactPhone || "/" }}</strong>
|
||
</div>
|
||
<div class="info-line">
|
||
<span>公司地址</span
|
||
><strong>{{ party.registeredAddress || "/" }}</strong>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section-title-row counterparty-toolbar">
|
||
<div class="section-title">相对方</div>
|
||
<a-button
|
||
v-if="!isViewMode && !isSingleAgreementChecked"
|
||
type="link"
|
||
class="text-action-btn"
|
||
@click="handleAddCounterparty"
|
||
>
|
||
添加
|
||
</a-button>
|
||
</div>
|
||
|
||
<template v-if="!isSingleAgreementChecked">
|
||
<div
|
||
v-for="party in counterpartyParties"
|
||
:key="party.localId"
|
||
class="party-block"
|
||
>
|
||
<div class="party-block-header">
|
||
<div class="field-label required">
|
||
{{ party.displayName }}
|
||
</div>
|
||
<a-button
|
||
v-if="!isViewMode"
|
||
type="link"
|
||
danger
|
||
class="text-action-btn"
|
||
@click="handleRemoveCounterparty(party.localId)"
|
||
>
|
||
删除
|
||
</a-button>
|
||
</div>
|
||
<div class="search-input-row">
|
||
<a-select
|
||
v-model:value="party.companyName"
|
||
:options="getPartyOptions(party.roleCode)"
|
||
show-search
|
||
:placeholder="`请选择${party.displayName}`"
|
||
:filter-option="filterPartnerOption"
|
||
@change="
|
||
(value, option) =>
|
||
handleCompanySelect(party, value, option)
|
||
"
|
||
/>
|
||
</div>
|
||
<div class="party-info-box">
|
||
<div class="info-line">
|
||
<span>{{ party.displayName }}</span
|
||
><strong>{{ party.companyName || "/" }}</strong>
|
||
</div>
|
||
<div class="info-line">
|
||
<span>电话</span
|
||
><strong>{{ party.contactPhone || "/" }}</strong>
|
||
</div>
|
||
<div class="info-line">
|
||
<span>公司地址</span
|
||
><strong>{{ party.registeredAddress || "/" }}</strong>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
v-if="!counterpartyParties.length"
|
||
class="party-empty-state"
|
||
>
|
||
暂无相对方,请点击右上角“添加”
|
||
</div>
|
||
</template>
|
||
<div v-else class="party-empty-state">
|
||
当前为单方协议,无需维护相对方
|
||
</div>
|
||
|
||
<div class="field-grid field-grid-2">
|
||
<a-form-item class="tight-item" label="合同名称" required>
|
||
<a-input
|
||
v-model:value="detailState.contractName"
|
||
placeholder="请输入"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item class="tight-item" label="合同号">
|
||
<a-input
|
||
v-model:value="detailState.contractCode"
|
||
placeholder="不填则系统自动生成"
|
||
:disabled="!!detailState.id"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item
|
||
class="tight-item field-span-2"
|
||
label="合同类型"
|
||
required
|
||
>
|
||
<a-select
|
||
v-model:value="detailState.contractTypeCode"
|
||
:options="contractTypeOptions"
|
||
placeholder="请选择合同类型"
|
||
/>
|
||
</a-form-item>
|
||
</div>
|
||
|
||
<div class="period-block">
|
||
<div class="field-label required">合同起止时间</div>
|
||
<a-radio-group v-model:value="detailState.contractPeriodType">
|
||
<a-radio value="0">固定期限</a-radio>
|
||
<a-radio value="1">无固定期限</a-radio>
|
||
</a-radio-group>
|
||
<div
|
||
class="period-row"
|
||
v-if="detailState.contractPeriodType === '0'"
|
||
>
|
||
<a-date-picker
|
||
v-model:value="detailState.effectiveStart"
|
||
value-format="YYYY-MM-DD"
|
||
placeholder="开始日期"
|
||
style="width: 48%"
|
||
/>
|
||
<a-date-picker
|
||
v-model:value="detailState.effectiveEnd"
|
||
value-format="YYYY-MM-DD"
|
||
placeholder="结束日期"
|
||
style="width: 48%"
|
||
/>
|
||
</div>
|
||
<a-input
|
||
v-else
|
||
v-model:value="detailState.periodExplain"
|
||
placeholder="期限说明"
|
||
class="period-explain-input"
|
||
/>
|
||
</div>
|
||
|
||
<div class="field-grid field-grid-2">
|
||
<a-form-item class="tight-item" label="计价方式" required>
|
||
<a-select
|
||
v-model:value="detailState.valuationMode"
|
||
:options="valuationModeOptions"
|
||
placeholder="请选择计价方式"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item class="tight-item" label="币种" required>
|
||
<a-select
|
||
v-model:value="detailState.currencyName"
|
||
:options="currencyOptions"
|
||
placeholder="请选择币种"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item
|
||
class="tight-item field-span-2"
|
||
label="签订方式"
|
||
required
|
||
>
|
||
<a-select
|
||
v-model:value="detailState.sealType"
|
||
:options="sealTypeOptions"
|
||
placeholder="请选择签订方式"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item class="tight-item field-span-2" label="委托类型">
|
||
<a-select
|
||
v-model:value="detailState.mandateType"
|
||
:options="mandateTypeOptions"
|
||
placeholder="请选择委托类型"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item class="tight-item field-span-2" label="货种">
|
||
<a-select
|
||
v-model:value="detailState.cargoType"
|
||
:options="cargoTypeOptions"
|
||
placeholder="请选择货种"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item
|
||
class="tight-item field-span-2"
|
||
label="付款方式"
|
||
required
|
||
>
|
||
<a-select
|
||
v-model:value="detailState.paymentMethod"
|
||
:options="paymentMethodOptions"
|
||
placeholder="请选择付款方式"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item class="tight-item" label="收支方向" required>
|
||
<a-select
|
||
v-model:value="detailState.paymentDirection"
|
||
:options="paymentDirectionOptions"
|
||
placeholder="请选择收支方向"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item class="tight-item" label="签署方式" required>
|
||
<a-select
|
||
v-model:value="detailState.isElectron"
|
||
:options="isElectronOptions"
|
||
placeholder="请选择签署方式"
|
||
/>
|
||
</a-form-item>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="right-column">
|
||
<div class="section-card">
|
||
<div class="basic-header">
|
||
<div class="basic-title">
|
||
<span class="dot"></span>
|
||
基本信息
|
||
</div>
|
||
</div>
|
||
|
||
<div class="summary-grid">
|
||
<div class="summary-line">
|
||
<span>合同名称</span
|
||
><strong>{{ detailState.contractName || "未填写" }}</strong>
|
||
</div>
|
||
<div class="summary-line">
|
||
<span>我方</span><strong>{{ ourPartySummaryText }}</strong>
|
||
</div>
|
||
<div class="summary-line">
|
||
<span>合同编号</span
|
||
><strong>{{ detailState.contractCode || "未填写" }}</strong>
|
||
</div>
|
||
<div class="summary-line">
|
||
<span>相对方</span
|
||
><strong>{{
|
||
isSingleAgreementChecked
|
||
? "单方协议,无相对方"
|
||
: counterpartySummaryText
|
||
}}</strong>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="upload-block">
|
||
<div class="upload-label">合同正文</div>
|
||
<div
|
||
class="readonly-upload-box contract-body-entry"
|
||
:class="{ 'is-clickable': !isViewMode }"
|
||
@click="handleBodyEntryClick"
|
||
>
|
||
<div class="contract-body-entry-name">
|
||
{{ contractBodyDisplayText }}
|
||
</div>
|
||
<div class="contract-body-entry-hint">
|
||
{{ contractBodyHint }}
|
||
</div>
|
||
</div>
|
||
<a-space
|
||
v-if="detailState.contractBodyFilePath"
|
||
style="margin-top: 8px"
|
||
>
|
||
<a-button
|
||
size="small"
|
||
:disabled="false"
|
||
@click="
|
||
downloadFile(
|
||
detailState.contractBodyFilePath,
|
||
detailState.contractBodyFileName || '合同正文.docx',
|
||
)
|
||
"
|
||
>
|
||
下载
|
||
</a-button>
|
||
</a-space>
|
||
</div>
|
||
|
||
<div v-if="!isSingleAgreementChecked" class="upload-block">
|
||
<div class="upload-label">签订依据附件</div>
|
||
<a-upload-dragger
|
||
:file-list="gistAttachmentList"
|
||
:before-upload="handleGistAttachmentUpload"
|
||
@remove="handleRemoveGistAttachment"
|
||
:disabled="isViewMode"
|
||
:show-upload-list="{ showRemoveIcon: !isViewMode }"
|
||
multiple
|
||
>
|
||
<div class="other-upload-text">
|
||
签订依据文件,对应法务 contractGistFile
|
||
</div>
|
||
</a-upload-dragger>
|
||
<div
|
||
v-if="detailState.contractGistFileList?.length"
|
||
class="download-list"
|
||
>
|
||
<a-space wrap>
|
||
<a-button
|
||
v-for="item in detailState.contractGistFileList"
|
||
:key="item.uid || item.filePath"
|
||
size="small"
|
||
:disabled="false"
|
||
@click="
|
||
downloadFile(
|
||
item.filePath,
|
||
item.name || item.fileName,
|
||
)
|
||
"
|
||
>
|
||
下载 {{ item.name || item.fileName }}
|
||
</a-button>
|
||
</a-space>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
v-if="!isSingleAgreementChecked && requiresAuthAttachment"
|
||
class="upload-block"
|
||
>
|
||
<div class="upload-label">授权委托书附件</div>
|
||
<a-upload-dragger
|
||
:file-list="authAttachmentUploadList"
|
||
:before-upload="handleAuthAttachmentUpload"
|
||
@remove="handleRemoveAuthAttachment"
|
||
:disabled="isViewMode"
|
||
:show-upload-list="{ showRemoveIcon: !isViewMode }"
|
||
multiple
|
||
>
|
||
<div class="other-upload-text">
|
||
当前签订方式需要上传授权委托书附件
|
||
</div>
|
||
</a-upload-dragger>
|
||
<div
|
||
v-if="detailState.authAttachmentList?.length"
|
||
class="download-list"
|
||
>
|
||
<a-space wrap>
|
||
<a-button
|
||
v-for="item in detailState.authAttachmentList"
|
||
:key="item.uid || item.filePath"
|
||
size="small"
|
||
:disabled="false"
|
||
@click="
|
||
downloadFile(
|
||
item.filePath,
|
||
item.name || item.fileName,
|
||
)
|
||
"
|
||
>
|
||
下载 {{ item.name || item.fileName }}
|
||
</a-button>
|
||
</a-space>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="upload-block">
|
||
<div class="upload-label">其他附件</div>
|
||
<a-upload-dragger
|
||
:file-list="otherAttachmentList"
|
||
:before-upload="handleOtherAttachmentUpload"
|
||
@remove="handleRemoveOtherAttachment"
|
||
:disabled="isViewMode"
|
||
:show-upload-list="{ showRemoveIcon: !isViewMode }"
|
||
multiple
|
||
>
|
||
<div class="other-upload-text">
|
||
其他附件(可上传多个文件)
|
||
</div>
|
||
</a-upload-dragger>
|
||
<div
|
||
v-if="detailState.otherAttachmentList?.length"
|
||
class="download-list"
|
||
>
|
||
<a-space wrap>
|
||
<a-button
|
||
v-for="item in detailState.otherAttachmentList"
|
||
:key="item.uid || item.filePath"
|
||
size="small"
|
||
:disabled="false"
|
||
@click="
|
||
downloadFile(
|
||
item.filePath,
|
||
item.name || item.fileName,
|
||
)
|
||
"
|
||
>
|
||
下载 {{ item.name || item.fileName }}
|
||
</a-button>
|
||
</a-space>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="detailState.signedFilePath" class="upload-block">
|
||
<div class="upload-label">已签署文件</div>
|
||
<div class="readonly-upload-box">
|
||
<div class="contract-body-entry-name">
|
||
{{ detailState.signedFileName || "已签署文件" }}
|
||
</div>
|
||
<div class="contract-body-entry-hint">
|
||
当前合同的签署完成版本
|
||
</div>
|
||
</div>
|
||
<a-space style="margin-top: 8px">
|
||
<a-button
|
||
size="small"
|
||
:disabled="false"
|
||
@click="
|
||
downloadFile(
|
||
detailState.signedFilePath,
|
||
detailState.signedFileName || '已签署合同.pdf',
|
||
)
|
||
"
|
||
>
|
||
下载
|
||
</a-button>
|
||
</a-space>
|
||
</div>
|
||
|
||
<a-form-item class="tight-item" label="主要内容">
|
||
<a-textarea
|
||
v-model:value="detailState.primaryContent"
|
||
:rows="2"
|
||
placeholder="选填"
|
||
/>
|
||
</a-form-item>
|
||
|
||
<a-form-item class="tight-item" label="正文/摘要">
|
||
<a-textarea
|
||
v-model:value="detailState.bodySummary"
|
||
:rows="2"
|
||
placeholder="选填,如不填则自动取主要内容"
|
||
/>
|
||
</a-form-item>
|
||
|
||
<a-form-item
|
||
class="tight-item"
|
||
label="金额说明"
|
||
:required="['0', '3'].includes(detailState.valuationMode)"
|
||
>
|
||
<a-textarea
|
||
v-model:value="detailState.amountExplain"
|
||
:rows="2"
|
||
placeholder='计价方式为"无金额"或"无法预估总价"时必填,说明金额相关情况'
|
||
/>
|
||
</a-form-item>
|
||
|
||
<a-form-item class="tight-item" label="签订依据说明">
|
||
<a-textarea
|
||
v-model:value="detailState.contractGistRemark"
|
||
:rows="2"
|
||
placeholder="选填,签订本合同的依据说明(如:根据XXXX号文件)"
|
||
/>
|
||
</a-form-item>
|
||
|
||
<div class="basic-title contract-attr-title">
|
||
<span class="dot"></span>
|
||
合同属性
|
||
</div>
|
||
|
||
<div class="field-grid field-grid-2">
|
||
<a-form-item class="tight-item" label="计价方式">
|
||
<a-input
|
||
:value="
|
||
getOptionLabel(
|
||
valuationModeOptions,
|
||
detailState.valuationMode,
|
||
)
|
||
"
|
||
readonly
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item class="tight-item" label="标的额">
|
||
<a-input-number
|
||
v-model:value="detailState.contractAmount"
|
||
:min="0"
|
||
:precision="2"
|
||
:controls="false"
|
||
style="width: 100%"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item
|
||
v-if="detailState.valuationMode === '2'"
|
||
class="tight-item"
|
||
label="预估金额"
|
||
required
|
||
>
|
||
<a-input-number
|
||
v-model:value="detailState.estimatedAmount"
|
||
:min="0"
|
||
:precision="2"
|
||
:controls="false"
|
||
style="width: 100%"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item class="tight-item" label="币种">
|
||
<a-input
|
||
:value="
|
||
getOptionLabel(
|
||
currencyOptions,
|
||
detailState.currencyName,
|
||
)
|
||
"
|
||
readonly
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item class="tight-item" label="签订方式">
|
||
<a-input
|
||
:value="
|
||
getOptionLabel(sealTypeOptions, detailState.sealType)
|
||
"
|
||
readonly
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item class="tight-item" label="文本来源" required>
|
||
<a-select
|
||
v-model:value="detailState.textSource"
|
||
:options="textSourceOptions"
|
||
placeholder="请选择文本来源"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item class="tight-item" label="合同来源" required>
|
||
<a-select
|
||
v-model:value="detailState.contractSource"
|
||
:options="contractSourceOptions"
|
||
placeholder="请选择合同来源"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item class="tight-item" label="协助部门">
|
||
<a-input
|
||
v-model:value="detailState.assistDept"
|
||
placeholder="协助部门"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item
|
||
class="tight-item field-span-2"
|
||
label="合同执行人"
|
||
required
|
||
>
|
||
<a-input
|
||
v-model:value="detailState.executorAccount"
|
||
placeholder="合同执行人手机号"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item class="tight-item" label="二级单位合同编码">
|
||
<a-input
|
||
v-model:value="detailState.selfCode"
|
||
placeholder="选填:二级单位内部合同编码,对应法务 selfCode"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item class="tight-item" label="主合同编码">
|
||
<a-input
|
||
v-model:value="detailState.mainContractCode"
|
||
placeholder="选填:补录/变更时关联主合同编码,对应法务 mainContractCode"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item class="tight-item" label="固定期限日期类型">
|
||
<a-select
|
||
v-model:value="detailState.dateType"
|
||
:options="dateTypeOptions"
|
||
placeholder="请选择"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item class="tight-item" label="签约主体编码">
|
||
<a-input
|
||
:value="signingSubjectCodeText"
|
||
placeholder="根据我方主体自动汇总主体编码"
|
||
readonly
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item
|
||
v-if="detailState.isPublicTender"
|
||
class="tight-item"
|
||
label="关联招标号"
|
||
required
|
||
>
|
||
<a-input
|
||
v-model:value="detailState.biddingNo"
|
||
placeholder="公开招标时必填"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item class="tight-item" label="开票时间">
|
||
<a-date-picker
|
||
v-model:value="detailState.invoiceDate"
|
||
value-format="YYYY-MM-DD"
|
||
style="width: 100%"
|
||
placeholder="选填,用于账单开票提醒"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item class="tight-item" label="收款时间">
|
||
<a-date-picker
|
||
v-model:value="detailState.receiveDate"
|
||
value-format="YYYY-MM-DD"
|
||
style="width: 100%"
|
||
placeholder="选填,用于应收账款提醒"
|
||
/>
|
||
</a-form-item>
|
||
|
||
</div>
|
||
|
||
<div class="boolean-grid">
|
||
<div class="boolean-row">
|
||
<span>是否需修订条款</span>
|
||
<a-radio-group v-model:value="detailState.needAmendments">
|
||
<a-radio value="1">是</a-radio>
|
||
<a-radio value="0">否</a-radio>
|
||
</a-radio-group>
|
||
</div>
|
||
<div class="boolean-row">
|
||
<span>是否涉及港澳台</span>
|
||
<a-radio-group
|
||
v-model:value="detailState.isInvolveHongKongMacaoTaiwan"
|
||
>
|
||
<a-radio value="1">是</a-radio>
|
||
<a-radio value="0">否</a-radio>
|
||
</a-radio-group>
|
||
</div>
|
||
<div class="boolean-row">
|
||
<span>是否公开招标</span>
|
||
<a-radio-group v-model:value="detailState.isPublicTender">
|
||
<a-radio value="1">是</a-radio>
|
||
<a-radio value="0">否</a-radio>
|
||
</a-radio-group>
|
||
</div>
|
||
<div class="boolean-row">
|
||
<span>签约主体是否涉外</span>
|
||
<a-radio-group
|
||
v-model:value="detailState.isForeignRelatedSubject"
|
||
>
|
||
<a-radio value="1">是</a-radio>
|
||
<a-radio value="0">否</a-radio>
|
||
</a-radio-group>
|
||
</div>
|
||
<div
|
||
v-if="detailState.paymentDirection === '1'"
|
||
class="boolean-row"
|
||
>
|
||
<span>是否列入当年预算</span>
|
||
<a-radio-group
|
||
v-model:value="detailState.isIncludedInCurrentYearBudget"
|
||
>
|
||
<a-radio value="1">是</a-radio>
|
||
<a-radio value="0">否</a-radio>
|
||
</a-radio-group>
|
||
</div>
|
||
<div class="boolean-row">
|
||
<span>是否共享</span>
|
||
<a-radio-group v-model:value="detailState.isShared">
|
||
<a-radio value="1">是</a-radio>
|
||
<a-radio value="0">否</a-radio>
|
||
</a-radio-group>
|
||
</div>
|
||
<div class="boolean-row">
|
||
<span>是否重大合同</span>
|
||
<a-radio-group v-model:value="detailState.isMajorContract">
|
||
<a-radio value="1">是</a-radio>
|
||
<a-radio value="0">否</a-radio>
|
||
</a-radio-group>
|
||
</div>
|
||
<div class="boolean-row">
|
||
<span>是否单方协议</span>
|
||
<a-radio-group
|
||
v-model:value="detailState.isSingleAgreement"
|
||
>
|
||
<a-radio value="1">是</a-radio>
|
||
<a-radio value="0">否</a-radio>
|
||
</a-radio-group>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<div class="extra-amount-section">
|
||
<div class="extra-label">额外金额</div>
|
||
<div class="extra-check-row">
|
||
<label
|
||
v-for="item in extraAmountFieldOptions"
|
||
:key="item.key"
|
||
class="extra-check-item"
|
||
>
|
||
<a-checkbox
|
||
:disabled="isViewMode"
|
||
v-model:checked="
|
||
detailState.extraAmounts[`${item.key}Enabled`]
|
||
"
|
||
>
|
||
{{ item.label }}
|
||
</a-checkbox>
|
||
</label>
|
||
</div>
|
||
<div class="extra-input-row">
|
||
<a-input-number
|
||
v-for="item in extraAmountFieldOptions"
|
||
:key="`${item.key}_input`"
|
||
v-model:value="detailState.extraAmounts[item.key]"
|
||
:disabled="
|
||
isViewMode ||
|
||
!detailState.extraAmounts[`${item.key}Enabled`]
|
||
"
|
||
:min="0"
|
||
:controls="false"
|
||
:precision="2"
|
||
:placeholder="`${item.label}金额`"
|
||
style="width: 24%"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="plan-section">
|
||
<div class="basic-title contract-attr-title">
|
||
<span class="dot"></span>
|
||
履行计划
|
||
</div>
|
||
<div class="plan-toolbar">
|
||
<a-button
|
||
v-if="!isViewMode"
|
||
type="dashed"
|
||
size="small"
|
||
@click="addPerformancePlan"
|
||
>
|
||
<PlusOutlined />
|
||
新增履行计划
|
||
</a-button>
|
||
</div>
|
||
<a-table
|
||
:data-source="detailState.performancePlans"
|
||
:pagination="false"
|
||
size="small"
|
||
:scroll="{ x: 780 }"
|
||
row-key="id"
|
||
class="plan-table"
|
||
>
|
||
<a-table-column title="履行事项" key="planMatter">
|
||
<template #default="{ record }">
|
||
<a-input
|
||
v-model:value="record.planMatter"
|
||
placeholder="如:第一笔付款"
|
||
/>
|
||
</template>
|
||
</a-table-column>
|
||
<a-table-column title="履约条件" key="planWhere">
|
||
<template #default="{ record }">
|
||
<a-input
|
||
v-model:value="record.planWhere"
|
||
placeholder="如:达到xx条件"
|
||
/>
|
||
</template>
|
||
</a-table-column>
|
||
<a-table-column title="节点日期" key="planTime" width="140">
|
||
<template #default="{ record }">
|
||
<a-date-picker
|
||
v-model:value="record.planTime"
|
||
value-format="YYYY-MM-DD"
|
||
style="width: 100%"
|
||
placeholder="选择日期"
|
||
/>
|
||
</template>
|
||
</a-table-column>
|
||
<a-table-column
|
||
title="提醒提前天数"
|
||
key="planDay"
|
||
width="130"
|
||
>
|
||
<template #default="{ record }">
|
||
<a-input-number
|
||
v-model:value="record.planDay"
|
||
:min="0"
|
||
:controls="false"
|
||
style="width: 100%"
|
||
placeholder="提前几天提醒"
|
||
/>
|
||
</template>
|
||
</a-table-column>
|
||
<a-table-column
|
||
title="款项金额"
|
||
key="planMoney"
|
||
width="130"
|
||
>
|
||
<template #default="{ record }">
|
||
<a-input-number
|
||
v-model:value="record.planMoney"
|
||
:min="0"
|
||
:controls="false"
|
||
style="width: 100%"
|
||
placeholder="请输入金额"
|
||
/>
|
||
</template>
|
||
</a-table-column>
|
||
<a-table-column
|
||
title="所属主体"
|
||
key="planSubjectName"
|
||
width="200"
|
||
>
|
||
<template #default="{ record }">
|
||
<a-select
|
||
v-model:value="record.planSubjectCode"
|
||
:options="planSubjectOptions"
|
||
show-search
|
||
allow-clear
|
||
placeholder="选择所属主体机构"
|
||
:filter-option="filterPartnerOption"
|
||
style="width: 100%"
|
||
@change="(value, option) => { record.planSubjectName = option?.label || ''; }"
|
||
/>
|
||
</template>
|
||
</a-table-column>
|
||
<a-table-column title="备注" key="planDetail">
|
||
<template #default="{ record }">
|
||
<a-input
|
||
v-model:value="record.planDetail"
|
||
placeholder="选填备注"
|
||
/>
|
||
</template>
|
||
</a-table-column>
|
||
<a-table-column
|
||
title="操作"
|
||
key="action"
|
||
width="80"
|
||
fixed="right"
|
||
>
|
||
<template #default="{ record }">
|
||
<a-button
|
||
type="link"
|
||
danger
|
||
size="small"
|
||
@click="removePerformancePlan(record.id)"
|
||
>
|
||
删除
|
||
</a-button>
|
||
</template>
|
||
</a-table-column>
|
||
<template #emptyText>
|
||
<div class="empty-plan-text">暂无数据</div>
|
||
</template>
|
||
</a-table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</a-form>
|
||
</div>
|
||
|
||
<div class="editor-footer">
|
||
<a-space>
|
||
<a-button v-if="!isViewMode" @click="resetCurrentState"
|
||
>重置</a-button
|
||
>
|
||
<a-button @click="visible = false">{{
|
||
isViewMode ? "关闭" : "取消"
|
||
}}</a-button>
|
||
<a-button v-if="!isViewMode" type="primary" @click="handleSave"
|
||
>保存</a-button
|
||
>
|
||
</a-space>
|
||
</div>
|
||
</div>
|
||
|
||
<ContractTemplateFillDemoModal
|
||
ref="bodyConfigModalRef"
|
||
@save="handleBodyConfigSave"
|
||
/>
|
||
</a-modal>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, ref } from "vue";
|
||
import { message } from "ant-design-vue";
|
||
import { PlusOutlined } from "@ant-design/icons-vue";
|
||
import ContractTemplateFillDemoModal from "./ContractTemplateFillDemoModal.vue";
|
||
import {
|
||
downloadLaunchFile,
|
||
getLaunchSelfProfile,
|
||
queryPartnerOptions,
|
||
uploadLaunchFile,
|
||
} from "@/api/launch.js";
|
||
import { getOrgTree } from "@/api/manifest.js";
|
||
import {
|
||
contractSourceOptions,
|
||
contractTypeOptions,
|
||
currencyOptions,
|
||
dateTypeOptions,
|
||
extraAmountFieldOptions,
|
||
isElectronOptions,
|
||
paymentDirectionOptions,
|
||
sealTypeOptions,
|
||
textSourceOptions,
|
||
valuationModeOptions,
|
||
getOptionLabel,
|
||
} from "../contractLaunchOptions.js";
|
||
|
||
const counterpartyOptions = ref([]);
|
||
const ourPartyOrgOptions = ref([]);
|
||
|
||
/** 履行计划所属主体下拉选项:从全部可选机构中选取 */
|
||
const planSubjectOptions = computed(() =>
|
||
ourPartyOrgOptions.value.map((opt) => ({
|
||
label: opt.raw?.orgName || opt.label || opt.value,
|
||
value: String(opt.raw?.orgId ?? ""),
|
||
})),
|
||
);
|
||
|
||
// ========== 合同相关字典 ==========
|
||
|
||
const mandateTypeOptions = [
|
||
{ label: "海铁联运委托", value: "海铁联运委托" },
|
||
{ label: "一站式物流委托", value: "一站式物流委托" },
|
||
{ label: "采购执行委托", value: "采购执行委托" },
|
||
{ label: "销售执行委托", value: "销售执行委托" },
|
||
];
|
||
|
||
const cargoTypeOptions = [
|
||
{ label: "燃料油", value: "燃料油" },
|
||
{ label: "港口设备", value: "港口设备" },
|
||
{ label: "软件服务", value: "软件服务" },
|
||
{ label: "综合物流", value: "综合物流" },
|
||
];
|
||
|
||
const paymentMethodOptions = [
|
||
{
|
||
label: "银行转账(30%预付款,70%验收后支付)",
|
||
value: "银行转账(30%预付款,70%验收后支付)",
|
||
},
|
||
{ label: "银行转账(按月结算)", value: "银行转账(按月结算)" },
|
||
{ label: "银行转账(一次性结清)", value: "银行转账(一次性结清)" },
|
||
];
|
||
|
||
const PARTY_ROLE_OUR = "OUR";
|
||
const PARTY_ROLE_OPPOSITE = "OPPOSITE";
|
||
|
||
function formatDateTimeValue(value, endOfDay = false) {
|
||
if (!value) return null;
|
||
if (String(value).includes(" ")) return value;
|
||
return `${value} ${endOfDay ? "23:59:59" : "00:00:00"}`;
|
||
}
|
||
|
||
function normalizeFileItem(item = {}) {
|
||
return {
|
||
uid:
|
||
item.uid ||
|
||
`file_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`,
|
||
name: item.name || item.fileName || "",
|
||
size: item.size ?? item.fileSize ?? 0,
|
||
status: item.status || "done",
|
||
filePath: item.filePath || "",
|
||
fileType: item.fileType || "",
|
||
fileCategory: item.fileCategory || "",
|
||
};
|
||
}
|
||
|
||
function normalizeBooleanValue(value) {
|
||
if (value === 1 || value === "1" || value === true || value === "Y" || value === "true")
|
||
return "1";
|
||
return "0";
|
||
}
|
||
|
||
function pickBooleanSource(source = {}, key) {
|
||
const mapping = {
|
||
// needAmendments 改为 String 1/0,不再走 boolean 映射
|
||
isInvolveHongKongMacaoTaiwan: [
|
||
source.isInvolveHongKongMacaoTaiwan,
|
||
source.isSgat,
|
||
],
|
||
isPublicTender: [source.isPublicTender, source.isOpenBidding],
|
||
isForeignRelatedSubject: [
|
||
source.isForeignRelatedSubject,
|
||
source.contractNature,
|
||
],
|
||
isIncludedInCurrentYearBudget: [
|
||
source.isIncludedInCurrentYearBudget,
|
||
source.isYearBudget,
|
||
],
|
||
isShared: [source.isShared, source.isShare],
|
||
isMajorContract: [source.isMajorContract],
|
||
};
|
||
const values = mapping[key] || [];
|
||
for (const value of values) {
|
||
if (value !== undefined && value !== null && value !== "") {
|
||
return value;
|
||
}
|
||
}
|
||
return undefined;
|
||
}
|
||
|
||
function getFileNameFromDetail(source, category) {
|
||
if (category === "body") {
|
||
return source.contractBodyFileName || source.contractTextFileName || "";
|
||
}
|
||
return "";
|
||
}
|
||
|
||
function splitFilesByCategory(source = {}) {
|
||
const sourceFiles = source.fileList || [];
|
||
const bodyFile =
|
||
sourceFiles.find((item) => item.fileCategory === "body") || {};
|
||
|
||
return {
|
||
contractBodyFileName:
|
||
source.contractBodyFileName ||
|
||
bodyFile.fileName ||
|
||
getFileNameFromDetail(source, "body") ||
|
||
"",
|
||
contractBodyFilePath:
|
||
source.contractBodyFilePath ||
|
||
bodyFile.filePath ||
|
||
source.contractTextFilePath ||
|
||
"",
|
||
contractBodyFileType:
|
||
source.contractBodyFileType ||
|
||
bodyFile.fileType ||
|
||
source.contractTextFileType ||
|
||
"",
|
||
contractBodyFileSize:
|
||
source.contractBodyFileSize ??
|
||
bodyFile.fileSize ??
|
||
source.contractTextFileSize ??
|
||
null,
|
||
otherAttachmentList: source.otherAttachmentList?.length
|
||
? source.otherAttachmentList.map(normalizeFileItem)
|
||
: sourceFiles
|
||
.filter((item) => item.fileCategory === "other")
|
||
.map(normalizeFileItem),
|
||
contractGistFileList: source.contractGistFileList?.length
|
||
? source.contractGistFileList.map(normalizeFileItem)
|
||
: sourceFiles
|
||
.filter((item) => item.fileCategory === "gist")
|
||
.map(normalizeFileItem),
|
||
authAttachmentList: source.authAttachmentList?.length
|
||
? source.authAttachmentList.map(normalizeFileItem)
|
||
: sourceFiles
|
||
.filter((item) => item.fileCategory === "auth")
|
||
.map(normalizeFileItem),
|
||
};
|
||
}
|
||
|
||
function buildExtraAmountPayload(extraAmounts = {}) {
|
||
const enabledItems = extraAmountFieldOptions.filter(
|
||
(item) => extraAmounts[`${item.key}Enabled`],
|
||
);
|
||
const result = {
|
||
ewAmountType: enabledItems.map((item) => item.code).join(","),
|
||
djAmount: 0,
|
||
yjAmount: 0,
|
||
yfkAmount: 0,
|
||
bzjAmount: 0,
|
||
};
|
||
|
||
enabledItems.forEach((item) => {
|
||
result[item.amountField] = Number(extraAmounts[item.key] || 0);
|
||
});
|
||
|
||
return result;
|
||
}
|
||
|
||
function restoreExtraAmounts(source = {}, current = {}) {
|
||
const result = { ...current, ...(source.extraAmounts || {}) };
|
||
const selectedCodes = String(source.ewAmountType || "")
|
||
.split(",")
|
||
.map((item) => item.trim())
|
||
.filter(Boolean);
|
||
|
||
extraAmountFieldOptions.forEach((item) => {
|
||
const amount = source[item.amountField];
|
||
if (selectedCodes.includes(item.code) || amount || amount === 0) {
|
||
result[`${item.key}Enabled`] =
|
||
selectedCodes.includes(item.code) || amount > 0;
|
||
if (amount !== undefined && amount !== null) {
|
||
result[item.key] = amount;
|
||
}
|
||
}
|
||
});
|
||
|
||
return result;
|
||
}
|
||
|
||
function createLocalId(prefix = "party") {
|
||
return `${prefix}_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`;
|
||
}
|
||
|
||
function createEmptyParty(roleCode, index = 0) {
|
||
const isOur = roleCode === PARTY_ROLE_OUR;
|
||
return {
|
||
localId: createLocalId(isOur ? "our" : "opposite"),
|
||
roleCode,
|
||
roleName: isOur ? "我方" : "相对方",
|
||
displayName: isOur ? `我方主体${index + 1}` : `相对方${index + 1}`,
|
||
subjectCode: "",
|
||
partnerId: null,
|
||
customerSysid: null,
|
||
oppositeId: "",
|
||
companyName: "",
|
||
companyType: "",
|
||
companyNature: "",
|
||
orgNature: "",
|
||
orgNatureLevel: "",
|
||
oppositeCharacter: "",
|
||
oppCharacter: "",
|
||
contactName: "",
|
||
contactPhone: "",
|
||
contactEmail: "",
|
||
contactAddress: "",
|
||
registeredAddress: "",
|
||
legalRepresentative: "",
|
||
creditCode: "",
|
||
registeredCapitalText: "",
|
||
countryName: "",
|
||
naturalPersonIdType: "",
|
||
naturalPersonIdNumber: "",
|
||
isGroupInternal: false,
|
||
isHongKongMacaoTaiwan: false,
|
||
};
|
||
}
|
||
|
||
function mapLaunchPartyToFormParty(item = {}) {
|
||
const roleCode =
|
||
String(item.roleCode || PARTY_ROLE_OPPOSITE).toUpperCase() ===
|
||
PARTY_ROLE_OUR
|
||
? PARTY_ROLE_OUR
|
||
: PARTY_ROLE_OPPOSITE;
|
||
return {
|
||
...createEmptyParty(roleCode),
|
||
...item,
|
||
localId:
|
||
item.localId ||
|
||
createLocalId(roleCode === PARTY_ROLE_OUR ? "our" : "opposite"),
|
||
roleCode,
|
||
roleName: roleCode === PARTY_ROLE_OUR ? "我方" : "相对方",
|
||
subjectCode: item.subjectCode || "",
|
||
oppCharacter: item.oppositeCharacter || item.oppCharacter || "",
|
||
oppositeCharacter: item.oppositeCharacter || item.oppCharacter || "",
|
||
displayName: "",
|
||
customerSysid: item.customerSysid || item.partnerId || null,
|
||
};
|
||
}
|
||
|
||
function buildPartyStateFromList(list = []) {
|
||
if (!Array.isArray(list) || !list.length) return null;
|
||
const result = {
|
||
ourPartyList: [],
|
||
counterpartyList: [],
|
||
};
|
||
list.forEach((item) => {
|
||
if (!item?.roleCode) return;
|
||
const mapped = mapLaunchPartyToFormParty(item);
|
||
if (mapped.roleCode === PARTY_ROLE_OUR) {
|
||
result.ourPartyList.push(mapped);
|
||
} else {
|
||
result.counterpartyList.push(mapped);
|
||
}
|
||
});
|
||
return normalizePartyState(result);
|
||
}
|
||
|
||
function isPartyFilled(party = {}) {
|
||
const ignoreKeys = new Set([
|
||
"localId",
|
||
"roleCode",
|
||
"roleName",
|
||
"displayName",
|
||
]);
|
||
return Object.entries(party).some(([key, value]) => {
|
||
if (ignoreKeys.has(key)) return false;
|
||
if (Array.isArray(value)) return value.length > 0;
|
||
if (typeof value === "number") return !Number.isNaN(value);
|
||
return String(value ?? "").trim() !== "";
|
||
});
|
||
}
|
||
|
||
function normalizePartyArray(list = [], roleCode) {
|
||
return (Array.isArray(list) ? list : [])
|
||
.filter((party) => isPartyFilled(party))
|
||
.map((party, index) => ({
|
||
...createEmptyParty(roleCode, index),
|
||
...party,
|
||
localId:
|
||
party.localId ||
|
||
createLocalId(roleCode === PARTY_ROLE_OUR ? "our" : "opposite"),
|
||
roleCode,
|
||
roleName: roleCode === PARTY_ROLE_OUR ? "我方" : "相对方",
|
||
displayName:
|
||
roleCode === PARTY_ROLE_OUR
|
||
? `我方主体${index + 1}`
|
||
: `相对方${index + 1}`,
|
||
}));
|
||
}
|
||
|
||
function normalizePartyState(partyState) {
|
||
const state = partyState || {};
|
||
const ourPartyList = normalizePartyArray(
|
||
Array.isArray(state.ourPartyList)
|
||
? state.ourPartyList
|
||
: state.partyA
|
||
? [state.partyA]
|
||
: [],
|
||
PARTY_ROLE_OUR,
|
||
);
|
||
const normalizedOurList = ourPartyList.length
|
||
? ourPartyList
|
||
: [
|
||
{
|
||
...createEmptyParty(PARTY_ROLE_OUR, 0),
|
||
},
|
||
];
|
||
const counterpartyList = normalizePartyArray(
|
||
state.counterpartyList,
|
||
PARTY_ROLE_OPPOSITE,
|
||
);
|
||
return {
|
||
partyA: { ...normalizedOurList[0] },
|
||
ourPartyList: normalizedOurList,
|
||
counterpartyList,
|
||
isThreePartyContract: counterpartyList.length > 1,
|
||
};
|
||
}
|
||
|
||
function applySubjectCodesToPartyState(partyState, signingSubjectCode) {
|
||
const state = normalizePartyState(partyState);
|
||
const codes = String(signingSubjectCode || "")
|
||
.split(",")
|
||
.map((item) => item.trim())
|
||
.filter(Boolean);
|
||
state.ourPartyList = state.ourPartyList.map((party, index) => ({
|
||
...party,
|
||
subjectCode:
|
||
party.subjectCode || (codes.length === 1 ? codes[0] : codes[index] || ""),
|
||
}));
|
||
state.partyA = state.ourPartyList[0] || createEmptyParty(PARTY_ROLE_OUR, 0);
|
||
return state;
|
||
}
|
||
|
||
function copyPartyState(input) {
|
||
const source = input || {};
|
||
const listState = buildPartyStateFromList(source.partyList);
|
||
if (listState) {
|
||
return applySubjectCodesToPartyState(
|
||
listState,
|
||
source.signingSubjectCode ?? "",
|
||
);
|
||
}
|
||
return normalizePartyState({
|
||
ourPartyList: source.ourPartyList || (source.partyA ? [source.partyA] : []),
|
||
counterpartyList: source.counterpartyList || [],
|
||
isThreePartyContract: source.isThreePartyContract,
|
||
});
|
||
}
|
||
/** 创建一条空的履行计划 */
|
||
function createPerformancePlan() {
|
||
return {
|
||
id: `plan_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`,
|
||
planMatter: "",
|
||
planWhere: "",
|
||
planTime: "",
|
||
planDay: 0,
|
||
planMoney: "",
|
||
planSubjectCode: "",
|
||
planSubjectName: "",
|
||
executorUnitId: null,
|
||
planDetail: "",
|
||
};
|
||
}
|
||
|
||
/** 创建空白的合同表单数据 */
|
||
function createContractFormData() {
|
||
return {
|
||
id: null,
|
||
contractSysId: null,
|
||
contractName: "",
|
||
contractCode: "",
|
||
contractTypeCode: undefined,
|
||
contractAmount: null,
|
||
estimatedAmount: null,
|
||
effectiveStart: "",
|
||
effectiveEnd: "",
|
||
contractPeriodType: "0",
|
||
periodExplain: "",
|
||
paymentMethod: undefined,
|
||
paymentDirection: "0",
|
||
isElectron: "1",
|
||
textSource: "standardText",
|
||
contractSource: "ECP",
|
||
valuationMode: undefined,
|
||
currencyName: undefined,
|
||
sealType: undefined,
|
||
mandateType: undefined,
|
||
cargoType: undefined,
|
||
primaryContent: "",
|
||
amountExplain: "",
|
||
assistDept: "",
|
||
executorAccount: "",
|
||
selfCode: "",
|
||
mainContractCode: "",
|
||
dateType: "0",
|
||
signingSubjectCode: "",
|
||
contractGistRemark: "",
|
||
bodySummary: "",
|
||
invoiceDate: "",
|
||
receiveDate: "",
|
||
biddingNo: "",
|
||
contractBodyFileName: "",
|
||
contractBodyFilePath: "",
|
||
contractBodyFileType: "",
|
||
contractBodyFileSize: null,
|
||
contractBodyArrayBuffer: null,
|
||
otherAttachmentList: [],
|
||
contractGistFileList: [],
|
||
authAttachmentList: [],
|
||
// 正文模板字段值统一放这里,后续新增模板字段时尽量只改 templateFieldList
|
||
templateFieldValues: {},
|
||
partyState: {
|
||
isThreePartyContract: false,
|
||
ourPartyList: [
|
||
{
|
||
...createEmptyParty(PARTY_ROLE_OUR, 0),
|
||
},
|
||
],
|
||
counterpartyList: [],
|
||
},
|
||
isSingleAgreement: "0",
|
||
isInvolveHongKongMacaoTaiwan: "0",
|
||
isPublicTender: "0",
|
||
isForeignRelatedSubject: "0",
|
||
isIncludedInCurrentYearBudget: "0",
|
||
isShared: "0",
|
||
isMajorContract: "0",
|
||
needAmendments: "0",
|
||
extraAmounts: {
|
||
depositEnabled: false,
|
||
deposit: 0,
|
||
pledgeEnabled: false,
|
||
pledge: 0,
|
||
advanceEnabled: false,
|
||
advance: 0,
|
||
guaranteeEnabled: false,
|
||
guarantee: 0,
|
||
},
|
||
ewAmountType: "",
|
||
djAmount: 0,
|
||
yjAmount: 0,
|
||
yfkAmount: 0,
|
||
bzjAmount: 0,
|
||
performancePlans: [],
|
||
};
|
||
}
|
||
|
||
/** 深拷贝合同表单数据 */
|
||
function copyContractFormData(data = {}) {
|
||
const form = createContractFormData();
|
||
const source = data || {};
|
||
const fileState = splitFilesByCategory(source);
|
||
Object.keys(form).forEach((key) => {
|
||
if (key === "extraAmounts") {
|
||
form.extraAmounts = restoreExtraAmounts(source, form.extraAmounts);
|
||
} else if (key === "templateFieldValues") {
|
||
if (
|
||
source.templateFieldValues &&
|
||
typeof source.templateFieldValues === "object"
|
||
) {
|
||
form.templateFieldValues = { ...source.templateFieldValues };
|
||
} else if (source.templateFieldValuesJson) {
|
||
try {
|
||
form.templateFieldValues = JSON.parse(source.templateFieldValuesJson);
|
||
} catch (error) {
|
||
form.templateFieldValues = {};
|
||
}
|
||
} else {
|
||
form.templateFieldValues = {};
|
||
}
|
||
} else if (key === "performancePlans") {
|
||
form.performancePlans = (source.performancePlans || []).map((item) => ({
|
||
...createPerformancePlan(),
|
||
...item,
|
||
}));
|
||
} else if (key === "partyState") {
|
||
form.partyState = copyPartyState(source.partyState || source);
|
||
} else if (key === "contractSysId") {
|
||
form.contractSysId = source.contractSysId || null;
|
||
} else if (key === "otherAttachmentList") {
|
||
form.otherAttachmentList = fileState.otherAttachmentList;
|
||
} else if (key === "contractGistFileList") {
|
||
form.contractGistFileList = fileState.contractGistFileList;
|
||
} else if (key === "authAttachmentList") {
|
||
form.authAttachmentList = fileState.authAttachmentList;
|
||
} else if (
|
||
[
|
||
"contractBodyFileName",
|
||
"contractBodyFilePath",
|
||
"contractBodyFileType",
|
||
"contractBodyFileSize",
|
||
].includes(key)
|
||
) {
|
||
form[key] = fileState[key] ?? form[key];
|
||
} else if (key === "contractBodyArrayBuffer") {
|
||
form.contractBodyArrayBuffer = source.contractBodyArrayBuffer
|
||
? source.contractBodyArrayBuffer instanceof ArrayBuffer
|
||
? source.contractBodyArrayBuffer.slice(0)
|
||
: null
|
||
: null;
|
||
} else if (
|
||
key === "invoiceDate" ||
|
||
key === "receiveDate"
|
||
) {
|
||
form[key] = source[key] ? String(source[key]).slice(0, 10) : form[key];
|
||
} else if (key === "assistDept") {
|
||
form.assistDept =
|
||
source.assistDept ?? source.assistDepartmentName ?? form.assistDept;
|
||
} else if (key === "signingSubjectCode") {
|
||
form.signingSubjectCode =
|
||
source.signingSubjectCode ?? source.orgId ?? form.signingSubjectCode;
|
||
} else if (key === "isSingleAgreement") {
|
||
form.isSingleAgreement =
|
||
source.isSingleAgreement ?? form.isSingleAgreement;
|
||
} else if (
|
||
[
|
||
"isInvolveHongKongMacaoTaiwan",
|
||
"isPublicTender",
|
||
"isForeignRelatedSubject",
|
||
"isIncludedInCurrentYearBudget",
|
||
"isShared",
|
||
"isMajorContract",
|
||
].includes(key)
|
||
) {
|
||
form[key] = normalizeBooleanValue(pickBooleanSource(source, key));
|
||
} else {
|
||
form[key] = source[key] ?? form[key];
|
||
}
|
||
});
|
||
if (source.textSource) {
|
||
form.textSource = source.textSource;
|
||
}
|
||
if (source.contractTextFileName && !form.contractBodyFileName) {
|
||
form.contractBodyFileName = source.contractTextFileName;
|
||
}
|
||
// 兼容旧数据:如果只有 contractType(中文名)没有 contractTypeCode,尝试反向查找编码
|
||
if (!form.contractTypeCode && source.contractType) {
|
||
const matched = contractTypeOptions.find(
|
||
(opt) => opt.label === source.contractType || opt.simpleLabel === source.contractType,
|
||
);
|
||
if (matched) form.contractTypeCode = matched.value;
|
||
}
|
||
return form;
|
||
}
|
||
|
||
const emit = defineEmits(["save"]);
|
||
|
||
const visible = ref(false);
|
||
const modalMode = ref("create");
|
||
const creationMode = ref("template");
|
||
const templateKey = ref("");
|
||
const templateName = ref("");
|
||
const detailState = ref(createContractFormData());
|
||
const otherAttachmentList = ref([]);
|
||
const gistAttachmentList = ref([]);
|
||
const authAttachmentUploadList = ref([]);
|
||
const bodyConfigModalRef = ref(null);
|
||
const currentSelfParty = ref(null);
|
||
|
||
const isViewMode = computed(() => modalMode.value === "view");
|
||
|
||
const isSingleAgreementChecked = computed(
|
||
() => String(detailState.value.isSingleAgreement || "0") === "1",
|
||
);
|
||
|
||
const isOurPartyLocked = computed(() => isSingleAgreementChecked.value);
|
||
|
||
const ourParties = computed(
|
||
() => detailState.value.partyState.ourPartyList || [],
|
||
);
|
||
|
||
const counterpartyParties = computed(
|
||
() => detailState.value.partyState.counterpartyList || [],
|
||
);
|
||
|
||
const counterpartySummaryText = computed(() =>
|
||
counterpartyParties.value.length
|
||
? counterpartyParties.value
|
||
.map((party) => party.companyName || party.displayName)
|
||
.join(";")
|
||
: "未填写",
|
||
);
|
||
|
||
const ourPartySummaryText = computed(() =>
|
||
ourParties.value.length
|
||
? ourParties.value
|
||
.map((party) => party.companyName || party.displayName)
|
||
.join(";")
|
||
: "未填写",
|
||
);
|
||
|
||
const signingSubjectCodeText = computed(() =>
|
||
ourParties.value
|
||
.map((party) => String(party.subjectCode || "").trim())
|
||
.filter(Boolean)
|
||
.join(","),
|
||
);
|
||
|
||
const currentSelfSubjectCode = computed(() =>
|
||
String(currentSelfParty.value?.subjectCode || "").trim(),
|
||
);
|
||
|
||
const contractBodyDisplayText = computed(
|
||
() =>
|
||
detailState.value.contractBodyFileName ||
|
||
(creationMode.value === "manual"
|
||
? "点击上传合同正文 Word 文件"
|
||
: templateName.value || "点击配置合同正文"),
|
||
);
|
||
|
||
const contractBodyHint = computed(() =>
|
||
creationMode.value === "manual"
|
||
? "当前为手动创建,支持上传并预览 .docx 正文"
|
||
: templateName.value
|
||
? `当前模板:${templateName.value},点击继续维护模板信息和预览`
|
||
: "当前为模板创建,点击维护模板信息和正文预览",
|
||
);
|
||
|
||
const requiresAuthAttachment = computed(() =>
|
||
["GZBSQRQZ", "HTZYZBSQRQZ"].includes(detailState.value.sealType),
|
||
);
|
||
|
||
function forceSingleAgreementDefaults() {
|
||
if (!isSingleAgreementChecked.value || !currentSelfParty.value) return;
|
||
detailState.value.isElectron = "0";
|
||
detailState.value.sealType = "YWZ";
|
||
detailState.value.partyState.ourPartyList = [
|
||
{
|
||
...createEmptyParty(PARTY_ROLE_OUR, 0),
|
||
...currentSelfParty.value,
|
||
localId:
|
||
detailState.value.partyState.ourPartyList?.[0]?.localId ||
|
||
createLocalId("our"),
|
||
},
|
||
];
|
||
detailState.value.partyState.counterpartyList = [];
|
||
refreshPartyDisplayMeta();
|
||
}
|
||
|
||
function syncUploadLists() {
|
||
otherAttachmentList.value = (detailState.value.otherAttachmentList || []).map(
|
||
(item, index) => ({
|
||
uid: item.uid || `other_${index + 1}`,
|
||
name: item.name || item.fileName,
|
||
size: item.size ?? item.fileSize,
|
||
status: item.status || "done",
|
||
}),
|
||
);
|
||
gistAttachmentList.value = (detailState.value.contractGistFileList || []).map(
|
||
(item, index) => ({
|
||
uid: item.uid || `gist_${index + 1}`,
|
||
name: item.name || item.fileName,
|
||
size: item.size ?? item.fileSize,
|
||
status: item.status || "done",
|
||
}),
|
||
);
|
||
authAttachmentUploadList.value = (
|
||
detailState.value.authAttachmentList || []
|
||
).map((item, index) => ({
|
||
uid: item.uid || `auth_${index + 1}`,
|
||
name: item.name || item.fileName,
|
||
size: item.size ?? item.fileSize,
|
||
status: item.status || "done",
|
||
}));
|
||
}
|
||
|
||
function syncCounterpartyState() {
|
||
detailState.value.partyState = normalizePartyState(
|
||
detailState.value.partyState,
|
||
);
|
||
refreshPartyDisplayMeta();
|
||
}
|
||
|
||
function refreshPartyDisplayMeta() {
|
||
detailState.value.partyState.ourPartyList = (
|
||
detailState.value.partyState.ourPartyList || []
|
||
).map((party, index) => ({
|
||
...party,
|
||
roleCode: PARTY_ROLE_OUR,
|
||
roleName: "我方",
|
||
displayName: `我方主体${index + 1}`,
|
||
}));
|
||
detailState.value.partyState.counterpartyList = (
|
||
detailState.value.partyState.counterpartyList || []
|
||
).map((party, index) => ({
|
||
...party,
|
||
roleCode: PARTY_ROLE_OPPOSITE,
|
||
roleName: "相对方",
|
||
displayName: `相对方${index + 1}`,
|
||
}));
|
||
detailState.value.partyState.partyA =
|
||
detailState.value.partyState.ourPartyList[0] ||
|
||
createEmptyParty(PARTY_ROLE_OUR, 0);
|
||
detailState.value.partyState.isThreePartyContract =
|
||
detailState.value.partyState.counterpartyList.length > 1;
|
||
detailState.value.signingSubjectCode = signingSubjectCodeText.value;
|
||
}
|
||
|
||
function resetCurrentState() {
|
||
creationMode.value = "template";
|
||
templateKey.value = "";
|
||
templateName.value = "";
|
||
detailState.value = createContractFormData();
|
||
syncCounterpartyState();
|
||
syncUploadLists();
|
||
}
|
||
|
||
function filterPartnerOption(input, option) {
|
||
const keyword = String(input || "")
|
||
.trim()
|
||
.toLowerCase();
|
||
if (!keyword) return true;
|
||
return [
|
||
option?.label,
|
||
option?.value,
|
||
option?.raw?.oppositeCode,
|
||
option?.raw?.oppositeId,
|
||
]
|
||
.filter(Boolean)
|
||
.some((item) => String(item).toLowerCase().includes(keyword));
|
||
}
|
||
|
||
function getRelativeTypeLabel(value) {
|
||
const map = {
|
||
client: "客户",
|
||
supplier: "供应商",
|
||
Partner: "合作伙伴",
|
||
clientAndSup: "客户及供应商",
|
||
other: "其他",
|
||
};
|
||
return map[value] || value || "";
|
||
}
|
||
|
||
function isSyncedCounterpartyOption(option) {
|
||
const raw = option?.raw || {};
|
||
return raw.syncStatus === "synced" && !!String(raw.oppositeId || "").trim();
|
||
}
|
||
|
||
function mapPartnerToParty(raw = {}, roleKey, companyName) {
|
||
const firstContact = raw.contacts?.[0] || {};
|
||
const roleCode =
|
||
roleKey === PARTY_ROLE_OUR ? PARTY_ROLE_OUR : PARTY_ROLE_OPPOSITE;
|
||
return {
|
||
...createEmptyParty(roleCode),
|
||
partnerId: raw.customerSysid || null,
|
||
customerSysid: raw.customerSysid || null,
|
||
subjectCode: "",
|
||
oppositeId: raw.oppositeId || "",
|
||
companyName: raw.customerNameC || companyName || "",
|
||
companyType: getRelativeTypeLabel(raw.relativeType),
|
||
companyNature: raw.oppositeCharacter || "",
|
||
orgNature: raw.natureOfEnterpriseOrg || "",
|
||
orgNatureLevel: raw.natureOfEnterpriseOrgLev || "",
|
||
oppositeCharacter: raw.oppositeCharacter || "",
|
||
oppCharacter: raw.oppositeCharacter || "",
|
||
creditCode: raw.customerTaxno || "",
|
||
legalRepresentative: raw.legalPersonName || "",
|
||
registeredAddress: raw.customerRegaddr || "",
|
||
contactName: firstContact.contactName || "",
|
||
contactPhone: firstContact.contactPhone || raw.officephone || "",
|
||
contactEmail: firstContact.email || raw.emailAddress || "",
|
||
contactAddress: firstContact.postalAddress || raw.customerAddress || "",
|
||
registeredCapitalText: raw.regCapital || raw.registeredCapital || "",
|
||
countryName: raw.countryName || raw.country || "",
|
||
naturalPersonIdType: raw.certificateType || "",
|
||
naturalPersonIdNumber: raw.certificateNum || "",
|
||
isGroupInternal: raw.groupFlag === "Y" || raw.isJtn === "1",
|
||
isHongKongMacaoTaiwan: raw.gatFlag === "Y" || raw.isSgat === "1",
|
||
roleCode,
|
||
roleName: roleCode === PARTY_ROLE_OUR ? "我方" : "相对方",
|
||
};
|
||
}
|
||
|
||
function mapOrgNodeToParty(raw = {}) {
|
||
return {
|
||
...createEmptyParty(PARTY_ROLE_OUR),
|
||
subjectCode: raw.orgId == null ? "" : String(raw.orgId),
|
||
companyName: raw.orgName || "",
|
||
companyNature: raw.companyNature || "1",
|
||
orgNature: raw.orgNature || "QY",
|
||
orgNatureLevel: raw.orgNatureLevel || "",
|
||
contactPhone: raw.contactPhone || "",
|
||
contactEmail: raw.contactEmail || "",
|
||
contactAddress: raw.contactAddress || "",
|
||
registeredAddress: raw.registeredAddress || "",
|
||
legalRepresentative: raw.legalRepresentative || "",
|
||
creditCode: raw.creditCode || "",
|
||
registeredCapitalText: raw.regCapital || raw.registeredCapital || "",
|
||
countryName: raw.countryName || raw.country || "",
|
||
isGroupInternal: true,
|
||
isHongKongMacaoTaiwan: false,
|
||
roleCode: PARTY_ROLE_OUR,
|
||
roleName: "我方",
|
||
};
|
||
}
|
||
|
||
function getPartyOptions(roleCode) {
|
||
if (roleCode !== PARTY_ROLE_OUR) {
|
||
return (counterpartyOptions.value || []).filter(isSyncedCounterpartyOption);
|
||
}
|
||
return ourPartyOrgOptions.value || [];
|
||
}
|
||
|
||
function handleCompanySelect(party, companyName, option = {}) {
|
||
if (!party) return;
|
||
|
||
const optionList =
|
||
party.roleCode === PARTY_ROLE_OUR
|
||
? ourPartyOrgOptions.value
|
||
: counterpartyOptions.value;
|
||
const rawData =
|
||
option?.raw || optionList.find((item) => item.value === companyName)?.raw;
|
||
if (rawData) {
|
||
Object.assign(
|
||
party,
|
||
party.roleCode === PARTY_ROLE_OUR
|
||
? mapOrgNodeToParty(rawData)
|
||
: mapPartnerToParty(rawData, party.roleCode, companyName),
|
||
);
|
||
refreshPartyDisplayMeta();
|
||
return;
|
||
}
|
||
|
||
party.companyName = companyName;
|
||
refreshPartyDisplayMeta();
|
||
}
|
||
|
||
function handleAddOurParty() {
|
||
if (isOurPartyLocked.value) return;
|
||
detailState.value.partyState.ourPartyList =
|
||
detailState.value.partyState.ourPartyList || [];
|
||
detailState.value.partyState.ourPartyList.push(
|
||
createEmptyParty(
|
||
PARTY_ROLE_OUR,
|
||
detailState.value.partyState.ourPartyList.length,
|
||
),
|
||
);
|
||
refreshPartyDisplayMeta();
|
||
}
|
||
|
||
function handleContractTypeChange(value) {
|
||
detailState.value.contractTypeCode = value;
|
||
}
|
||
|
||
function handleAddCounterparty() {
|
||
if (isSingleAgreementChecked.value) return;
|
||
detailState.value.partyState.counterpartyList =
|
||
detailState.value.partyState.counterpartyList || [];
|
||
detailState.value.partyState.counterpartyList.push(
|
||
createEmptyParty(
|
||
PARTY_ROLE_OPPOSITE,
|
||
detailState.value.partyState.counterpartyList.length,
|
||
),
|
||
);
|
||
refreshPartyDisplayMeta();
|
||
}
|
||
|
||
function handleRemoveOurParty(localId) {
|
||
if (isOurPartyLocked.value) return;
|
||
detailState.value.partyState.ourPartyList = (
|
||
detailState.value.partyState.ourPartyList || []
|
||
).filter((item) => item.localId !== localId);
|
||
if (!detailState.value.partyState.ourPartyList.length) {
|
||
detailState.value.partyState.ourPartyList = [
|
||
createEmptyParty(PARTY_ROLE_OUR, 0),
|
||
];
|
||
}
|
||
refreshPartyDisplayMeta();
|
||
}
|
||
|
||
function handleRemoveCounterparty(localId) {
|
||
if (isSingleAgreementChecked.value) return;
|
||
detailState.value.partyState.counterpartyList = (
|
||
detailState.value.partyState.counterpartyList || []
|
||
).filter((item) => item.localId !== localId);
|
||
refreshPartyDisplayMeta();
|
||
}
|
||
|
||
function handleOtherAttachmentUpload(file) {
|
||
return uploadAttachmentFile(file, "other");
|
||
}
|
||
|
||
function handleGistAttachmentUpload(file) {
|
||
return uploadAttachmentFile(file, "gist");
|
||
}
|
||
|
||
function handleAuthAttachmentUpload(file) {
|
||
return uploadAttachmentFile(file, "auth");
|
||
}
|
||
|
||
function uploadAttachmentFile(file, category) {
|
||
const formData = new FormData();
|
||
formData.append("file", file);
|
||
uploadLaunchFile(formData).then((res) => {
|
||
if (String(res?.code || "") !== "0000") {
|
||
message.error(res?.message || res?.msg || "附件上传失败");
|
||
return;
|
||
}
|
||
const item = {
|
||
uid: `other_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`,
|
||
name: file.name,
|
||
size: file.size,
|
||
status: "done",
|
||
filePath: res?.result || "",
|
||
fileType: file.name.split(".").pop()?.toLowerCase() || "",
|
||
fileCategory: category,
|
||
};
|
||
if (category === "gist") {
|
||
detailState.value.contractGistFileList.push(item);
|
||
gistAttachmentList.value.push(item);
|
||
} else if (category === "auth") {
|
||
detailState.value.authAttachmentList.push(item);
|
||
authAttachmentUploadList.value.push(item);
|
||
} else {
|
||
detailState.value.otherAttachmentList.push(item);
|
||
otherAttachmentList.value.push(item);
|
||
}
|
||
message.success(`已添加附件:${file.name}`);
|
||
});
|
||
return false;
|
||
}
|
||
|
||
function handleRemoveOtherAttachment(file) {
|
||
detailState.value.otherAttachmentList =
|
||
detailState.value.otherAttachmentList.filter(
|
||
(item) => item.uid !== file.uid && item.name !== file.name,
|
||
);
|
||
otherAttachmentList.value = otherAttachmentList.value.filter(
|
||
(item) => item.uid !== file.uid && item.name !== file.name,
|
||
);
|
||
}
|
||
|
||
function handleRemoveGistAttachment(file) {
|
||
detailState.value.contractGistFileList =
|
||
detailState.value.contractGistFileList.filter(
|
||
(item) => item.uid !== file.uid && item.name !== file.name,
|
||
);
|
||
gistAttachmentList.value = gistAttachmentList.value.filter(
|
||
(item) => item.uid !== file.uid && item.name !== file.name,
|
||
);
|
||
}
|
||
|
||
function handleRemoveAuthAttachment(file) {
|
||
detailState.value.authAttachmentList =
|
||
detailState.value.authAttachmentList.filter(
|
||
(item) => item.uid !== file.uid && item.name !== file.name,
|
||
);
|
||
authAttachmentUploadList.value = authAttachmentUploadList.value.filter(
|
||
(item) => item.uid !== file.uid && item.name !== file.name,
|
||
);
|
||
}
|
||
|
||
async function downloadFile(filePath, fileName = "附件") {
|
||
if (!filePath) {
|
||
message.warning("文件路径为空,无法下载");
|
||
return;
|
||
}
|
||
try {
|
||
const resp = await downloadLaunchFile(filePath);
|
||
const arrayBuffer = resp?.data ?? resp;
|
||
const blob = new Blob([arrayBuffer]);
|
||
const url = window.URL.createObjectURL(blob);
|
||
const link = document.createElement("a");
|
||
link.href = url;
|
||
link.download = fileName || "附件";
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
window.URL.revokeObjectURL(url);
|
||
} catch (error) {
|
||
message.error("文件下载失败");
|
||
}
|
||
}
|
||
|
||
function openBodyConfigModal() {
|
||
bodyConfigModalRef.value?.showModal(
|
||
{
|
||
detailState: copyContractFormData(detailState.value),
|
||
creationMode: creationMode.value,
|
||
templateKey: templateKey.value,
|
||
templateName: templateName.value,
|
||
},
|
||
isViewMode.value ? "view" : "edit",
|
||
);
|
||
}
|
||
|
||
function handleBodyEntryClick() {
|
||
if (isViewMode.value) {
|
||
return;
|
||
}
|
||
openBodyConfigModal();
|
||
}
|
||
|
||
function handleBodyConfigSave(payload = {}) {
|
||
creationMode.value = payload.creationMode || "template";
|
||
templateKey.value = payload.templateKey || "";
|
||
templateName.value = payload.templateName || "";
|
||
detailState.value = copyContractFormData(payload.detailState);
|
||
syncCounterpartyState();
|
||
syncUploadLists();
|
||
|
||
if (modalMode.value === "edit" && detailState.value.id) {
|
||
const errorMessage = validateDraft();
|
||
if (errorMessage) {
|
||
message.success(
|
||
"合同正文配置已保存到当前页面,请继续完善主信息后再保存合同草稿。",
|
||
);
|
||
return;
|
||
}
|
||
|
||
syncCounterpartyState();
|
||
detailState.value.contractAmount =
|
||
detailState.value.contractAmount === null ||
|
||
detailState.value.contractAmount === undefined ||
|
||
detailState.value.contractAmount === ""
|
||
? null
|
||
: Number(detailState.value.contractAmount);
|
||
|
||
emit("save", {
|
||
mode: modalMode.value,
|
||
record: buildLaunchRecord(),
|
||
successMessage: "合同正文配置已保存",
|
||
});
|
||
return;
|
||
}
|
||
|
||
message.success(
|
||
"合同正文配置已保存到当前页面,请继续点击底部“保存”提交合同草稿。",
|
||
);
|
||
}
|
||
|
||
function addPerformancePlan() {
|
||
detailState.value.performancePlans.push(createPerformancePlan());
|
||
}
|
||
|
||
function removePerformancePlan(id) {
|
||
detailState.value.performancePlans =
|
||
detailState.value.performancePlans.filter((item) => item.id !== id);
|
||
}
|
||
|
||
/** 草稿保存校验(宽松):仅校验必填核心字段 */
|
||
function validateDraft() {
|
||
if (
|
||
!ourParties.value.length ||
|
||
ourParties.value.some((party) => !party.companyName?.trim())
|
||
)
|
||
return "请确认我方信息";
|
||
if (
|
||
currentSelfSubjectCode.value &&
|
||
!ourParties.value.some(
|
||
(party) =>
|
||
String(party.subjectCode || "").trim() === currentSelfSubjectCode.value,
|
||
)
|
||
) {
|
||
return "当前登录主体必须包含在我方主体中";
|
||
}
|
||
if (
|
||
isSingleAgreementChecked.value &&
|
||
currentSelfSubjectCode.value &&
|
||
ourParties.value.some(
|
||
(party) =>
|
||
String(party.subjectCode || "").trim() !== currentSelfSubjectCode.value,
|
||
)
|
||
) {
|
||
return "单方协议仅允许使用当前登录主体";
|
||
}
|
||
if (!isSingleAgreementChecked.value && !counterpartyParties.value.length)
|
||
return "请至少添加一个相对方";
|
||
if (
|
||
!isSingleAgreementChecked.value &&
|
||
counterpartyParties.value.some((party) => !party.companyName?.trim())
|
||
)
|
||
return "请选择相对方";
|
||
if (!detailState.value.contractName?.trim()) return "请填写合同名称";
|
||
if (!detailState.value.contractTypeCode) return "请选择合同类型";
|
||
if (
|
||
detailState.value.contractPeriodType === "0" &&
|
||
(!detailState.value.effectiveStart || !detailState.value.effectiveEnd)
|
||
)
|
||
return "请选择固定期限起止日期";
|
||
if (
|
||
detailState.value.contractPeriodType === "1" &&
|
||
!detailState.value.periodExplain?.trim()
|
||
)
|
||
return "请填写期限说明";
|
||
if (!detailState.value.contractBodyFileName?.trim())
|
||
return "请先配置合同正文";
|
||
if (
|
||
!isSingleAgreementChecked.value &&
|
||
requiresAuthAttachment.value &&
|
||
!(detailState.value.authAttachmentList || []).length
|
||
)
|
||
return "请上传授权委托书附件";
|
||
// 履行计划:草稿保存时即要求至少一条且字段完整
|
||
if (!(detailState.value.performancePlans || []).length)
|
||
return "请至少添加一条履行计划";
|
||
for (const plan of detailState.value.performancePlans || []) {
|
||
if (!plan.planMatter?.trim()) return "请填写履行计划的履行事项";
|
||
if (!plan.planWhere?.trim()) return "请填写履行计划的履约条件";
|
||
if (!plan.planTime) return "请选择履行计划的节点日期";
|
||
if (
|
||
plan.planDay === null ||
|
||
plan.planDay === undefined ||
|
||
plan.planDay === ""
|
||
)
|
||
return "请填写履行计划的提醒提前天数";
|
||
if (
|
||
!["0", "3"].includes(detailState.value.valuationMode) &&
|
||
(plan.planMoney === null ||
|
||
plan.planMoney === undefined ||
|
||
plan.planMoney === "")
|
||
)
|
||
return "请填写履行计划的款项金额";
|
||
}
|
||
return "";
|
||
}
|
||
|
||
/** 提交法务校验(严格):补全 pushContract 所有必填项 + 履行计划 */
|
||
function validateSubmit() {
|
||
if (
|
||
!ourParties.value.length ||
|
||
ourParties.value.some((party) => !party.companyName?.trim())
|
||
)
|
||
return "请确认我方信息";
|
||
if (
|
||
currentSelfSubjectCode.value &&
|
||
!ourParties.value.some(
|
||
(party) =>
|
||
String(party.subjectCode || "").trim() === currentSelfSubjectCode.value,
|
||
)
|
||
) {
|
||
return "当前登录主体必须包含在我方主体中";
|
||
}
|
||
if (
|
||
isSingleAgreementChecked.value &&
|
||
currentSelfSubjectCode.value &&
|
||
ourParties.value.some(
|
||
(party) =>
|
||
String(party.subjectCode || "").trim() !== currentSelfSubjectCode.value,
|
||
)
|
||
) {
|
||
return "单方协议仅允许使用当前登录主体";
|
||
}
|
||
if (!isSingleAgreementChecked.value && !counterpartyParties.value.length)
|
||
return "请至少添加一个相对方";
|
||
if (
|
||
!isSingleAgreementChecked.value &&
|
||
counterpartyParties.value.some((party) => !party.companyName?.trim())
|
||
)
|
||
return "请选择相对方";
|
||
if (!detailState.value.contractName?.trim()) return "请填写合同名称";
|
||
if (!detailState.value.contractTypeCode) return "请选择合同类型";
|
||
if (
|
||
detailState.value.contractPeriodType === "0" &&
|
||
(!detailState.value.effectiveStart || !detailState.value.effectiveEnd)
|
||
)
|
||
return "请选择固定期限起止日期";
|
||
if (
|
||
detailState.value.contractPeriodType === "1" &&
|
||
!detailState.value.periodExplain?.trim()
|
||
)
|
||
return "请填写期限说明";
|
||
if (!detailState.value.valuationMode) return "请选择计价方式";
|
||
// 固定总价时标的额必填
|
||
if (
|
||
detailState.value.valuationMode === "1" &&
|
||
(detailState.value.contractAmount === null ||
|
||
detailState.value.contractAmount === undefined ||
|
||
detailState.value.contractAmount === "")
|
||
)
|
||
return "请填写合同标的额";
|
||
// 预估总价时预估金额必填
|
||
if (
|
||
detailState.value.valuationMode === "2" &&
|
||
(detailState.value.estimatedAmount === null ||
|
||
detailState.value.estimatedAmount === undefined ||
|
||
detailState.value.estimatedAmount === "")
|
||
)
|
||
return "请填写预估金额";
|
||
// 无金额或无法预估总价时金额说明必填
|
||
if (
|
||
(detailState.value.valuationMode === "0" ||
|
||
detailState.value.valuationMode === "3") &&
|
||
!detailState.value.amountExplain?.trim()
|
||
)
|
||
return "请填写金额说明";
|
||
if (!detailState.value.currencyName) return "请选择币种";
|
||
if (!detailState.value.paymentMethod) return "请选择付款方式";
|
||
if (!detailState.value.contractSource) return "请选择合同来源";
|
||
if (!detailState.value.sealType) return "请选择签订方式";
|
||
if (!detailState.value.textSource) return "请选择文本来源";
|
||
// needAmendments 必填(文档要求)
|
||
if (
|
||
!detailState.value.needAmendments ||
|
||
detailState.value.needAmendments === ""
|
||
)
|
||
return "请选择是否需修订条款";
|
||
// 以下为 pushContract 必填项补充校验(接口文档要求)
|
||
if (detailState.value.isMajorContract === undefined || detailState.value.isMajorContract === null || detailState.value.isMajorContract === "")
|
||
return "请选择是否重大合同";
|
||
if (detailState.value.isPublicTender === undefined || detailState.value.isPublicTender === null || detailState.value.isPublicTender === "")
|
||
return "请选择是否公开招标";
|
||
if (detailState.value.isInvolveHongKongMacaoTaiwan === undefined || detailState.value.isInvolveHongKongMacaoTaiwan === null || detailState.value.isInvolveHongKongMacaoTaiwan === "")
|
||
return "请选择是否涉及港澳台";
|
||
if (!detailState.value.paymentDirection) return "请选择收支方向";
|
||
if (!detailState.value.contractTypeCode) return "请选择合同类型编码";
|
||
if (!detailState.value.executorAccount?.trim()) return "请填写合同执行人";
|
||
if (detailState.value.isPublicTender && !detailState.value.biddingNo?.trim())
|
||
return "请填写关联招标号";
|
||
if (!detailState.value.contractBodyFileName?.trim())
|
||
return "请先配置合同正文";
|
||
if (
|
||
!isSingleAgreementChecked.value &&
|
||
requiresAuthAttachment.value &&
|
||
!(detailState.value.authAttachmentList || []).length
|
||
)
|
||
return "请上传授权委托书附件";
|
||
if (!(detailState.value.performancePlans || []).length)
|
||
return "请至少添加一条履行计划";
|
||
for (const plan of detailState.value.performancePlans || []) {
|
||
if (!plan.planMatter?.trim()) return "请填写履行计划的履行事项";
|
||
if (!plan.planWhere?.trim()) return "请填写履行计划的履约条件";
|
||
if (!plan.planTime) return "请选择履行计划的节点日期";
|
||
if (
|
||
plan.planDay === null ||
|
||
plan.planDay === undefined ||
|
||
plan.planDay === ""
|
||
)
|
||
return "请填写履行计划的提醒提前天数";
|
||
if (
|
||
!["0", "3"].includes(detailState.value.valuationMode) &&
|
||
(plan.planMoney === null ||
|
||
plan.planMoney === undefined ||
|
||
plan.planMoney === "")
|
||
)
|
||
return "请填写履行计划的款项金额";
|
||
}
|
||
return "";
|
||
}
|
||
|
||
function handleSave() {
|
||
const errorMessage = validateDraft();
|
||
if (errorMessage) {
|
||
message.warning(errorMessage);
|
||
return;
|
||
}
|
||
|
||
syncCounterpartyState();
|
||
detailState.value.contractAmount =
|
||
detailState.value.contractAmount === null ||
|
||
detailState.value.contractAmount === undefined ||
|
||
detailState.value.contractAmount === ""
|
||
? null
|
||
: Number(detailState.value.contractAmount);
|
||
detailState.value.estimatedAmount =
|
||
detailState.value.estimatedAmount === null ||
|
||
detailState.value.estimatedAmount === undefined ||
|
||
detailState.value.estimatedAmount === ""
|
||
? null
|
||
: Number(detailState.value.estimatedAmount);
|
||
|
||
const record = buildLaunchRecord();
|
||
emit("save", {
|
||
mode: modalMode.value,
|
||
record,
|
||
});
|
||
visible.value = false;
|
||
message.success("合同方信息已保存");
|
||
}
|
||
|
||
function buildLaunchRecord() {
|
||
const partyList = [];
|
||
ourParties.value.forEach((party) => {
|
||
partyList.push({
|
||
roleCode: PARTY_ROLE_OUR,
|
||
roleName: "我方",
|
||
subjectCode: party.subjectCode || "",
|
||
partnerId: party.partnerId || party.customerSysid || null,
|
||
oppositeId: party.oppositeId,
|
||
companyName: party.companyName,
|
||
companyType: party.companyType,
|
||
oppositeCharacter: party.oppCharacter || "1",
|
||
contactName: party.contactName,
|
||
contactPhone: party.contactPhone,
|
||
contactEmail: party.contactEmail,
|
||
contactAddress: party.contactAddress,
|
||
registeredAddress: party.registeredAddress,
|
||
legalRepresentative: party.legalRepresentative,
|
||
creditCode: party.creditCode,
|
||
naturalPersonIdType: party.naturalPersonIdType,
|
||
naturalPersonIdNumber: party.naturalPersonIdNumber,
|
||
isGroupInternal: party.isGroupInternal ? "1" : "0",
|
||
isHongKongMacaoTaiwan: party.isHongKongMacaoTaiwan ? "1" : "0",
|
||
});
|
||
});
|
||
|
||
if (!isSingleAgreementChecked.value) {
|
||
counterpartyParties.value.forEach((party) => {
|
||
partyList.push({
|
||
roleCode: PARTY_ROLE_OPPOSITE,
|
||
roleName: "相对方",
|
||
partnerId: party.partnerId || party.customerSysid || null,
|
||
oppositeId: party.oppositeId,
|
||
companyName: party.companyName,
|
||
companyType: party.companyType,
|
||
oppositeCharacter: party.oppCharacter || "1",
|
||
contactName: party.contactName,
|
||
contactPhone: party.contactPhone,
|
||
contactEmail: party.contactEmail,
|
||
contactAddress: party.contactAddress,
|
||
registeredAddress: party.registeredAddress,
|
||
legalRepresentative: party.legalRepresentative,
|
||
creditCode: party.creditCode,
|
||
naturalPersonIdType: party.naturalPersonIdType,
|
||
naturalPersonIdNumber: party.naturalPersonIdNumber,
|
||
isGroupInternal: party.isGroupInternal ? "1" : "0",
|
||
isHongKongMacaoTaiwan: party.isHongKongMacaoTaiwan ? "1" : "0",
|
||
});
|
||
});
|
||
}
|
||
|
||
const fileList = [];
|
||
if (detailState.value.contractBodyFileName) {
|
||
fileList.push({
|
||
fileCategory: "body",
|
||
fileName: detailState.value.contractBodyFileName,
|
||
filePath: detailState.value.contractBodyFilePath || "",
|
||
fileType: detailState.value.contractBodyFileType || "docx",
|
||
fileSize: detailState.value.contractBodyFileSize || null,
|
||
});
|
||
}
|
||
(detailState.value.contractGistFileList || []).forEach((item) => {
|
||
fileList.push({
|
||
fileCategory: "gist",
|
||
fileName: item.name || item.fileName,
|
||
filePath: item.filePath || "",
|
||
fileType: item.fileType || "",
|
||
fileSize: item.size ?? item.fileSize ?? 0,
|
||
});
|
||
});
|
||
(detailState.value.authAttachmentList || []).forEach((item) => {
|
||
fileList.push({
|
||
fileCategory: "auth",
|
||
fileName: item.name || item.fileName,
|
||
filePath: item.filePath || "",
|
||
fileType: item.fileType || "",
|
||
fileSize: item.size ?? item.fileSize ?? 0,
|
||
});
|
||
});
|
||
(detailState.value.otherAttachmentList || []).forEach((item) => {
|
||
fileList.push({
|
||
fileCategory: "other",
|
||
fileName: item.name || item.fileName,
|
||
filePath: item.filePath || "",
|
||
fileType: item.fileType || "",
|
||
fileSize: item.size ?? item.fileSize ?? 0,
|
||
});
|
||
});
|
||
|
||
const extraAmountPayload = buildExtraAmountPayload(
|
||
detailState.value.extraAmounts,
|
||
);
|
||
|
||
return {
|
||
id: detailState.value.id || null,
|
||
contractSysId: detailState.value.contractSysId || null,
|
||
contractCode: detailState.value.contractCode,
|
||
contractName: detailState.value.contractName,
|
||
contractTypeCode: detailState.value.contractTypeCode,
|
||
contractSource: detailState.value.contractSource || "ECP",
|
||
creationMode: creationMode.value,
|
||
templateId: templateKey.value || null,
|
||
templateName: templateName.value || "",
|
||
contractAmount: detailState.value.contractAmount,
|
||
currencyName: detailState.value.currencyName,
|
||
contractPeriodType: detailState.value.contractPeriodType,
|
||
periodExplain: detailState.value.periodExplain,
|
||
effectiveStart: formatDateTimeValue(detailState.value.effectiveStart),
|
||
effectiveEnd: formatDateTimeValue(detailState.value.effectiveEnd, true),
|
||
paymentMethod: detailState.value.paymentMethod,
|
||
primaryContent: detailState.value.primaryContent,
|
||
amountExplain: detailState.value.amountExplain,
|
||
valuationMode: detailState.value.valuationMode,
|
||
needAmendments: detailState.value.needAmendments,
|
||
estimatedAmount:
|
||
detailState.value.valuationMode === "2"
|
||
? detailState.value.estimatedAmount
|
||
: null,
|
||
sealType: detailState.value.sealType,
|
||
textSource:
|
||
detailState.value.textSource ||
|
||
(creationMode.value === "manual" ? "draftBySelf" : "standardText"),
|
||
mandateType: detailState.value.mandateType,
|
||
cargoType: detailState.value.cargoType,
|
||
assistDepartmentName: detailState.value.assistDept,
|
||
executorAccount: detailState.value.executorAccount,
|
||
creatorPhone: detailState.value.executorAccount,
|
||
selfCode: detailState.value.selfCode || "",
|
||
mainContractCode: detailState.value.mainContractCode || "",
|
||
dateType: detailState.value.dateType || "0",
|
||
isAddit: detailState.value.mainContractCode ? 1 : 2,
|
||
isElectron: detailState.value.isElectron || "1",
|
||
isSingleAgreement: detailState.value.isSingleAgreement || "0",
|
||
paymentDirection: detailState.value.paymentDirection || "0",
|
||
isMajorContract: detailState.value.isMajorContract,
|
||
isOpenBidding: detailState.value.isPublicTender,
|
||
isYearBudget: detailState.value.isIncludedInCurrentYearBudget,
|
||
isShare: detailState.value.isShared,
|
||
contractNature: detailState.value.isForeignRelatedSubject,
|
||
isSgat: detailState.value.isInvolveHongKongMacaoTaiwan,
|
||
biddingNo: detailState.value.isPublicTender
|
||
? detailState.value.biddingNo || ""
|
||
: "",
|
||
contractGistRemark: detailState.value.contractGistRemark || "",
|
||
bodySummary:
|
||
detailState.value.bodySummary || detailState.value.primaryContent || "",
|
||
contractTextFileName: detailState.value.contractBodyFileName || "",
|
||
contractTextFilePath: detailState.value.contractBodyFilePath || "",
|
||
contractTextFileType: detailState.value.contractBodyFileType || "docx",
|
||
contractTextFileSize: detailState.value.contractBodyFileSize || null,
|
||
invoiceDate: formatDateTimeValue(detailState.value.invoiceDate),
|
||
receiveDate: formatDateTimeValue(detailState.value.receiveDate),
|
||
templateFieldValuesJson: JSON.stringify(
|
||
detailState.value.templateFieldValues || {},
|
||
),
|
||
remindDays: detailState.value.remindDays ?? 3,
|
||
signingSubjectCode:
|
||
signingSubjectCodeText.value ||
|
||
detailState.value.signingSubjectCode ||
|
||
"",
|
||
extraAmounts: { ...detailState.value.extraAmounts },
|
||
...extraAmountPayload,
|
||
partyList,
|
||
fileList,
|
||
performancePlans: (detailState.value.performancePlans || []).map(
|
||
(item) => ({
|
||
planMatter: item.planMatter,
|
||
planWhere: item.planWhere,
|
||
planTime: formatDateTimeValue(item.planTime),
|
||
planDay: item.planDay,
|
||
planMoney: item.planMoney,
|
||
planSubjectCode: item.planSubjectCode,
|
||
planSubjectName: item.planSubjectName,
|
||
executorUnitId: item.executorUnitId || null,
|
||
planDetail: item.planDetail,
|
||
planStatus: "0",
|
||
}),
|
||
),
|
||
};
|
||
}
|
||
|
||
function applySelfProfile(profile = {}) {
|
||
const firstOurParty =
|
||
detailState.value.partyState.ourPartyList?.[0] ||
|
||
createEmptyParty(PARTY_ROLE_OUR, 0);
|
||
currentSelfParty.value = {
|
||
...createEmptyParty(PARTY_ROLE_OUR, 0),
|
||
companyName: profile.orgName || "",
|
||
subjectCode: profile.id || "",
|
||
contactPhone: profile.contactPhone || "",
|
||
contactEmail: profile.contactEmail || "",
|
||
contactAddress: profile.contactAddress || "",
|
||
registeredAddress: profile.registeredAddress || "",
|
||
legalRepresentative: profile.legalRepresentative || "",
|
||
creditCode: profile.creditCode || "",
|
||
};
|
||
const shouldHydrateFirstParty =
|
||
(!firstOurParty.companyName && !firstOurParty.subjectCode) ||
|
||
firstOurParty.companyName === profile.orgName ||
|
||
(profile.id && firstOurParty.subjectCode === profile.id);
|
||
if (!shouldHydrateFirstParty) {
|
||
return;
|
||
}
|
||
detailState.value.partyState.ourPartyList = [
|
||
{
|
||
...firstOurParty,
|
||
companyName: profile.orgName || firstOurParty.companyName,
|
||
subjectCode: profile.id || firstOurParty.subjectCode,
|
||
contactPhone: profile.contactPhone || firstOurParty.contactPhone,
|
||
contactEmail: profile.contactEmail || firstOurParty.contactEmail,
|
||
contactAddress: profile.contactAddress || firstOurParty.contactAddress,
|
||
registeredAddress:
|
||
profile.registeredAddress || firstOurParty.registeredAddress,
|
||
legalRepresentative:
|
||
profile.legalRepresentative || firstOurParty.legalRepresentative,
|
||
creditCode: profile.creditCode || firstOurParty.creditCode,
|
||
},
|
||
...(detailState.value.partyState.ourPartyList || []).slice(1),
|
||
];
|
||
refreshPartyDisplayMeta();
|
||
forceSingleAgreementDefaults();
|
||
}
|
||
|
||
async function showModal(context = {}, mode = "create") {
|
||
modalMode.value = mode || "create";
|
||
const source =
|
||
context.detailState ||
|
||
(Object.keys(context || {}).length ? context : createContractFormData());
|
||
|
||
creationMode.value =
|
||
context.creationMode ||
|
||
source.creationMode ||
|
||
(source.contractBodyArrayBuffer || source.contractBodyFilePath
|
||
? "manual"
|
||
: "template");
|
||
templateKey.value = context.templateKey || source.templateId || "";
|
||
templateName.value = context.templateName || source.templateName || "";
|
||
detailState.value = copyContractFormData(source);
|
||
const selfProfile = await getLaunchSelfProfile();
|
||
applySelfProfile(selfProfile || {});
|
||
forceSingleAgreementDefaults();
|
||
syncCounterpartyState();
|
||
syncUploadLists();
|
||
loadOurPartyOptions();
|
||
loadCounterpartyOptions();
|
||
visible.value = true;
|
||
}
|
||
|
||
function loadPartnerOptions() {
|
||
loadCounterpartyOptions();
|
||
}
|
||
|
||
function flattenOrgTree(nodes = [], result = []) {
|
||
(nodes || []).forEach((node) => {
|
||
result.push({
|
||
orgId: node.id,
|
||
orgName: node.orgName,
|
||
contactPhone: node.contactPhone || "",
|
||
contactEmail: node.contactEmail || "",
|
||
contactAddress: node.contactAddress || "",
|
||
registeredAddress: node.registeredAddress || "",
|
||
legalRepresentative: node.legalRepresentative || "",
|
||
creditCode: node.creditCode || "",
|
||
});
|
||
if (Array.isArray(node.children) && node.children.length) {
|
||
flattenOrgTree(node.children, result);
|
||
}
|
||
});
|
||
return result;
|
||
}
|
||
|
||
function loadOurPartyOptions() {
|
||
return getOrgTree().then((tree) => {
|
||
const flatList = flattenOrgTree(tree);
|
||
ourPartyOrgOptions.value = flatList.map((item) => ({
|
||
label: `${item.orgName}${String(item.orgId || "").trim() === currentSelfSubjectCode.value ? "(当前主体)" : ""}`,
|
||
value: item.orgName,
|
||
raw: item,
|
||
}));
|
||
});
|
||
}
|
||
|
||
function loadCounterpartyOptions() {
|
||
queryPartnerOptions().then((list) => {
|
||
const options = (list || []).map((item) => ({
|
||
label: `${item.customerNameC || "未命名相对方"}${item.oppositeId ? `(${item.oppositeId})` : ""}`,
|
||
value: item.customerNameC,
|
||
raw: item,
|
||
}));
|
||
counterpartyOptions.value = options;
|
||
});
|
||
}
|
||
|
||
defineExpose({
|
||
showModal,
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* 详情/查看模式下禁用表单文字颜色加深 */
|
||
.editor-form:deep(.ant-select-disabled .ant-select-selection-item) {
|
||
color: rgba(0, 0, 0, 0.85) !important;
|
||
}
|
||
.editor-form:deep(.ant-select-disabled .ant-select-selection-placeholder) {
|
||
color: rgba(0, 0, 0, 0.85) !important;
|
||
}
|
||
.editor-form:deep(.ant-input[disabled]) {
|
||
color: rgba(0, 0, 0, 0.85) !important;
|
||
-webkit-text-fill-color: rgba(0, 0, 0, 0.85) !important;
|
||
}
|
||
.editor-form:deep(.ant-input-number-disabled .ant-input-number-input) {
|
||
color: rgba(0, 0, 0, 0.85) !important;
|
||
-webkit-text-fill-color: rgba(0, 0, 0, 0.85) !important;
|
||
}
|
||
.editor-form:deep(.ant-picker.ant-picker-disabled .ant-picker-input input) {
|
||
color: rgba(0, 0, 0, 0.85) !important;
|
||
-webkit-text-fill-color: rgba(0, 0, 0, 0.85) !important;
|
||
}
|
||
.editor-form:deep(.ant-radio-wrapper.ant-radio-wrapper-disabled .ant-radio + span) {
|
||
color: rgba(0, 0, 0, 0.85) !important;
|
||
}
|
||
.editor-form:deep(.ant-checkbox-wrapper.ant-checkbox-wrapper-disabled .ant-checkbox + span) {
|
||
color: rgba(0, 0, 0, 0.85) !important;
|
||
}
|
||
.editor-form:deep(.ant-input-disabled) {
|
||
color: rgba(0, 0, 0, 0.85) !important;
|
||
-webkit-text-fill-color: rgba(0, 0, 0, 0.85) !important;
|
||
}
|
||
|
||
.contract-main-editor {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
background: #f6f8fb;
|
||
}
|
||
|
||
.editor-header {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
gap: 16px;
|
||
padding: 18px 24px 14px;
|
||
background: #fff;
|
||
border-bottom: 1px solid #e5e7eb;
|
||
}
|
||
|
||
.editor-header h3 {
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
color: #0f172a;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.editor-body {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 18px 20px;
|
||
}
|
||
|
||
.editor-form {
|
||
height: 100%;
|
||
}
|
||
|
||
.main-grid {
|
||
display: grid;
|
||
grid-template-columns: 470px minmax(0, 1fr);
|
||
gap: 18px;
|
||
}
|
||
|
||
.section-card {
|
||
background: #fff;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 8px;
|
||
padding: 16px 18px;
|
||
}
|
||
|
||
.section-title-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 16px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.counterparty-toolbar {
|
||
margin-top: 18px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.section-title,
|
||
.basic-title {
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
color: #111827;
|
||
}
|
||
|
||
.basic-header {
|
||
border-bottom: 1px solid #eef2f7;
|
||
padding-bottom: 10px;
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.basic-title {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.dot {
|
||
width: 10px;
|
||
height: 10px;
|
||
background: #2563eb;
|
||
border-radius: 2px;
|
||
}
|
||
|
||
.party-block + .party-block {
|
||
margin-top: 14px;
|
||
}
|
||
|
||
.party-block-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
}
|
||
|
||
.field-label {
|
||
font-size: 13px;
|
||
color: #334155;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.required::before {
|
||
content: "*";
|
||
color: #ef4444;
|
||
margin-right: 4px;
|
||
}
|
||
|
||
.search-input-row {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) 34px;
|
||
gap: 6px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.icon-btn,
|
||
.mini-search-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #2563eb;
|
||
}
|
||
|
||
.text-action-btn {
|
||
padding-inline: 0;
|
||
}
|
||
|
||
.party-info-box {
|
||
position: relative;
|
||
background: #fafbff;
|
||
border: 1px solid #edf2f7;
|
||
border-radius: 6px;
|
||
padding: 10px 38px 10px 12px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.info-line {
|
||
display: flex;
|
||
gap: 10px;
|
||
font-size: 12px;
|
||
line-height: 1.8;
|
||
}
|
||
|
||
.info-line span {
|
||
width: 62px;
|
||
flex-shrink: 0;
|
||
color: #475569;
|
||
}
|
||
|
||
.info-line strong {
|
||
color: #0f172a;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.mini-search-btn {
|
||
position: absolute;
|
||
top: 8px;
|
||
right: 8px;
|
||
}
|
||
|
||
.invoice-btn {
|
||
width: 100%;
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.party-empty-state {
|
||
padding: 10px 12px;
|
||
border: 1px dashed #dbe3ee;
|
||
border-radius: 8px;
|
||
background: #f8fbff;
|
||
color: #64748b;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.field-grid {
|
||
display: grid;
|
||
gap: 10px 14px;
|
||
}
|
||
|
||
.field-grid-2 {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
|
||
.field-grid-3 {
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
}
|
||
|
||
.field-span-2 {
|
||
grid-column: span 2;
|
||
}
|
||
|
||
.field-span-3 {
|
||
grid-column: span 3;
|
||
}
|
||
|
||
.tight-item {
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.period-block {
|
||
margin-top: 10px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.period-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.period-explain-input {
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.summary-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 10px 18px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.summary-line {
|
||
display: flex;
|
||
gap: 10px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.summary-line span {
|
||
width: 62px;
|
||
flex-shrink: 0;
|
||
color: #475569;
|
||
}
|
||
|
||
.summary-line strong {
|
||
color: #0f172a;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.upload-block {
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.upload-label {
|
||
font-size: 13px;
|
||
color: #334155;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.readonly-upload-box {
|
||
min-height: 44px;
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: center;
|
||
flex-direction: column;
|
||
padding: 10px 12px;
|
||
border: 1px dashed #dbe3ee;
|
||
border-radius: 8px;
|
||
background: #f8fbff;
|
||
color: #475569;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.contract-body-entry {
|
||
gap: 4px;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.contract-body-entry.is-clickable {
|
||
cursor: pointer;
|
||
}
|
||
|
||
.contract-body-entry.is-clickable:hover {
|
||
border-color: #91caff;
|
||
background: #eef6ff;
|
||
}
|
||
|
||
.contract-body-entry-name {
|
||
color: #0f172a;
|
||
font-weight: 600;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.contract-body-entry-hint {
|
||
color: #64748b;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.other-upload-text {
|
||
font-size: 13px;
|
||
color: #475569;
|
||
}
|
||
|
||
.contract-attr-title {
|
||
margin-top: 14px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.boolean-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 10px 18px;
|
||
margin-top: 6px;
|
||
}
|
||
|
||
.boolean-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
min-height: 32px;
|
||
font-size: 13px;
|
||
color: #334155;
|
||
}
|
||
|
||
.extra-amount-section {
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.extra-label {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #334155;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.extra-check-row,
|
||
.extra-input-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
}
|
||
|
||
.extra-check-item {
|
||
width: 24%;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.plan-section {
|
||
margin-top: 18px;
|
||
}
|
||
|
||
.plan-toolbar {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.plan-table :deep(.ant-table-cell) {
|
||
vertical-align: top;
|
||
}
|
||
|
||
.empty-plan-text {
|
||
padding: 12px 0;
|
||
color: #94a3b8;
|
||
}
|
||
|
||
.detail-collapse {
|
||
margin-top: 16px;
|
||
background: transparent;
|
||
}
|
||
|
||
.editor-footer {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
padding: 14px 20px 18px;
|
||
background: #fff;
|
||
border-top: 1px solid #e5e7eb;
|
||
}
|
||
|
||
@media (max-width: 1280px) {
|
||
.main-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 900px) {
|
||
.field-grid-2,
|
||
.field-grid-3,
|
||
.summary-grid,
|
||
.boolean-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.field-span-2,
|
||
.field-span-3 {
|
||
grid-column: span 1;
|
||
}
|
||
|
||
.extra-check-row,
|
||
.extra-input-row {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.extra-check-item,
|
||
.extra-input-row :deep(.ant-input-number) {
|
||
width: 100% !important;
|
||
}
|
||
}
|
||
</style>
|