1762 lines
54 KiB
Vue
1762 lines
54 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="party-block">
|
||
<div class="field-label required">我方</div>
|
||
<a-input
|
||
:value="detailState.partyState.partyA.companyName"
|
||
readonly
|
||
disabled
|
||
/>
|
||
<div class="party-info-box">
|
||
<div class="info-line">
|
||
<span>我方</span
|
||
><strong>{{
|
||
detailState.partyState.partyA.companyName || "/"
|
||
}}</strong>
|
||
</div>
|
||
<div class="info-line">
|
||
<span>电话</span
|
||
><strong>{{
|
||
detailState.partyState.partyA.contactPhone || "/"
|
||
}}</strong>
|
||
</div>
|
||
<div class="info-line">
|
||
<span>公司地址</span
|
||
><strong>{{
|
||
detailState.partyState.partyA.registeredAddress || "/"
|
||
}}</strong>
|
||
</div>
|
||
<div class="info-line">
|
||
<span>银行/账号</span
|
||
><strong>{{
|
||
detailState.partyState.partyA.bankAccount || "/"
|
||
}}</strong>
|
||
</div>
|
||
<a-button
|
||
class="mini-search-btn"
|
||
type="text"
|
||
@click="showSearchTip('我方详情')"
|
||
>
|
||
<SearchOutlined />
|
||
</a-button>
|
||
</div>
|
||
<a-button
|
||
class="invoice-btn"
|
||
@click="showInvoiceTitles(detailState.partyState.partyA, '我方')"
|
||
>
|
||
<PlusOutlined />
|
||
备选开票抬头选择
|
||
</a-button>
|
||
</div>
|
||
|
||
<div class="section-title-row counterparty-toolbar">
|
||
<div class="section-title">相对方</div>
|
||
<a-button
|
||
v-if="!isViewMode"
|
||
type="link"
|
||
class="text-action-btn"
|
||
@click="handleAddCounterparty"
|
||
>
|
||
添加
|
||
</a-button>
|
||
</div>
|
||
|
||
<div
|
||
v-for="(party, index) in counterpartyParties"
|
||
:key="party.roleKey"
|
||
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.roleKey)"
|
||
>
|
||
删除
|
||
</a-button>
|
||
</div>
|
||
<div class="search-input-row">
|
||
<a-select
|
||
v-model:value="party.companyName"
|
||
:options="contractCompanyOptions"
|
||
show-search
|
||
:placeholder="`请选择${party.displayName}`"
|
||
@change="handleCompanySelect(party.roleKey, $event)"
|
||
/>
|
||
<a-button
|
||
class="icon-btn"
|
||
@click="showSearchTip(`${party.displayName}`)"
|
||
>
|
||
<SearchOutlined />
|
||
</a-button>
|
||
</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 class="info-line">
|
||
<span>银行/账号</span
|
||
><strong>{{ party.bankAccount || "/" }}</strong>
|
||
</div>
|
||
<a-button
|
||
class="mini-search-btn"
|
||
type="text"
|
||
@click="showSearchTip(`${party.displayName}详情`)"
|
||
>
|
||
<SearchOutlined />
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="!counterpartyParties.length" 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="合同号" required>
|
||
<a-input
|
||
v-model:value="detailState.contractCode"
|
||
placeholder="请输入"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item
|
||
class="tight-item field-span-2"
|
||
label="合同类型"
|
||
required
|
||
>
|
||
<a-select
|
||
v-model:value="detailState.contractType"
|
||
: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="委托类型"
|
||
required
|
||
>
|
||
<a-select
|
||
v-model:value="detailState.mandateType"
|
||
:options="mandateTypeOptions"
|
||
placeholder="委托类型"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item
|
||
class="tight-item field-span-2"
|
||
label="货种"
|
||
required
|
||
>
|
||
<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>
|
||
</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>{{
|
||
detailState.partyState.partyA.companyName || "未填写"
|
||
}}</strong>
|
||
</div>
|
||
<div class="summary-line">
|
||
<span>合同编号</span
|
||
><strong>{{ detailState.contractCode || "未填写" }}</strong>
|
||
</div>
|
||
<div class="summary-line">
|
||
<span>相对方</span
|
||
><strong>{{ 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="openBodyConfigModal"
|
||
>
|
||
<div class="contract-body-entry-name">
|
||
{{ contractBodyDisplayText }}
|
||
</div>
|
||
<div class="contract-body-entry-hint">
|
||
{{ contractBodyHint }}
|
||
</div>
|
||
</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>
|
||
|
||
<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="金额说明" required>
|
||
<a-textarea
|
||
v-model:value="detailState.amountExplain"
|
||
:rows="2"
|
||
placeholder="选填"
|
||
/>
|
||
</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 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="协助部门">
|
||
<a-input
|
||
v-model:value="detailState.assistDept"
|
||
placeholder="协助部门"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item class="tight-item" label="合同类型" required>
|
||
<a-select
|
||
v-model:value="detailState.contractTypeCode"
|
||
:options="contractTypeOptions"
|
||
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>
|
||
</div>
|
||
|
||
<div class="boolean-grid">
|
||
<div class="boolean-row">
|
||
<span>是否涉及港澳台</span>
|
||
<a-radio-group
|
||
v-model:value="detailState.isInvolveHongKongMacaoTaiwan"
|
||
>
|
||
<a-radio :value="true">是</a-radio>
|
||
<a-radio :value="false">否</a-radio>
|
||
</a-radio-group>
|
||
</div>
|
||
<div class="boolean-row">
|
||
<span>是否公开招标</span>
|
||
<a-radio-group v-model:value="detailState.isPublicTender">
|
||
<a-radio :value="true">是</a-radio>
|
||
<a-radio :value="false">否</a-radio>
|
||
</a-radio-group>
|
||
</div>
|
||
<div class="boolean-row">
|
||
<span>签约主体是否涉外</span>
|
||
<a-radio-group
|
||
v-model:value="detailState.isForeignRelatedSubject"
|
||
>
|
||
<a-radio :value="true">是</a-radio>
|
||
<a-radio :value="false">否</a-radio>
|
||
</a-radio-group>
|
||
</div>
|
||
<div class="boolean-row">
|
||
<span>是否列入当年预算</span>
|
||
<a-radio-group
|
||
v-model:value="detailState.isIncludedInCurrentYearBudget"
|
||
>
|
||
<a-radio :value="true">是</a-radio>
|
||
<a-radio :value="false">否</a-radio>
|
||
</a-radio-group>
|
||
</div>
|
||
<div class="boolean-row">
|
||
<span>是否共享</span>
|
||
<a-radio-group v-model:value="detailState.isShared">
|
||
<a-radio :value="true">是</a-radio>
|
||
<a-radio :value="false">否</a-radio>
|
||
</a-radio-group>
|
||
</div>
|
||
<div class="boolean-row">
|
||
<span>是否重大合同</span>
|
||
<a-radio-group v-model:value="detailState.isMajorContract">
|
||
<a-radio :value="true">是</a-radio>
|
||
<a-radio :value="false">否</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
|
||
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="!detailState.extraAmounts[`${item.key}Enabled`]"
|
||
:min="0"
|
||
:controls="false"
|
||
:precision="2"
|
||
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
|
||
type="dashed"
|
||
size="small"
|
||
@click="addPerformancePlan"
|
||
>
|
||
<PlusOutlined />
|
||
新增履行计划
|
||
</a-button>
|
||
</div>
|
||
<a-table
|
||
:data-source="detailState.performancePlans"
|
||
:pagination="false"
|
||
size="small"
|
||
:scroll="{ x: 820 }"
|
||
row-key="id"
|
||
class="plan-table"
|
||
>
|
||
<a-table-column title="履行事项" key="planMatter">
|
||
<template #default="{ record }">
|
||
<a-input v-model:value="record.planMatter" />
|
||
</template>
|
||
</a-table-column>
|
||
<a-table-column title="履约条件" key="planWhere">
|
||
<template #default="{ record }">
|
||
<a-input v-model:value="record.planWhere" />
|
||
</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%"
|
||
/>
|
||
</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%"
|
||
/>
|
||
</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%"
|
||
/>
|
||
</template>
|
||
</a-table-column>
|
||
<a-table-column title="备注" key="planDetail">
|
||
<template #default="{ record }">
|
||
<a-input v-model:value="record.planDetail" />
|
||
</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-collapse class="detail-collapse">
|
||
<a-collapse-panel
|
||
v-for="party in displayedParties"
|
||
:key="party.roleKey"
|
||
:header="`${party.displayName}详细字段维护`"
|
||
>
|
||
<div class="field-grid field-grid-3">
|
||
<a-form-item class="tight-item" label="公司类型">
|
||
<a-select
|
||
v-model:value="party.companyType"
|
||
:options="companyTypeOptions"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item class="tight-item" label="公司性质">
|
||
<a-select
|
||
v-model:value="party.companyNature"
|
||
:options="companyNatureOptions"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item class="tight-item" label="组织性质">
|
||
<a-select
|
||
v-model:value="party.organizationNature"
|
||
:options="organizationNatureOptions"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item class="tight-item" label="组织性质企业二级">
|
||
<a-select
|
||
v-model:value="party.enterpriseNatureLevel2"
|
||
:options="enterpriseNatureLevel2Options"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item class="tight-item" label="统一社会信用代码">
|
||
<a-input v-model:value="party.creditCode" />
|
||
</a-form-item>
|
||
<a-form-item class="tight-item" label="TIN号">
|
||
<a-input v-model:value="party.tinNo" />
|
||
</a-form-item>
|
||
<a-form-item class="tight-item" label="国家/地区">
|
||
<a-select
|
||
v-model:value="party.countryCode"
|
||
:options="countryOptions"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item class="tight-item" label="法定代表人">
|
||
<a-input v-model:value="party.legalRepresentative" />
|
||
</a-form-item>
|
||
<a-form-item class="tight-item field-span-3" label="注册地址">
|
||
<a-input v-model:value="party.registeredAddress" />
|
||
</a-form-item>
|
||
<a-form-item class="tight-item" label="注册资本">
|
||
<a-input v-model:value="party.registeredCapital" />
|
||
</a-form-item>
|
||
<a-form-item class="tight-item" label="联系电话">
|
||
<a-input v-model:value="party.contactPhone" />
|
||
</a-form-item>
|
||
<a-form-item class="tight-item" label="电子邮箱">
|
||
<a-input v-model:value="party.contactEmail" />
|
||
</a-form-item>
|
||
<a-form-item class="tight-item field-span-3" label="通讯地址">
|
||
<a-input v-model:value="party.contactAddress" />
|
||
</a-form-item>
|
||
<a-form-item class="tight-item" label="开户行">
|
||
<a-input v-model:value="party.bankName" />
|
||
</a-form-item>
|
||
<a-form-item class="tight-item field-span-2" label="银行账号">
|
||
<a-input v-model:value="party.bankAccount" />
|
||
</a-form-item>
|
||
<a-form-item class="tight-item field-span-3" label="备选开票抬头">
|
||
<a-select
|
||
v-model:value="party.invoiceTitleOptions"
|
||
mode="tags"
|
||
:token-separators="[',', ',']"
|
||
/>
|
||
</a-form-item>
|
||
</div>
|
||
</a-collapse-panel>
|
||
</a-collapse>
|
||
</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, SearchOutlined } from "@ant-design/icons-vue";
|
||
import ContractTemplateFillDemoModal from "./ContractTemplateFillDemoModal.vue";
|
||
|
||
// ========================================================================
|
||
// 以下为表单模型 + 字典 + 工具函数,直接内联在此文件中。
|
||
// 接入后端时,保留这些函数,替换入参/出参为真实接口即可。
|
||
// ========================================================================
|
||
|
||
// ========== 合同方相关字典 ==========
|
||
|
||
const partyCharacterOptions = [
|
||
{ label: "企业", value: "QY" },
|
||
{ label: "自然人", value: "ZZR" },
|
||
];
|
||
|
||
const companyTypeOptions = [
|
||
{ label: "客户", value: "客户" },
|
||
{ label: "供应商", value: "供应商" },
|
||
{ label: "合作伙伴", value: "合作伙伴" },
|
||
{ label: "客户及供应商", value: "客户及供应商" },
|
||
{ label: "其他", value: "其他" },
|
||
];
|
||
|
||
const companyNatureOptions = [
|
||
{ label: "境内组织", value: "境内组织" },
|
||
{ label: "境内个人", value: "境内个人" },
|
||
{ label: "境外组织", value: "境外组织" },
|
||
{ label: "境外个人", value: "境外个人" },
|
||
];
|
||
|
||
const organizationNatureOptions = [
|
||
{ label: "政府", value: "政府" },
|
||
{ label: "企业", value: "企业" },
|
||
{ label: "其他组织", value: "其他组织" },
|
||
];
|
||
|
||
const enterpriseNatureLevel2Options = [
|
||
{ label: "国有企业", value: "国有企业" },
|
||
{ label: "民营企业", value: "民营企业" },
|
||
{ label: "外资企业", value: "外资企业" },
|
||
{ label: "合资企业", value: "合资企业" },
|
||
{ label: "国有全资和控股企业", value: "国有全资和控股企业" },
|
||
];
|
||
|
||
const countryOptions = [
|
||
{ label: "中国", value: "GJ156" },
|
||
{ label: "中国香港", value: "GJ344" },
|
||
{ label: "中国澳门", value: "GJ446" },
|
||
{ label: "中国台湾", value: "GJ158" },
|
||
{ label: "新加坡", value: "GJ702" },
|
||
{ label: "美国", value: "GJ840" },
|
||
{ label: "英国", value: "GJ826" },
|
||
{ label: "日本", value: "GJ392" },
|
||
];
|
||
|
||
const contractCompanyOptions = [
|
||
{ label: "扬州宏富特种材料有限公司", value: "扬州宏富特种材料有限公司" },
|
||
{ label: "宁波港船务货运代理有限公司", value: "宁波港船务货运代理有限公司" },
|
||
{ label: "宁波浙港供应链科技有限公司", value: "宁波浙港供应链科技有限公司" },
|
||
{ label: "上海远洋能源有限公司", value: "上海远洋能源有限公司" },
|
||
{ label: "宁波保税区港航设备有限公司", value: "宁波保税区港航设备有限公司" },
|
||
{ label: "浙江临港数智科技有限公司", value: "浙江临港数智科技有限公司" },
|
||
{ label: "杭州智云实施服务有限公司", value: "杭州智云实施服务有限公司" },
|
||
{ label: "浙港物流平台有限公司", value: "浙港物流平台有限公司" },
|
||
];
|
||
|
||
// ========== 合同相关字典 ==========
|
||
|
||
const contractTypeOptions = [
|
||
{ label: "采购合同", value: "采购合同" },
|
||
{ label: "销售合同", value: "销售合同" },
|
||
{ label: "服务合同", value: "服务合同" },
|
||
{ label: "框架合同", value: "框架合同" },
|
||
];
|
||
|
||
const valuationModeOptions = [
|
||
{ label: "无金额", value: "0" },
|
||
{ label: "固定总价", value: "1" },
|
||
{ label: "预估总价", value: "2" },
|
||
{ label: "无法预估总价", value: "3" },
|
||
{ label: "固定单价", value: "4" },
|
||
];
|
||
|
||
const currencyOptions = [
|
||
{ label: "人民币", value: "RMB" },
|
||
{ label: "美元", value: "USD" },
|
||
{ label: "港币", value: "HKD" },
|
||
{ label: "欧元", value: "EUR" },
|
||
{ label: "印尼盾", value: "IDR" },
|
||
];
|
||
|
||
const sealTypeOptions = [
|
||
{ label: "公章", value: "GZ" },
|
||
{ label: "合同专用章", value: "HTZYZ" },
|
||
{ label: "公章+法定代表人私章", value: "GZFDDBRSZ" },
|
||
{ label: "公章+法定代表人签字", value: "GZFDDBRQZ" },
|
||
{ label: "公章+被授权人签字", value: "GZBSQRQZ" },
|
||
{ label: "合同专用章+被授权人签字", value: "HTZYZBSQRQZ" },
|
||
{ label: "合同专用章+法定代表人签字", value: "HTZYZFDDBRQZ" },
|
||
{ label: "其他", value: "QT" },
|
||
];
|
||
|
||
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 extraAmountFieldOptions = [
|
||
{ key: "deposit", label: "定金" },
|
||
{ key: "pledge", label: "押金" },
|
||
{ key: "advance", label: "预付款" },
|
||
{ key: "guarantee", label: "保证金" },
|
||
];
|
||
|
||
const SELF_COMPANY_NAME = "浙港物流平台有限公司";
|
||
const MAX_COUNTERPARTY_COUNT = 2;
|
||
|
||
function getOptionLabel(options, value) {
|
||
return options.find((item) => item.value === value)?.label || value || "";
|
||
}
|
||
|
||
// ========== 工具函数 ==========
|
||
|
||
/** 深拷贝合同方状态 */
|
||
function copyPartyState(input) {
|
||
const source = input || {};
|
||
const sourcePartyA = source.partyA || source.parties?.A || {};
|
||
const sourcePartyB = source.partyB || source.parties?.B || {};
|
||
const sourcePartyC = source.partyC || source.parties?.C || {};
|
||
return {
|
||
isThreePartyContract: Boolean(source.isThreePartyContract),
|
||
partyA: {
|
||
roleKey: "A",
|
||
roleName: "我方",
|
||
displayName: "我方",
|
||
...sourcePartyA,
|
||
companyName: sourcePartyA.companyName || SELF_COMPANY_NAME,
|
||
},
|
||
partyB: {
|
||
roleKey: "B",
|
||
roleName: "相对方",
|
||
displayName: "相对方1",
|
||
companyName: "",
|
||
...sourcePartyB,
|
||
},
|
||
partyC: {
|
||
roleKey: "C",
|
||
roleName: "相对方",
|
||
displayName: "相对方2",
|
||
companyName: "",
|
||
...sourcePartyC,
|
||
},
|
||
};
|
||
}
|
||
|
||
function createEmptyCounterparty(roleKey) {
|
||
const blankState = copyPartyState({});
|
||
return roleKey === "C" ? blankState.partyC : blankState.partyB;
|
||
}
|
||
|
||
function isPartyFilled(party = {}) {
|
||
const ignoreKeys = new Set(["roleKey", "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 buildCounterpartyRoleKeys(partyState) {
|
||
const state = copyPartyState(partyState);
|
||
const keys = [];
|
||
if (isPartyFilled(state.partyB)) keys.push("B");
|
||
if (isPartyFilled(state.partyC)) keys.push("C");
|
||
return keys;
|
||
}
|
||
|
||
function normalizePartyState(partyState) {
|
||
const state = copyPartyState(partyState);
|
||
if (!isPartyFilled(state.partyB) && isPartyFilled(state.partyC)) {
|
||
state.partyB = {
|
||
...createEmptyCounterparty("B"),
|
||
...state.partyC,
|
||
roleKey: "B",
|
||
roleName: "相对方",
|
||
displayName: "相对方1",
|
||
};
|
||
state.partyC = createEmptyCounterparty("C");
|
||
}
|
||
state.partyA.companyName = state.partyA.companyName || SELF_COMPANY_NAME;
|
||
state.isThreePartyContract =
|
||
isPartyFilled(state.partyB) && isPartyFilled(state.partyC);
|
||
return state;
|
||
}
|
||
|
||
function getPartyByRoleKey(partyState, roleKey) {
|
||
if (roleKey === "A") return partyState.partyA;
|
||
if (roleKey === "C") return partyState.partyC;
|
||
return partyState.partyB;
|
||
}
|
||
|
||
/** 创建一条空的履行计划 */
|
||
function createPerformancePlan() {
|
||
return {
|
||
id: `plan_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`,
|
||
planMatter: "",
|
||
planWhere: "",
|
||
planTime: "",
|
||
planDay: 0,
|
||
planMoney: "",
|
||
planDetail: "",
|
||
};
|
||
}
|
||
|
||
/** 创建空白的合同表单数据 */
|
||
function createContractFormData() {
|
||
return {
|
||
contractName: "",
|
||
contractCode: "",
|
||
contractType: "",
|
||
contractTypeCode: "",
|
||
contractAmount: null,
|
||
effectiveStart: "",
|
||
effectiveEnd: "",
|
||
contractPeriodType: "0",
|
||
periodExplain: "",
|
||
paymentMethod: "",
|
||
valuationMode: "",
|
||
currencyName: "",
|
||
sealType: "",
|
||
mandateType: "",
|
||
cargoType: "",
|
||
primaryContent: "",
|
||
amountExplain: "",
|
||
assistDept: "",
|
||
executorAccount: "",
|
||
|
||
contractBodyFileName: "",
|
||
contractBodyArrayBuffer: null,
|
||
otherAttachmentList: [],
|
||
partyState: {
|
||
isThreePartyContract: false,
|
||
partyA: { companyName: SELF_COMPANY_NAME },
|
||
partyB: { companyName: "" },
|
||
partyC: { companyName: "" },
|
||
},
|
||
isInvolveHongKongMacaoTaiwan: false,
|
||
isPublicTender: false,
|
||
isForeignRelatedSubject: false,
|
||
isIncludedInCurrentYearBudget: false,
|
||
isShared: false,
|
||
isMajorContract: false,
|
||
extraAmounts: {
|
||
depositEnabled: false,
|
||
deposit: 0,
|
||
pledgeEnabled: false,
|
||
pledge: 0,
|
||
advanceEnabled: false,
|
||
advance: 0,
|
||
guaranteeEnabled: false,
|
||
guarantee: 0,
|
||
},
|
||
performancePlans: [],
|
||
signDate: "",
|
||
};
|
||
}
|
||
|
||
/** 深拷贝合同表单数据 */
|
||
function copyContractFormData(data = {}) {
|
||
const form = createContractFormData();
|
||
const source = data || {};
|
||
Object.keys(form).forEach((key) => {
|
||
if (key === "extraAmounts") {
|
||
form.extraAmounts = {
|
||
...form.extraAmounts,
|
||
...(source.extraAmounts || {}),
|
||
};
|
||
} else if (key === "performancePlans") {
|
||
form.performancePlans = (source.performancePlans || []).map((item) => ({
|
||
...createPerformancePlan(),
|
||
...item,
|
||
}));
|
||
} else if (key === "partyState") {
|
||
form.partyState = normalizePartyState(source.partyState);
|
||
} else if (key === "otherAttachmentList") {
|
||
form.otherAttachmentList = [
|
||
...(source.otherAttachmentList || source.attachmentList || []),
|
||
];
|
||
} else if (key === "contractBodyArrayBuffer") {
|
||
form.contractBodyArrayBuffer = source.contractBodyArrayBuffer
|
||
? source.contractBodyArrayBuffer instanceof ArrayBuffer
|
||
? source.contractBodyArrayBuffer.slice(0)
|
||
: null
|
||
: null;
|
||
} else {
|
||
form[key] = source[key] ?? form[key];
|
||
}
|
||
});
|
||
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 bodyConfigModalRef = ref(null);
|
||
const counterpartyRoleKeys = ref([]);
|
||
|
||
const companyProfileMap = {
|
||
扬州宏富特种材料有限公司: {
|
||
contactPhone: "13800000001",
|
||
registeredAddress: "江苏省扬州市邗江区江阳中路88号",
|
||
bankAccount: "622202100000000001",
|
||
bankName: "中国工商银行扬州分行",
|
||
contactName: "王经理",
|
||
legalRepresentative: "张三",
|
||
creditCode: "91321000MA1X111111",
|
||
},
|
||
宁波港船务货运代理有限公司: {
|
||
contactPhone: "15757460062",
|
||
registeredAddress: "宁波市北仑区明州西路188号",
|
||
bankAccount: "39011600192100006088",
|
||
bankName: "中国工商银行股份有限公司宁波市支行",
|
||
contactName: "周经理",
|
||
legalRepresentative: "李四",
|
||
creditCode: "91330201MA2ABCDE9X",
|
||
},
|
||
宁波浙港供应链科技有限公司: {
|
||
contactPhone: "13900000088",
|
||
registeredAddress: "宁波市江北区长兴路689号",
|
||
bankAccount: "621700000000000088",
|
||
bankName: "中国银行宁波分行",
|
||
contactName: "陈经理",
|
||
legalRepresentative: "陈总",
|
||
creditCode: "91330200MA2SUPPLY01",
|
||
},
|
||
上海远洋能源有限公司: {
|
||
contactPhone: "13600000015",
|
||
registeredAddress: "上海市浦东新区临港大道188号",
|
||
bankAccount: "621483000000000015",
|
||
bankName: "招商银行上海分行",
|
||
contactName: "吴经理",
|
||
legalRepresentative: "孙先生",
|
||
creditCode: "91310115MAOCEAN001",
|
||
},
|
||
};
|
||
|
||
const isViewMode = computed(() => modalMode.value === "view");
|
||
|
||
const counterpartyParties = computed(() =>
|
||
counterpartyRoleKeys.value.map((roleKey) =>
|
||
getPartyByRoleKey(detailState.value.partyState, roleKey),
|
||
),
|
||
);
|
||
|
||
const displayedParties = computed(() => [
|
||
detailState.value.partyState.partyA,
|
||
...counterpartyParties.value,
|
||
]);
|
||
|
||
const counterpartySummaryText = computed(() =>
|
||
counterpartyParties.value.length
|
||
? counterpartyParties.value
|
||
.map((party) => party.companyName || party.displayName)
|
||
.join(";")
|
||
: "未填写",
|
||
);
|
||
|
||
const contractBodyDisplayText = computed(() =>
|
||
detailState.value.contractBodyFileName ||
|
||
(creationMode.value === "manual"
|
||
? "点击上传合同正文 Word 文件"
|
||
: templateName.value || "点击配置合同正文"),
|
||
);
|
||
|
||
const contractBodyHint = computed(() =>
|
||
creationMode.value === "manual"
|
||
? "当前为手动创建,支持上传并预览 .docx 正文"
|
||
: templateName.value
|
||
? `当前模板:${templateName.value},点击继续维护模板信息和预览`
|
||
: "当前为模板创建,点击维护模板信息和正文预览",
|
||
);
|
||
|
||
function syncUploadLists() {
|
||
otherAttachmentList.value = (detailState.value.otherAttachmentList || []).map(
|
||
(item, index) => ({
|
||
uid: item.uid || `other_${index + 1}`,
|
||
name: item.name,
|
||
size: item.size,
|
||
status: item.status || "done",
|
||
}),
|
||
);
|
||
}
|
||
|
||
function syncCounterpartyState() {
|
||
detailState.value.partyState = normalizePartyState(detailState.value.partyState);
|
||
counterpartyRoleKeys.value = buildCounterpartyRoleKeys(
|
||
detailState.value.partyState,
|
||
);
|
||
detailState.value.partyState.isThreePartyContract =
|
||
counterpartyRoleKeys.value.length > 1;
|
||
refreshPartyDisplayMeta();
|
||
}
|
||
|
||
function refreshPartyDisplayMeta() {
|
||
detailState.value.partyState.partyA.roleKey = "A";
|
||
detailState.value.partyState.partyA.roleName = "我方";
|
||
detailState.value.partyState.partyA.displayName = "我方";
|
||
|
||
counterpartyRoleKeys.value.forEach((roleKey, index) => {
|
||
const party = getPartyByRoleKey(detailState.value.partyState, roleKey);
|
||
party.roleKey = roleKey;
|
||
party.roleName = "相对方";
|
||
party.displayName = `相对方${index + 1}`;
|
||
});
|
||
}
|
||
|
||
function resetCurrentState() {
|
||
creationMode.value = "template";
|
||
templateKey.value = "";
|
||
templateName.value = "";
|
||
detailState.value = createContractFormData();
|
||
syncCounterpartyState();
|
||
syncUploadLists();
|
||
}
|
||
|
||
function showSearchTip(title) {
|
||
message.info(
|
||
`${title}查询接口当前未接入,这里先保留与原型一致的搜索入口样式。`,
|
||
);
|
||
}
|
||
|
||
function showInvoiceTitles(party, roleName) {
|
||
const titles = party.invoiceTitleOptions || [];
|
||
message.info(
|
||
titles.length
|
||
? `${roleName}备选开票抬头:${titles.join(";")}`
|
||
: `${roleName}暂无备选开票抬头`,
|
||
);
|
||
}
|
||
|
||
function handleCompanySelect(roleKey, companyName) {
|
||
const party =
|
||
roleKey === "A"
|
||
? detailState.value.partyState.partyA
|
||
: roleKey === "B"
|
||
? detailState.value.partyState.partyB
|
||
: detailState.value.partyState.partyC;
|
||
const profile = companyProfileMap[companyName];
|
||
if (!party || !profile) return;
|
||
|
||
Object.assign(party, {
|
||
companyName,
|
||
contactPhone: party.contactPhone || profile.contactPhone,
|
||
registeredAddress: party.registeredAddress || profile.registeredAddress,
|
||
bankAccount: party.bankAccount || profile.bankAccount,
|
||
bankName: party.bankName || profile.bankName,
|
||
contactName: party.contactName || profile.contactName,
|
||
legalRepresentative:
|
||
party.legalRepresentative || profile.legalRepresentative,
|
||
creditCode: party.creditCode || profile.creditCode,
|
||
});
|
||
}
|
||
|
||
function handleAddCounterparty() {
|
||
if (counterpartyRoleKeys.value.length >= MAX_COUNTERPARTY_COUNT) {
|
||
message.warning(`最多支持添加 ${MAX_COUNTERPARTY_COUNT} 个相对方`);
|
||
return;
|
||
}
|
||
|
||
const nextRoleKey = counterpartyRoleKeys.value.includes("B") ? "C" : "B";
|
||
detailState.value.partyState[nextRoleKey === "C" ? "partyC" : "partyB"] =
|
||
createEmptyCounterparty(nextRoleKey);
|
||
counterpartyRoleKeys.value = [...counterpartyRoleKeys.value, nextRoleKey];
|
||
detailState.value.partyState.isThreePartyContract =
|
||
counterpartyRoleKeys.value.length > 1;
|
||
refreshPartyDisplayMeta();
|
||
}
|
||
|
||
function handleRemoveCounterparty(roleKey) {
|
||
if (roleKey === "B" && counterpartyRoleKeys.value.includes("C")) {
|
||
detailState.value.partyState.partyB = {
|
||
...createEmptyCounterparty("B"),
|
||
...detailState.value.partyState.partyC,
|
||
roleKey: "B",
|
||
roleName: "相对方",
|
||
displayName: "相对方1",
|
||
};
|
||
detailState.value.partyState.partyC = createEmptyCounterparty("C");
|
||
counterpartyRoleKeys.value = ["B"];
|
||
} else {
|
||
detailState.value.partyState[roleKey === "C" ? "partyC" : "partyB"] =
|
||
createEmptyCounterparty(roleKey);
|
||
counterpartyRoleKeys.value = counterpartyRoleKeys.value.filter(
|
||
(key) => key !== roleKey,
|
||
);
|
||
}
|
||
|
||
detailState.value.partyState.isThreePartyContract =
|
||
counterpartyRoleKeys.value.length > 1;
|
||
refreshPartyDisplayMeta();
|
||
}
|
||
|
||
function handleOtherAttachmentUpload(file) {
|
||
const item = {
|
||
uid: `other_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`,
|
||
name: file.name,
|
||
size: file.size,
|
||
status: "done",
|
||
};
|
||
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 openBodyConfigModal() {
|
||
bodyConfigModalRef.value?.showModal(
|
||
{
|
||
detailState: copyContractFormData(detailState.value),
|
||
creationMode: creationMode.value,
|
||
templateKey: templateKey.value,
|
||
templateName: templateName.value,
|
||
},
|
||
isViewMode.value ? "view" : "edit",
|
||
);
|
||
}
|
||
|
||
function handleBodyConfigSave(payload = {}) {
|
||
creationMode.value = payload.creationMode || "template";
|
||
templateKey.value = payload.templateKey || "";
|
||
templateName.value = payload.templateName || "";
|
||
detailState.value = copyContractFormData(payload.detailState);
|
||
syncCounterpartyState();
|
||
syncUploadLists();
|
||
}
|
||
|
||
function addPerformancePlan() {
|
||
detailState.value.performancePlans.push(createPerformancePlan());
|
||
}
|
||
|
||
function removePerformancePlan(id) {
|
||
detailState.value.performancePlans =
|
||
detailState.value.performancePlans.filter((item) => item.id !== id);
|
||
}
|
||
|
||
function validateState() {
|
||
if (!detailState.value.partyState.partyA.companyName?.trim())
|
||
return "请确认我方信息";
|
||
if (!counterpartyRoleKeys.value.length) return "请至少添加一个相对方";
|
||
if (
|
||
counterpartyParties.value.some((party) => !party.companyName?.trim())
|
||
)
|
||
return "请选择相对方";
|
||
if (!detailState.value.contractName?.trim()) return "请填写合同名称";
|
||
if (!detailState.value.contractCode?.trim()) return "请填写合同号";
|
||
if (!detailState.value.contractType) 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.currencyName) return "请选择币种";
|
||
if (!detailState.value.sealType) return "请选择签订方式";
|
||
if (!detailState.value.mandateType) return "请选择委托类型";
|
||
if (!detailState.value.cargoType) return "请选择货种";
|
||
if (!detailState.value.contractTypeCode) return "请选择合同属性中的合同类型";
|
||
if (!detailState.value.executorAccount?.trim()) return "请填写合同执行人";
|
||
return "";
|
||
}
|
||
|
||
function handleSave() {
|
||
const errorMessage = validateState();
|
||
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);
|
||
|
||
emit("save", {
|
||
detailState: copyContractFormData(detailState.value),
|
||
creationMode: creationMode.value,
|
||
templateKey: templateKey.value,
|
||
templateName: templateName.value,
|
||
});
|
||
visible.value = false;
|
||
message.success("合同方信息已保存");
|
||
}
|
||
|
||
function showModal(context = {}, mode = "create") {
|
||
modalMode.value = mode || "create";
|
||
const source = context.detailState ||
|
||
(Object.keys(context || {}).length ? context : createContractFormData());
|
||
|
||
creationMode.value =
|
||
context.creationMode ||
|
||
(source.contractBodyArrayBuffer ? "manual" : "template");
|
||
templateKey.value = context.templateKey || "";
|
||
templateName.value = context.templateName || "";
|
||
detailState.value = copyContractFormData(source);
|
||
syncCounterpartyState();
|
||
syncUploadLists();
|
||
visible.value = true;
|
||
}
|
||
|
||
defineExpose({
|
||
showModal,
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
.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>
|