Files
stamp-ant/src/views/contract/launch/components/ContractPartyPlaceholderModal.vue
T

2991 lines
97 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>