单方协议

This commit is contained in:
2026-06-09 15:27:54 +08:00
parent bed100becb
commit 591ada5405
8 changed files with 557 additions and 382 deletions
@@ -37,7 +37,7 @@
<div class="section-title-row counterparty-toolbar">
<div class="section-title">我方主体</div>
<a-button
v-if="!isViewMode"
v-if="!isViewMode && !isSingleAgreementChecked"
type="link"
class="text-action-btn"
@click="handleAddOurParty"
@@ -46,7 +46,7 @@
</a-button>
</div>
<div class="party-empty-state" style="margin-bottom: 12px">
当前登录主体必须包含在我方主体中
{{ isSingleAgreementChecked ? "单方协议默认使用当前登录主体,且不可修改" : "当前登录主体必须包含在我方主体中" }}
</div>
<div
@@ -59,7 +59,7 @@
{{ party.displayName }}
</div>
<a-button
v-if="!isViewMode && ourParties.length > 1"
v-if="!isViewMode && !isSingleAgreementChecked && ourParties.length > 1"
type="link"
danger
class="text-action-btn"
@@ -73,6 +73,7 @@
v-model:value="party.companyName"
:options="getPartyOptions(party.roleCode)"
show-search
:disabled="isSingleAgreementChecked"
:placeholder="`请选择${party.displayName}`"
:filter-option="filterPartnerOption"
@change="
@@ -116,13 +117,6 @@
<SearchOutlined />
</a-button>
</div>
<a-button
class="invoice-btn"
@click="showInvoiceTitles(party, party.displayName)"
>
<PlusOutlined />
备选开票抬头选择
</a-button>
</div>
<div class="section-title-row counterparty-toolbar">
@@ -365,7 +359,7 @@
</div>
<div class="summary-line">
<span>相对方</span
><strong>{{ counterpartySummaryText }}</strong>
><strong>{{ isSingleAgreementChecked ? "单方协议,无相对方" : counterpartySummaryText }}</strong>
</div>
</div>
@@ -401,7 +395,7 @@
</a-space>
</div>
<div class="upload-block">
<div v-if="!isSingleAgreementChecked" class="upload-block">
<div class="upload-label">签订依据附件</div>
<a-upload-dragger
:file-list="gistAttachmentList"
@@ -437,7 +431,10 @@
</div>
</div>
<div v-if="requiresAuthAttachment" class="upload-block">
<div
v-if="!isSingleAgreementChecked && requiresAuthAttachment"
class="upload-block"
>
<div class="upload-label">授权委托书附件</div>
<a-upload-dragger
:file-list="authAttachmentUploadList"
@@ -994,86 +991,6 @@
</div>
</section>
<a-collapse class="detail-collapse">
<a-collapse-panel
v-for="party in allPartyDetails"
:key="party.localId"
: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>
@@ -1131,11 +1048,6 @@ import {
// ========== 合同方相关字典 ==========
const partyCharacterOptions = [
{ label: "企业", value: "QY" },
{ label: "自然人", value: "ZZR" },
];
const companyTypeOptions = [
{ label: "客户", value: "客户" },
{ label: "供应商", value: "供应商" },
@@ -1144,38 +1056,6 @@ const companyTypeOptions = [
{ 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 = ref([
{ label: "扬州宏富特种材料有限公司", value: "扬州宏富特种材料有限公司" },
{ label: "宁波港船务货运代理有限公司", value: "宁波港船务货运代理有限公司" },
@@ -1382,9 +1262,6 @@ function createEmptyParty(roleCode, index = 0) {
companyType: "",
oppositeCharacter: "",
oppCharacter: "",
companyNature: "",
organizationNature: "",
enterpriseNatureLevel2: "",
contactName: "",
contactPhone: "",
contactEmail: "",
@@ -1398,7 +1275,6 @@ function createEmptyParty(roleCode, index = 0) {
naturalPersonIdNumber: "",
isGroupInternal: false,
isHongKongMacaoTaiwan: false,
invoiceTitleOptions: [],
};
}
@@ -1417,9 +1293,6 @@ function mapLaunchPartyToFormParty(item = {}) {
roleCode,
roleName: roleCode === PARTY_ROLE_OUR ? "我方" : "相对方",
subjectCode: item.subjectCode || "",
companyNature: getCompanyNatureByOppositeCharacter(
item.oppositeCharacter || item.oppCharacter,
),
oppCharacter: item.oppositeCharacter || item.oppCharacter || "",
oppositeCharacter: item.oppositeCharacter || item.oppCharacter || "",
displayName: "",
@@ -1445,16 +1318,6 @@ function buildPartyStateFromList(list = []) {
return normalizePartyState(result);
}
function getCompanyNatureByOppositeCharacter(value) {
const map = {
1: "境内组织",
2: "境内个人",
3: "境外组织",
4: "境外个人",
};
return map[String(value || "")] || "";
}
function isPartyFilled(party = {}) {
const ignoreKeys = new Set([
"localId",
@@ -1834,11 +1697,6 @@ const ourPartySummaryText = computed(() =>
: "未填写",
);
const allPartyDetails = computed(() => [
...ourParties.value,
...counterpartyParties.value,
]);
const signingSubjectCodeText = computed(() =>
ourParties.value
.map((party) => String(party.subjectCode || "").trim())
@@ -1870,6 +1728,23 @@ 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) => ({
@@ -1943,15 +1818,6 @@ function showSearchTip(title) {
message.info(`${title}数据已从客户/主体库加载,可直接在下拉框搜索选择。`);
}
function showInvoiceTitles(party, roleName) {
const titles = party.invoiceTitleOptions || [];
message.info(
titles.length
? `${roleName}备选开票抬头:${titles.join("")}`
: `${roleName}暂无备选开票抬头`,
);
}
function filterPartnerOption(input, option) {
const keyword = String(input || "")
.trim()
@@ -1978,27 +1844,6 @@ function getRelativeTypeLabel(value) {
return map[value] || value || "";
}
function getOrganizationNatureLabel(value) {
const map = {
ZY: "政府",
QY: "企业",
QT: "其他组织",
};
return map[value] || value || "";
}
function getEnterpriseNatureLevel2Label(value) {
const map = {
GNQY: "国有企业",
MYQY: "民营企业",
WZQY: "外资企业",
HZQY: "合资企业",
GYQZHKGQY: "国有全资和控股企业",
QT: "其他",
};
return map[value] || value || "";
}
function mapPartnerToParty(raw = {}, roleKey, companyName) {
const firstContact = raw.contacts?.[0] || {};
const roleCode =
@@ -2013,17 +1858,9 @@ function mapPartnerToParty(raw = {}, roleKey, companyName) {
companyType: getRelativeTypeLabel(raw.relativeType),
oppositeCharacter: raw.oppositeCharacter || "",
oppCharacter: raw.oppositeCharacter || "",
companyNature: getCompanyNatureByOppositeCharacter(raw.oppositeCharacter),
organizationNature: getOrganizationNatureLabel(raw.natureOfEnterpriseOrg),
enterpriseNatureLevel2: getEnterpriseNatureLevel2Label(
raw.natureOfEnterpriseOrgLev,
),
creditCode: raw.customerTaxno || "",
tinNo: raw.tinCode || "",
countryCode: raw.country || "",
legalRepresentative: raw.legalPersonName || "",
registeredAddress: raw.customerRegaddr || "",
registeredCapital: raw.regCapital || raw.registeredCapital || "",
contactName: firstContact.contactName || "",
contactPhone: firstContact.contactPhone || raw.officephone || "",
contactEmail: firstContact.email || raw.emailAddress || "",
@@ -2034,7 +1871,6 @@ function mapPartnerToParty(raw = {}, roleKey, companyName) {
naturalPersonIdNumber: raw.certificateNum || "",
isGroupInternal: raw.groupFlag === "Y" || raw.isJtn === "1",
isHongKongMacaoTaiwan: raw.gatFlag === "Y" || raw.isSgat === "1",
invoiceTitleOptions: raw.customerNameC ? [raw.customerNameC] : [],
roleCode,
roleName: roleCode === PARTY_ROLE_OUR ? "我方" : "相对方",
};
@@ -2109,13 +1945,12 @@ function handleCompanySelect(party, companyName, option = {}) {
legalRepresentative:
party.legalRepresentative || profile.legalRepresentative,
creditCode: party.creditCode || profile.creditCode,
oppCharacter:
party.oppCharacter ||
(String(party.companyNature || "").includes("个人") ? "2" : "1"),
oppCharacter: party.oppCharacter || "1",
});
}
function handleAddOurParty() {
if (isSingleAgreementChecked.value) return;
detailState.value.partyState.ourPartyList =
detailState.value.partyState.ourPartyList || [];
detailState.value.partyState.ourPartyList.push(
@@ -2136,6 +1971,7 @@ function handleContractTypeCodeChange(value) {
}
function handleAddCounterparty() {
if (isSingleAgreementChecked.value) return;
detailState.value.partyState.counterpartyList =
detailState.value.partyState.counterpartyList || [];
detailState.value.partyState.counterpartyList.push(
@@ -2148,6 +1984,7 @@ function handleAddCounterparty() {
}
function handleRemoveOurParty(localId) {
if (isSingleAgreementChecked.value) return;
detailState.value.partyState.ourPartyList = (
detailState.value.partyState.ourPartyList || []
).filter((item) => item.localId !== localId);
@@ -2160,6 +1997,7 @@ function handleRemoveOurParty(localId) {
}
function handleRemoveCounterparty(localId) {
if (isSingleAgreementChecked.value) return;
detailState.value.partyState.counterpartyList = (
detailState.value.partyState.counterpartyList || []
).filter((item) => item.localId !== localId);
@@ -2247,7 +2085,7 @@ async function downloadFile(filePath, fileName = "附件") {
}
try {
const resp = await downloadLaunchFile(filePath);
const arrayBuffer = resp.data;
const arrayBuffer = resp?.data ?? resp;
const blob = new Blob([arrayBuffer]);
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
@@ -2336,6 +2174,16 @@ function validateState() {
) {
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 (
@@ -2405,6 +2253,7 @@ function validateState() {
if (!detailState.value.contractBodyFileName?.trim())
return "请先配置合同正文";
if (
!isSingleAgreementChecked.value &&
requiresAuthAttachment.value &&
!(detailState.value.authAttachmentList || []).length
)
@@ -2473,9 +2322,7 @@ function buildLaunchRecord() {
oppositeId: party.oppositeId,
companyName: party.companyName,
companyType: party.companyType,
oppositeCharacter:
party.oppCharacter ||
(String(party.companyNature || "").includes("个人") ? "2" : "1"),
oppositeCharacter: party.oppCharacter || "1",
contactName: party.contactName,
contactPhone: party.contactPhone,
contactEmail: party.contactEmail,
@@ -2501,9 +2348,7 @@ function buildLaunchRecord() {
oppositeId: party.oppositeId,
companyName: party.companyName,
companyType: party.companyType,
oppositeCharacter:
party.oppCharacter ||
(String(party.companyNature || "").includes("个人") ? "2" : "1"),
oppositeCharacter: party.oppCharacter || "1",
contactName: party.contactName,
contactPhone: party.contactPhone,
contactEmail: party.contactEmail,
@@ -2675,7 +2520,6 @@ function applySelfProfile(profile = {}) {
bankAccount: profile.bankAccount || "",
legalRepresentative: profile.legalRepresentative || "",
creditCode: profile.creditCode || "",
invoiceTitleOptions: profile.invoiceTitleOptions || [],
};
const shouldHydrateFirstParty =
!firstOurParty.companyName ||
@@ -2699,12 +2543,11 @@ function applySelfProfile(profile = {}) {
legalRepresentative:
profile.legalRepresentative || firstOurParty.legalRepresentative,
creditCode: profile.creditCode || firstOurParty.creditCode,
invoiceTitleOptions:
profile.invoiceTitleOptions || firstOurParty.invoiceTitleOptions || [],
},
...(detailState.value.partyState.ourPartyList || []).slice(1),
];
refreshPartyDisplayMeta();
forceSingleAgreementDefaults();
}
async function showModal(context = {}, mode = "create") {
@@ -2724,6 +2567,7 @@ async function showModal(context = {}, mode = "create") {
detailState.value = copyContractFormData(source);
const selfProfile = await getLaunchSelfProfile();
applySelfProfile(selfProfile || {});
forceSingleAgreementDefaults();
syncCounterpartyState();
syncUploadLists();
loadPartnerOptions();