单方协议

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-row counterparty-toolbar">
<div class="section-title">我方主体</div> <div class="section-title">我方主体</div>
<a-button <a-button
v-if="!isViewMode" v-if="!isViewMode && !isSingleAgreementChecked"
type="link" type="link"
class="text-action-btn" class="text-action-btn"
@click="handleAddOurParty" @click="handleAddOurParty"
@@ -46,7 +46,7 @@
</a-button> </a-button>
</div> </div>
<div class="party-empty-state" style="margin-bottom: 12px"> <div class="party-empty-state" style="margin-bottom: 12px">
当前登录主体必须包含在我方主体中 {{ isSingleAgreementChecked ? "单方协议默认使用当前登录主体,且不可修改" : "当前登录主体必须包含在我方主体中" }}
</div> </div>
<div <div
@@ -59,7 +59,7 @@
{{ party.displayName }} {{ party.displayName }}
</div> </div>
<a-button <a-button
v-if="!isViewMode && ourParties.length > 1" v-if="!isViewMode && !isSingleAgreementChecked && ourParties.length > 1"
type="link" type="link"
danger danger
class="text-action-btn" class="text-action-btn"
@@ -73,6 +73,7 @@
v-model:value="party.companyName" v-model:value="party.companyName"
:options="getPartyOptions(party.roleCode)" :options="getPartyOptions(party.roleCode)"
show-search show-search
:disabled="isSingleAgreementChecked"
:placeholder="`请选择${party.displayName}`" :placeholder="`请选择${party.displayName}`"
:filter-option="filterPartnerOption" :filter-option="filterPartnerOption"
@change=" @change="
@@ -116,13 +117,6 @@
<SearchOutlined /> <SearchOutlined />
</a-button> </a-button>
</div> </div>
<a-button
class="invoice-btn"
@click="showInvoiceTitles(party, party.displayName)"
>
<PlusOutlined />
备选开票抬头选择
</a-button>
</div> </div>
<div class="section-title-row counterparty-toolbar"> <div class="section-title-row counterparty-toolbar">
@@ -365,7 +359,7 @@
</div> </div>
<div class="summary-line"> <div class="summary-line">
<span>相对方</span <span>相对方</span
><strong>{{ counterpartySummaryText }}</strong> ><strong>{{ isSingleAgreementChecked ? "单方协议,无相对方" : counterpartySummaryText }}</strong>
</div> </div>
</div> </div>
@@ -401,7 +395,7 @@
</a-space> </a-space>
</div> </div>
<div class="upload-block"> <div v-if="!isSingleAgreementChecked" class="upload-block">
<div class="upload-label">签订依据附件</div> <div class="upload-label">签订依据附件</div>
<a-upload-dragger <a-upload-dragger
:file-list="gistAttachmentList" :file-list="gistAttachmentList"
@@ -437,7 +431,10 @@
</div> </div>
</div> </div>
<div v-if="requiresAuthAttachment" class="upload-block"> <div
v-if="!isSingleAgreementChecked && requiresAuthAttachment"
class="upload-block"
>
<div class="upload-label">授权委托书附件</div> <div class="upload-label">授权委托书附件</div>
<a-upload-dragger <a-upload-dragger
:file-list="authAttachmentUploadList" :file-list="authAttachmentUploadList"
@@ -994,86 +991,6 @@
</div> </div>
</section> </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> </a-form>
</div> </div>
@@ -1131,11 +1048,6 @@ import {
// ========== 合同方相关字典 ========== // ========== 合同方相关字典 ==========
const partyCharacterOptions = [
{ label: "企业", value: "QY" },
{ label: "自然人", value: "ZZR" },
];
const companyTypeOptions = [ const companyTypeOptions = [
{ label: "客户", value: "客户" }, { label: "客户", value: "客户" },
{ label: "供应商", value: "供应商" }, { label: "供应商", value: "供应商" },
@@ -1144,38 +1056,6 @@ const companyTypeOptions = [
{ 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 = ref([ const contractCompanyOptions = ref([
{ label: "扬州宏富特种材料有限公司", value: "扬州宏富特种材料有限公司" }, { label: "扬州宏富特种材料有限公司", value: "扬州宏富特种材料有限公司" },
{ label: "宁波港船务货运代理有限公司", value: "宁波港船务货运代理有限公司" }, { label: "宁波港船务货运代理有限公司", value: "宁波港船务货运代理有限公司" },
@@ -1382,9 +1262,6 @@ function createEmptyParty(roleCode, index = 0) {
companyType: "", companyType: "",
oppositeCharacter: "", oppositeCharacter: "",
oppCharacter: "", oppCharacter: "",
companyNature: "",
organizationNature: "",
enterpriseNatureLevel2: "",
contactName: "", contactName: "",
contactPhone: "", contactPhone: "",
contactEmail: "", contactEmail: "",
@@ -1398,7 +1275,6 @@ function createEmptyParty(roleCode, index = 0) {
naturalPersonIdNumber: "", naturalPersonIdNumber: "",
isGroupInternal: false, isGroupInternal: false,
isHongKongMacaoTaiwan: false, isHongKongMacaoTaiwan: false,
invoiceTitleOptions: [],
}; };
} }
@@ -1417,9 +1293,6 @@ function mapLaunchPartyToFormParty(item = {}) {
roleCode, roleCode,
roleName: roleCode === PARTY_ROLE_OUR ? "我方" : "相对方", roleName: roleCode === PARTY_ROLE_OUR ? "我方" : "相对方",
subjectCode: item.subjectCode || "", subjectCode: item.subjectCode || "",
companyNature: getCompanyNatureByOppositeCharacter(
item.oppositeCharacter || item.oppCharacter,
),
oppCharacter: item.oppositeCharacter || item.oppCharacter || "", oppCharacter: item.oppositeCharacter || item.oppCharacter || "",
oppositeCharacter: item.oppositeCharacter || item.oppCharacter || "", oppositeCharacter: item.oppositeCharacter || item.oppCharacter || "",
displayName: "", displayName: "",
@@ -1445,16 +1318,6 @@ function buildPartyStateFromList(list = []) {
return normalizePartyState(result); return normalizePartyState(result);
} }
function getCompanyNatureByOppositeCharacter(value) {
const map = {
1: "境内组织",
2: "境内个人",
3: "境外组织",
4: "境外个人",
};
return map[String(value || "")] || "";
}
function isPartyFilled(party = {}) { function isPartyFilled(party = {}) {
const ignoreKeys = new Set([ const ignoreKeys = new Set([
"localId", "localId",
@@ -1834,11 +1697,6 @@ const ourPartySummaryText = computed(() =>
: "未填写", : "未填写",
); );
const allPartyDetails = computed(() => [
...ourParties.value,
...counterpartyParties.value,
]);
const signingSubjectCodeText = computed(() => const signingSubjectCodeText = computed(() =>
ourParties.value ourParties.value
.map((party) => String(party.subjectCode || "").trim()) .map((party) => String(party.subjectCode || "").trim())
@@ -1870,6 +1728,23 @@ const requiresAuthAttachment = computed(() =>
["GZBSQRQZ", "HTZYZBSQRQZ"].includes(detailState.value.sealType), ["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() { function syncUploadLists() {
otherAttachmentList.value = (detailState.value.otherAttachmentList || []).map( otherAttachmentList.value = (detailState.value.otherAttachmentList || []).map(
(item, index) => ({ (item, index) => ({
@@ -1943,15 +1818,6 @@ function showSearchTip(title) {
message.info(`${title}数据已从客户/主体库加载,可直接在下拉框搜索选择。`); message.info(`${title}数据已从客户/主体库加载,可直接在下拉框搜索选择。`);
} }
function showInvoiceTitles(party, roleName) {
const titles = party.invoiceTitleOptions || [];
message.info(
titles.length
? `${roleName}备选开票抬头:${titles.join("")}`
: `${roleName}暂无备选开票抬头`,
);
}
function filterPartnerOption(input, option) { function filterPartnerOption(input, option) {
const keyword = String(input || "") const keyword = String(input || "")
.trim() .trim()
@@ -1978,27 +1844,6 @@ function getRelativeTypeLabel(value) {
return map[value] || 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) { function mapPartnerToParty(raw = {}, roleKey, companyName) {
const firstContact = raw.contacts?.[0] || {}; const firstContact = raw.contacts?.[0] || {};
const roleCode = const roleCode =
@@ -2013,17 +1858,9 @@ function mapPartnerToParty(raw = {}, roleKey, companyName) {
companyType: getRelativeTypeLabel(raw.relativeType), companyType: getRelativeTypeLabel(raw.relativeType),
oppositeCharacter: raw.oppositeCharacter || "", oppositeCharacter: raw.oppositeCharacter || "",
oppCharacter: raw.oppositeCharacter || "", oppCharacter: raw.oppositeCharacter || "",
companyNature: getCompanyNatureByOppositeCharacter(raw.oppositeCharacter),
organizationNature: getOrganizationNatureLabel(raw.natureOfEnterpriseOrg),
enterpriseNatureLevel2: getEnterpriseNatureLevel2Label(
raw.natureOfEnterpriseOrgLev,
),
creditCode: raw.customerTaxno || "", creditCode: raw.customerTaxno || "",
tinNo: raw.tinCode || "",
countryCode: raw.country || "",
legalRepresentative: raw.legalPersonName || "", legalRepresentative: raw.legalPersonName || "",
registeredAddress: raw.customerRegaddr || "", registeredAddress: raw.customerRegaddr || "",
registeredCapital: raw.regCapital || raw.registeredCapital || "",
contactName: firstContact.contactName || "", contactName: firstContact.contactName || "",
contactPhone: firstContact.contactPhone || raw.officephone || "", contactPhone: firstContact.contactPhone || raw.officephone || "",
contactEmail: firstContact.email || raw.emailAddress || "", contactEmail: firstContact.email || raw.emailAddress || "",
@@ -2034,7 +1871,6 @@ function mapPartnerToParty(raw = {}, roleKey, companyName) {
naturalPersonIdNumber: raw.certificateNum || "", naturalPersonIdNumber: raw.certificateNum || "",
isGroupInternal: raw.groupFlag === "Y" || raw.isJtn === "1", isGroupInternal: raw.groupFlag === "Y" || raw.isJtn === "1",
isHongKongMacaoTaiwan: raw.gatFlag === "Y" || raw.isSgat === "1", isHongKongMacaoTaiwan: raw.gatFlag === "Y" || raw.isSgat === "1",
invoiceTitleOptions: raw.customerNameC ? [raw.customerNameC] : [],
roleCode, roleCode,
roleName: roleCode === PARTY_ROLE_OUR ? "我方" : "相对方", roleName: roleCode === PARTY_ROLE_OUR ? "我方" : "相对方",
}; };
@@ -2109,13 +1945,12 @@ function handleCompanySelect(party, companyName, option = {}) {
legalRepresentative: legalRepresentative:
party.legalRepresentative || profile.legalRepresentative, party.legalRepresentative || profile.legalRepresentative,
creditCode: party.creditCode || profile.creditCode, creditCode: party.creditCode || profile.creditCode,
oppCharacter: oppCharacter: party.oppCharacter || "1",
party.oppCharacter ||
(String(party.companyNature || "").includes("个人") ? "2" : "1"),
}); });
} }
function handleAddOurParty() { function handleAddOurParty() {
if (isSingleAgreementChecked.value) return;
detailState.value.partyState.ourPartyList = detailState.value.partyState.ourPartyList =
detailState.value.partyState.ourPartyList || []; detailState.value.partyState.ourPartyList || [];
detailState.value.partyState.ourPartyList.push( detailState.value.partyState.ourPartyList.push(
@@ -2136,6 +1971,7 @@ function handleContractTypeCodeChange(value) {
} }
function handleAddCounterparty() { function handleAddCounterparty() {
if (isSingleAgreementChecked.value) return;
detailState.value.partyState.counterpartyList = detailState.value.partyState.counterpartyList =
detailState.value.partyState.counterpartyList || []; detailState.value.partyState.counterpartyList || [];
detailState.value.partyState.counterpartyList.push( detailState.value.partyState.counterpartyList.push(
@@ -2148,6 +1984,7 @@ function handleAddCounterparty() {
} }
function handleRemoveOurParty(localId) { function handleRemoveOurParty(localId) {
if (isSingleAgreementChecked.value) return;
detailState.value.partyState.ourPartyList = ( detailState.value.partyState.ourPartyList = (
detailState.value.partyState.ourPartyList || [] detailState.value.partyState.ourPartyList || []
).filter((item) => item.localId !== localId); ).filter((item) => item.localId !== localId);
@@ -2160,6 +1997,7 @@ function handleRemoveOurParty(localId) {
} }
function handleRemoveCounterparty(localId) { function handleRemoveCounterparty(localId) {
if (isSingleAgreementChecked.value) return;
detailState.value.partyState.counterpartyList = ( detailState.value.partyState.counterpartyList = (
detailState.value.partyState.counterpartyList || [] detailState.value.partyState.counterpartyList || []
).filter((item) => item.localId !== localId); ).filter((item) => item.localId !== localId);
@@ -2247,7 +2085,7 @@ async function downloadFile(filePath, fileName = "附件") {
} }
try { try {
const resp = await downloadLaunchFile(filePath); const resp = await downloadLaunchFile(filePath);
const arrayBuffer = resp.data; const arrayBuffer = resp?.data ?? resp;
const blob = new Blob([arrayBuffer]); const blob = new Blob([arrayBuffer]);
const url = window.URL.createObjectURL(blob); const url = window.URL.createObjectURL(blob);
const link = document.createElement("a"); const link = document.createElement("a");
@@ -2336,6 +2174,16 @@ function validateState() {
) { ) {
return "当前登录主体必须包含在我方主体中"; return "当前登录主体必须包含在我方主体中";
} }
if (
isSingleAgreementChecked.value &&
currentSelfSubjectCode.value &&
ourParties.value.some(
(party) =>
String(party.subjectCode || "").trim() !== currentSelfSubjectCode.value,
)
) {
return "单方协议仅允许使用当前登录主体";
}
if (!isSingleAgreementChecked.value && !counterpartyParties.value.length) if (!isSingleAgreementChecked.value && !counterpartyParties.value.length)
return "请至少添加一个相对方"; return "请至少添加一个相对方";
if ( if (
@@ -2405,6 +2253,7 @@ function validateState() {
if (!detailState.value.contractBodyFileName?.trim()) if (!detailState.value.contractBodyFileName?.trim())
return "请先配置合同正文"; return "请先配置合同正文";
if ( if (
!isSingleAgreementChecked.value &&
requiresAuthAttachment.value && requiresAuthAttachment.value &&
!(detailState.value.authAttachmentList || []).length !(detailState.value.authAttachmentList || []).length
) )
@@ -2473,9 +2322,7 @@ function buildLaunchRecord() {
oppositeId: party.oppositeId, oppositeId: party.oppositeId,
companyName: party.companyName, companyName: party.companyName,
companyType: party.companyType, companyType: party.companyType,
oppositeCharacter: oppositeCharacter: party.oppCharacter || "1",
party.oppCharacter ||
(String(party.companyNature || "").includes("个人") ? "2" : "1"),
contactName: party.contactName, contactName: party.contactName,
contactPhone: party.contactPhone, contactPhone: party.contactPhone,
contactEmail: party.contactEmail, contactEmail: party.contactEmail,
@@ -2501,9 +2348,7 @@ function buildLaunchRecord() {
oppositeId: party.oppositeId, oppositeId: party.oppositeId,
companyName: party.companyName, companyName: party.companyName,
companyType: party.companyType, companyType: party.companyType,
oppositeCharacter: oppositeCharacter: party.oppCharacter || "1",
party.oppCharacter ||
(String(party.companyNature || "").includes("个人") ? "2" : "1"),
contactName: party.contactName, contactName: party.contactName,
contactPhone: party.contactPhone, contactPhone: party.contactPhone,
contactEmail: party.contactEmail, contactEmail: party.contactEmail,
@@ -2675,7 +2520,6 @@ function applySelfProfile(profile = {}) {
bankAccount: profile.bankAccount || "", bankAccount: profile.bankAccount || "",
legalRepresentative: profile.legalRepresentative || "", legalRepresentative: profile.legalRepresentative || "",
creditCode: profile.creditCode || "", creditCode: profile.creditCode || "",
invoiceTitleOptions: profile.invoiceTitleOptions || [],
}; };
const shouldHydrateFirstParty = const shouldHydrateFirstParty =
!firstOurParty.companyName || !firstOurParty.companyName ||
@@ -2699,12 +2543,11 @@ function applySelfProfile(profile = {}) {
legalRepresentative: legalRepresentative:
profile.legalRepresentative || firstOurParty.legalRepresentative, profile.legalRepresentative || firstOurParty.legalRepresentative,
creditCode: profile.creditCode || firstOurParty.creditCode, creditCode: profile.creditCode || firstOurParty.creditCode,
invoiceTitleOptions:
profile.invoiceTitleOptions || firstOurParty.invoiceTitleOptions || [],
}, },
...(detailState.value.partyState.ourPartyList || []).slice(1), ...(detailState.value.partyState.ourPartyList || []).slice(1),
]; ];
refreshPartyDisplayMeta(); refreshPartyDisplayMeta();
forceSingleAgreementDefaults();
} }
async function showModal(context = {}, mode = "create") { async function showModal(context = {}, mode = "create") {
@@ -2724,6 +2567,7 @@ async function showModal(context = {}, mode = "create") {
detailState.value = copyContractFormData(source); detailState.value = copyContractFormData(source);
const selfProfile = await getLaunchSelfProfile(); const selfProfile = await getLaunchSelfProfile();
applySelfProfile(selfProfile || {}); applySelfProfile(selfProfile || {});
forceSingleAgreementDefaults();
syncCounterpartyState(); syncCounterpartyState();
syncUploadLists(); syncUploadLists();
loadPartnerOptions(); loadPartnerOptions();
@@ -1170,7 +1170,7 @@ async function fetchTemplateBuffer(templateFilePath) {
} }
const resp = await downloadTemplateFile(templateFilePath); const resp = await downloadTemplateFile(templateFilePath);
const arrayBuffer = resp.data; const arrayBuffer = resp?.data ?? resp;
const buffer = const buffer =
arrayBuffer instanceof ArrayBuffer ? arrayBuffer : arrayBuffer?.buffer; arrayBuffer instanceof ArrayBuffer ? arrayBuffer : arrayBuffer?.buffer;
templateBufferCache.set(templateFilePath, buffer); templateBufferCache.set(templateFilePath, buffer);
@@ -1,7 +1,7 @@
<template> <template>
<a-modal <a-modal
v-model:open="visible" v-model:open="visible"
title="上传已签署版本" :title="isSingleAgreement ? '上传盖章版' : '上传已签署版本'"
width="560px" width="560px"
:mask-closable="false" :mask-closable="false"
:confirm-loading="submitting" :confirm-loading="submitting"
@@ -22,7 +22,7 @@
<a-divider style="margin: 16px 0" /> <a-divider style="margin: 16px 0" />
<a-form layout="vertical"> <a-form layout="vertical">
<a-form-item label="上传已签署合同文件" required> <a-form-item :label="isSingleAgreement ? '上传已盖章文件' : '上传已签署合同文件'" required>
<div <div
class="upload-dragger" class="upload-dragger"
:class="{ 'is-dragover': dragOver }" :class="{ 'is-dragover': dragOver }"
@@ -40,7 +40,7 @@
/> />
<template v-if="!uploadFile"> <template v-if="!uploadFile">
<cloud-upload-outlined :style="{ 'fontSize': '36px', 'color': '#b0b8c5' }" /> <cloud-upload-outlined :style="{ 'fontSize': '36px', 'color': '#b0b8c5' }" />
<p class="upload-text">点击或将已签署文件拖拽到这里</p> <p class="upload-text">{{ isSingleAgreement ? "点击或将已盖章文件拖拽到这里" : "点击或将已签署文件拖拽到这里" }}</p>
<p class="upload-hint">支持 PDFDOCDOCX 文件且小于 20M</p> <p class="upload-hint">支持 PDFDOCDOCX 文件且小于 20M</p>
</template> </template>
<template v-else> <template v-else>
@@ -79,11 +79,11 @@
/> />
</a-form-item> </a-form-item>
<a-form-item label="不在线签署的原因" required> <a-form-item :label="isSingleAgreement ? '盖章说明' : '不在线签署的原因'" required>
<a-textarea <a-textarea
v-model:value="offlineReason" v-model:value="offlineReason"
:rows="4" :rows="4"
placeholder="请填写不在线签署的原因,例如:客户无法配合在线签署、对方无数字证书等" :placeholder="isSingleAgreement ? '请填写盖章说明,例如:内部审批通过后线下加盖业务章' : '请填写不在线签署的原因,例如:客户无法配合在线签署、对方无数字证书等'"
:maxlength="500" :maxlength="500"
show-count show-count
/> />
@@ -111,13 +111,15 @@ const activeDate = ref("");
const dragOver = ref(false); const dragOver = ref(false);
const submitting = ref(false); const submitting = ref(false);
const fileInputRef = ref(null); const fileInputRef = ref(null);
const isSingleAgreement = ref(false);
const emit = defineEmits(["done"]); const emit = defineEmits(["done"]);
function showModal(record) { function showModal(record) {
row.value = record; row.value = record;
isSingleAgreement.value = String(record?.isSingleAgreement || "0") === "1";
uploadFile.value = null; uploadFile.value = null;
offlineReason.value = ""; offlineReason.value = isSingleAgreement.value ? "内部审批通过后线下加盖业务章" : "";
signDate.value = sliceDateValue(record?.signDate) || todayString(); signDate.value = sliceDateValue(record?.signDate) || todayString();
sealDate.value = sliceDateValue(record?.sealDate) || signDate.value; sealDate.value = sliceDateValue(record?.sealDate) || signDate.value;
activeDate.value = sliceDateValue(record?.activeDate) || sliceDateValue(record?.effectiveStart); activeDate.value = sliceDateValue(record?.activeDate) || sliceDateValue(record?.effectiveStart);
@@ -201,12 +203,12 @@ function formatFileSize(bytes) {
async function handleSubmit() { async function handleSubmit() {
if (!uploadFile.value) { if (!uploadFile.value) {
message.warning("请先上传已签署文件"); message.warning(isSingleAgreement.value ? "请先上传已盖章文件" : "请先上传已签署文件");
return; return;
} }
const reason = (offlineReason.value || "").trim(); const reason = (offlineReason.value || "").trim();
if (!reason) { if (!reason) {
message.warning("请填写不在线签署的原因"); message.warning(isSingleAgreement.value ? "请填写盖章说明" : "请填写不在线签署的原因");
return; return;
} }
if (!signDate.value) { if (!signDate.value) {
@@ -242,7 +244,7 @@ async function handleSubmit() {
const fileSize = uploadFile.value.size; const fileSize = uploadFile.value.size;
const finishRes = await finishLaunchSeal(row.value.id, { const finishRes = await finishLaunchSeal(row.value.id, {
sealContractId: row.value.sealContractId || `OFFLINE-SEAL-${row.value.id}`, sealContractId: row.value.sealContractId || `${isSingleAgreement.value ? "OFFLINE-SINGLE" : "OFFLINE-SEAL"}-${row.value.id}`,
sealContractStatus: 2000, sealContractStatus: 2000,
signedFileName: fileName, signedFileName: fileName,
signedFilePath: filePath, signedFilePath: filePath,
@@ -260,7 +262,7 @@ async function handleSubmit() {
return; return;
} }
message.success("已签署版本上传成功,合同已流转到待采集"); message.success(isSingleAgreement.value ? "盖章版上传成功,单方协议已办结" : "已签署版本上传成功,合同已流转到待采集");
visible.value = false; visible.value = false;
emit("done"); emit("done");
} catch (err) { } catch (err) {
@@ -50,6 +50,7 @@ export const currencyOptions = [
]; ];
export const sealTypeOptions = [ export const sealTypeOptions = [
{ label: "内部业务章", value: "YWZ" },
{ label: "公章", value: "GZ" }, { label: "公章", value: "GZ" },
{ label: "合同专用章", value: "HTZYZ" }, { label: "合同专用章", value: "HTZYZ" },
{ label: "公章+法定代表人私章", value: "GZFDDBRSZ" }, { label: "公章+法定代表人私章", value: "GZFDDBRSZ" },
+361 -82
View File
@@ -10,17 +10,29 @@
<a-row :gutter="16" style="width: 100%"> <a-row :gutter="16" style="width: 100%">
<a-col :md="6" :sm="24"> <a-col :md="6" :sm="24">
<a-form-item label="合同名称"> <a-form-item label="合同名称">
<a-input v-model:value="queryParam.contractName" allow-clear placeholder="请输入合同名称" /> <a-input
v-model:value="queryParam.contractName"
allow-clear
placeholder="请输入合同名称"
/>
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :md="6" :sm="24"> <a-col :md="6" :sm="24">
<a-form-item label="合同编码"> <a-form-item label="合同编码">
<a-input v-model:value="queryParam.contractCode" allow-clear placeholder="请输入合同编码" /> <a-input
v-model:value="queryParam.contractCode"
allow-clear
placeholder="请输入合同编码"
/>
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :md="6" :sm="24"> <a-col :md="6" :sm="24">
<a-form-item label="合同类型"> <a-form-item label="合同类型">
<a-input v-model:value="queryParam.contractType" allow-clear placeholder="请输入合同类型编码" /> <a-input
v-model:value="queryParam.contractType"
allow-clear
placeholder="请输入合同类型编码"
/>
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :md="6" :sm="24"> <a-col :md="6" :sm="24">
@@ -55,10 +67,36 @@
<vxe-toolbar ref="toolbarRef" custom> <vxe-toolbar ref="toolbarRef" custom>
<template #buttons> <template #buttons>
<a-space wrap> <a-space wrap>
<a-button type="primary" :icon="h(SearchOutlined)" @click="handleQuery">查询</a-button> <a-button
<a-button :icon="h(RedoOutlined)" @click="handleReset">重置</a-button> type="primary"
<a-button type="primary" ghost :icon="h(PlusOutlined)" @click="handleCreate">发起合同</a-button> :icon="h(SearchOutlined)"
<a-button danger :icon="h(DeleteOutlined)" :disabled="!selectedRowKeys.length" @click="handleBatchDelete">删除</a-button> @click="handleQuery"
>查询</a-button
>
<a-button :icon="h(RedoOutlined)" @click="handleReset"
>重置</a-button
>
<a-button
type="primary"
ghost
:icon="h(PlusOutlined)"
@click="handleCreate"
>发起合同</a-button
>
<a-button
type="primary"
ghost
:icon="h(PlusOutlined)"
@click="handleCreateSingleAgreement"
>发起单方协议</a-button
>
<a-button
danger
:icon="h(DeleteOutlined)"
:disabled="!selectedRowKeys.length"
@click="handleBatchDelete"
>删除</a-button
>
</a-space> </a-space>
</template> </template>
</vxe-toolbar> </vxe-toolbar>
@@ -74,33 +112,97 @@
:checkbox-config="{ range: true }" :checkbox-config="{ range: true }"
:data="tableData" :data="tableData"
:loading="loading" :loading="loading"
:loading-config="{ icon: 'vxe-icon-indicator roll', text: '正在拼命加载中...' }" :loading-config="{
icon: 'vxe-icon-indicator roll',
text: '正在拼命加载中...',
}"
@checkbox-range-change="selectChangeEvent" @checkbox-range-change="selectChangeEvent"
@checkbox-change="selectChangeEvent" @checkbox-change="selectChangeEvent"
@checkbox-all="selectChangeEvent" @checkbox-all="selectChangeEvent"
> >
<vxe-column type="checkbox" width="56" /> <vxe-column type="checkbox" width="56" />
<vxe-column type="seq" title="序号" width="68" /> <vxe-column type="seq" title="序号" width="68" />
<vxe-column field="contractCode" title="合同编码" width="160" show-overflow="title" /> <vxe-column
<vxe-column field="lawContractCode" title="法务合同编号" width="160" show-overflow="title" /> field="contractCode"
<vxe-column field="contractName" title="合同名称" width="220" show-overflow="title" /> title="合同编码"
<vxe-column field="contractType" title="合同类型" width="160" show-overflow="title"> width="160"
show-overflow="title"
/>
<vxe-column
field="lawContractCode"
title="法务合同编号"
width="160"
show-overflow="title"
/>
<vxe-column
field="contractName"
title="合同名称"
width="220"
show-overflow="title"
/>
<vxe-column
field="contractType"
title="合同类型"
width="160"
show-overflow="title"
>
<template #default="{ row }"> <template #default="{ row }">
{{ getContractTypeSimpleLabel(row.contractTypeCode || row.contractType) }} {{
getContractTypeSimpleLabel(
row.contractTypeCode || row.contractType,
)
}}
</template> </template>
</vxe-column> </vxe-column>
<vxe-column field="templateName" title="所选模板" width="180" show-overflow="title" /> <vxe-column
<vxe-column field="counterpartySummary" title="合同相对方" width="260" show-overflow="title" /> field="templateName"
title="所选模板"
width="180"
show-overflow="title"
/>
<vxe-column
field="counterpartySummary"
title="合同相对方"
width="260"
show-overflow="title"
/>
<vxe-column field="status" title="状态" width="120"> <vxe-column field="status" title="状态" width="120">
<template #default="{ row }"> <template #default="{ row }">
<a-tag :color="getStatusColor(row.status)">{{ getStatusLabel(row.status) }}</a-tag> <a-tag :color="getStatusColor(row.status)">{{
getStatusLabelByRow(row)
}}</a-tag>
</template> </template>
</vxe-column> </vxe-column>
<vxe-column field="approveMessage" title="审批说明" width="220" show-overflow="title" /> <vxe-column
<vxe-column field="initiator" title="发起人" width="100" show-overflow="title" /> field="approveMessage"
<vxe-column field="initiatorDept" title="所属部门" width="140" show-overflow="title" /> title="审批说明"
<vxe-column field="createTime" title="发起时间" width="170" show-overflow="title" /> width="220"
<vxe-column field="updateTime" title="最后更新时间" width="170" show-overflow="title" /> show-overflow="title"
/>
<vxe-column
field="initiator"
title="发起人"
width="100"
show-overflow="title"
/>
<vxe-column
field="initiatorDept"
title="所属部门"
width="140"
show-overflow="title"
/>
<vxe-column
field="createTime"
title="发起时间"
width="170"
show-overflow="title"
/>
<vxe-column
field="updateTime"
title="最后更新时间"
width="170"
show-overflow="title"
/>
<vxe-column title="操作" width="720" fixed="right"> <vxe-column title="操作" width="720" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<div class="table-actions"> <div class="table-actions">
@@ -108,17 +210,47 @@
<a @click="handleCopy(row)">复制</a> <a @click="handleCopy(row)">复制</a>
<a v-if="canChange(row)" @click="handleChange(row)">变更</a> <a v-if="canChange(row)" @click="handleChange(row)">变更</a>
<a v-if="canEdit(row)" @click="handleEdit(row)">编辑</a> <a v-if="canEdit(row)" @click="handleEdit(row)">编辑</a>
<a v-if="canSubmit(row)" @click="handleSubmit(row)">{{ isSingleAgreementRecord(row) ? "提交审批" : "提交法务" }}</a> <a v-if="canSubmit(row)" @click="handleSubmit(row)">{{
<a v-if="canViewApprove(row)" @click="handleViewApprove(row)">审批记录</a> isSingleAgreementRecord(row) ? "提交审批" : "提交法务"
<a v-if="canViewLawCallback(row)" @click="handleViewLawCallback(row)">回调data</a> }}</a>
<a v-if="canMockApprove(row)" @click="handleMockApprove(row)">{{ isSingleAgreementRecord(row) ? "审批通过" : "测试通过" }}</a> <a v-if="canViewApprove(row)" @click="handleViewApprove(row)"
<a v-if="canMockReject(row)" @click="handleMockReject(row)">{{ isSingleAgreementRecord(row) ? "审批驳回" : "测试驳回" }}</a> >审批记录</a
<a v-if="canOnlineSign(row)" @click="handleOnlineSign(row)">在线签订</a> >
<a v-if="canDownloadSigned(row)" @click="handleDownloadSigned(row)">下载签署文件</a> <a
<a v-if="canUploadSigned(row)" @click="handleUploadSigned(row)">上传已签署版本</a> v-if="canViewLawCallback(row)"
<a v-if="canMockFinishSign(row)" @click="handleMockFinishSign(row)">测试签署完成</a> @click="handleViewLawCallback(row)"
<a v-if="canCollect(row)" @click="handleMockCollect(row)">测试采集</a> >回调data</a
<a v-if="canArchive(row)" @click="handleArchive(row)">归档</a> >
<a v-if="canMockApprove(row)" @click="handleMockApprove(row)">{{
isSingleAgreementRecord(row) ? "审批通过" : "测试通过"
}}</a>
<a v-if="canMockReject(row)" @click="handleMockReject(row)">{{
isSingleAgreementRecord(row) ? "审批驳回" : "测试驳回"
}}</a>
<a v-if="canOnlineSign(row)" @click="handleOnlineSign(row)">{{
isSingleAgreementRecord(row) ? "发起盖章" : "在线签订"
}}</a>
<a
v-if="canDownloadSigned(row)"
@click="handleDownloadSigned(row)"
>下载签署文件</a
>
<a v-if="canUploadSigned(row)" @click="handleUploadSigned(row)">{{
isSingleAgreementRecord(row) ? "上传盖章版" : "上传已签署版本"
}}</a>
<a
v-if="canMockFinishSign(row)"
@click="handleMockFinishSign(row)"
>{{
isSingleAgreementRecord(row) ? "测试盖章完成" : "测试签署完成"
}}</a
>
<a v-if="canCollect(row)" @click="handleMockCollect(row)">{{
isSingleAgreementRecord(row) ? "测试生效" : "测试采集"
}}</a>
<a v-if="canArchive(row)" @click="handleArchive(row)">{{
isSingleAgreementRecord(row) ? "办结" : "归档"
}}</a>
<a v-if="canTerminate(row)" @click="handleTerminate(row)">终止</a> <a v-if="canTerminate(row)" @click="handleTerminate(row)">终止</a>
<a v-if="canDelete(row)" @click="handleDelete(row)">删除</a> <a v-if="canDelete(row)" @click="handleDelete(row)">删除</a>
</div> </div>
@@ -141,10 +273,22 @@
/> />
</a-card> </a-card>
<ContractPartyPlaceholderModal ref="contractModalRef" @save="handleModalSave" /> <ContractPartyPlaceholderModal
<MixedSignModal ref="mixedSignModalRef" @business-updated="handleSignBusinessUpdated" /> ref="contractModalRef"
<UploadSignedModal ref="uploadSignedModalRef" @done="handleSignBusinessUpdated" /> @save="handleModalSave"
<ContractArchiveModal ref="contractArchiveModalRef" @done="handleArchiveDone" /> />
<MixedSignModal
ref="mixedSignModalRef"
@business-updated="handleSignBusinessUpdated"
/>
<UploadSignedModal
ref="uploadSignedModalRef"
@done="handleSignBusinessUpdated"
/>
<ContractArchiveModal
ref="contractArchiveModalRef"
@done="handleArchiveDone"
/>
</section> </section>
</template> </template>
@@ -302,6 +446,13 @@ function getStatusColor(status) {
return statusColorMap[status] || "default"; return statusColorMap[status] || "default";
} }
function getStatusLabelByRow(row) {
if (isSingleAgreementRecord(row) && row?.status === "pending_sign") {
return "待盖章";
}
return getStatusLabel(row?.status);
}
function canEdit(row) { function canEdit(row) {
return ["draft", "rejected"].includes(row.status); return ["draft", "rejected"].includes(row.status);
} }
@@ -316,7 +467,9 @@ function canSubmit(row) {
function canChange(row) { function canChange(row) {
// 变更/补充协议:仅已完成/待采集/待归档的合同允许基于它发起变更 // 变更/补充协议:仅已完成/待采集/待归档的合同允许基于它发起变更
return ["completed", "pending_archive", "pending_collect"].includes(row.status); return ["completed", "pending_archive", "pending_collect"].includes(
row.status,
);
} }
function canMockApprove(row) { function canMockApprove(row) {
@@ -329,11 +482,21 @@ function canMockReject(row) {
function canOnlineSign(row) { function canOnlineSign(row) {
// 在线签订只对电子合同开放;纸质/线下签署走“上传已签署版本”兜底。 // 在线签订只对电子合同开放;纸质/线下签署走“上传已签署版本”兜底。
return String(row.isElectron || "1") === "1" && ["pending_sign", "signing"].includes(row.status); return (
!isSingleAgreementRecord(row) &&
String(row.isElectron || "1") === "1" &&
["pending_sign", "signing"].includes(row.status)
);
} }
function canMockFinishSign(row) { function canMockFinishSign(row) {
return String(row.isElectron || "1") === "1" && ["pending_sign", "signing"].includes(row.status); if (isSingleAgreementRecord(row)) {
return row.status === "pending_sign";
}
return (
String(row.isElectron || "1") === "1" &&
["pending_sign", "signing"].includes(row.status)
);
} }
function canUploadSigned(row) { function canUploadSigned(row) {
@@ -341,11 +504,11 @@ function canUploadSigned(row) {
} }
function canCollect(row) { function canCollect(row) {
return row.status === "pending_collect"; return !isSingleAgreementRecord(row) && row.status === "pending_collect";
} }
function canArchive(row) { function canArchive(row) {
return row.status === "pending_archive"; return !isSingleAgreementRecord(row) && row.status === "pending_archive";
} }
function isSingleAgreementRecord(row) { function isSingleAgreementRecord(row) {
@@ -353,14 +516,34 @@ function isSingleAgreementRecord(row) {
} }
function canViewApprove(row) { function canViewApprove(row) {
return !isSingleAgreementRecord(row) return (
&& Boolean(row.lawContractId) !isSingleAgreementRecord(row) &&
&& ["reviewing", "rejected", "pending_sign", "signing", "pending_collect", "pending_archive", "completed"].includes(row.status); Boolean(row.lawContractId) &&
[
"reviewing",
"rejected",
"pending_sign",
"signing",
"pending_collect",
"pending_archive",
"completed",
].includes(row.status)
);
} }
function canViewLawCallback(row) { function canViewLawCallback(row) {
return !isSingleAgreementRecord(row) return (
&& ["reviewing", "rejected", "pending_sign", "signing", "pending_collect", "pending_archive", "completed"].includes(row.status); !isSingleAgreementRecord(row) &&
[
"reviewing",
"rejected",
"pending_sign",
"signing",
"pending_collect",
"pending_archive",
"completed",
].includes(row.status)
);
} }
function canDownloadSigned(row) { function canDownloadSigned(row) {
@@ -441,6 +624,10 @@ function handleCreate() {
contractModalRef.value?.showModal({}, "create"); contractModalRef.value?.showModal({}, "create");
} }
function handleCreateSingleAgreement() {
contractModalRef.value?.showModal({ isSingleAgreement: "1" }, "create");
}
function handleView(row) { function handleView(row) {
getLaunchDetail(row.id).then((res) => { getLaunchDetail(row.id).then((res) => {
if (!isSuccessResponse(res) || !getDataResult(res)) { if (!isSuccessResponse(res) || !getDataResult(res)) {
@@ -511,20 +698,34 @@ function handleSubmit(row) {
Modal.confirm({ Modal.confirm({
title: singleAgreement ? "确认提交内部审批" : "确认提交法务审批", title: singleAgreement ? "确认提交内部审批" : "确认提交法务审批",
content: h("div", [ content: h("div", [
h("p", singleAgreement h(
"p",
singleAgreement
? `确定将单方协议【${row.contractName}】提交内部审批吗?` ? `确定将单方协议【${row.contractName}】提交内部审批吗?`
: `确定将合同【${row.contractName}】提交法务系统审批吗?`), : `确定将合同【${row.contractName}】提交法务系统审批吗?`,
h("p", { style: { color: "#999", fontSize: "12px" } }, "提交后合同进入审批中状态审批完成前不可编辑"), ),
h(
"p",
{ style: { color: "#999", fontSize: "12px" } },
"提交后合同进入审批中状态审批完成前不可编辑",
),
]), ]),
okText: "确认提交", okText: "确认提交",
cancelText: "取消", cancelText: "取消",
onOk: () => onOk: () =>
submitLaunchRecord(row.id).then((res) => { submitLaunchRecord(row.id).then((res) => {
if (!isSuccessResponse(res)) { if (!isSuccessResponse(res)) {
message.error(getResponseMessage(res, singleAgreement ? "提交内部审批失败" : "提交法务失败")); message.error(
getResponseMessage(
res,
singleAgreement ? "提交内部审批失败" : "提交法务失败",
),
);
return; return;
} }
message.success(singleAgreement ? "已提交内部审批等待审批" : "已提交法务等待审批"); message.success(
singleAgreement ? "已提交内部审批等待审批" : "已提交法务等待审批",
);
getList(); getList();
}), }),
icon: null, icon: null,
@@ -555,16 +756,41 @@ function handleViewApprove(row) {
} }
function buildLawCallbackModalContent(sample) { function buildLawCallbackModalContent(sample) {
const approvedJson = JSON.stringify({ data: sample?.approvedData || {} }, null, 2); const approvedJson = JSON.stringify(
const rejectedJson = JSON.stringify({ data: sample?.rejectedData || {} }, null, 2); { data: sample?.approvedData || {} },
return h("div", { style: { display: "flex", flexDirection: "column", gap: "12px" } }, [ null,
2,
);
const rejectedJson = JSON.stringify(
{ data: sample?.rejectedData || {} },
null,
2,
);
return h(
"div",
{ style: { display: "flex", flexDirection: "column", gap: "12px" } },
[
h("div", [ h("div", [
h("div", { style: { marginBottom: "6px", fontWeight: 600 } }, "回调地址"), h(
h("div", { style: { color: "#666", wordBreak: "break-all" } }, `${sample?.callbackMethod || "POST"} ${sample?.callbackUrl || "/contract/launch/callback/law/approve"}`), "div",
{ style: { marginBottom: "6px", fontWeight: 600 } },
"回调地址",
),
h(
"div",
{ style: { color: "#666", wordBreak: "break-all" } },
`${sample?.callbackMethod || "POST"} ${sample?.callbackUrl || "/contract/launch/callback/law/approve"}`,
),
]), ]),
h("div", [ h("div", [
h("div", { style: { marginBottom: "6px", fontWeight: 600 } }, "审批通过 data"), h(
h("pre", { "div",
{ style: { marginBottom: "6px", fontWeight: 600 } },
"审批通过 data",
),
h(
"pre",
{
style: { style: {
margin: 0, margin: 0,
maxHeight: "260px", maxHeight: "260px",
@@ -577,11 +803,19 @@ function buildLawCallbackModalContent(sample) {
whiteSpace: "pre-wrap", whiteSpace: "pre-wrap",
wordBreak: "break-word", wordBreak: "break-word",
}, },
}, approvedJson), },
approvedJson,
),
]), ]),
h("div", [ h("div", [
h("div", { style: { marginBottom: "6px", fontWeight: 600 } }, "审批驳回 data"), h(
h("pre", { "div",
{ style: { marginBottom: "6px", fontWeight: 600 } },
"审批驳回 data",
),
h(
"pre",
{
style: { style: {
margin: 0, margin: 0,
maxHeight: "220px", maxHeight: "220px",
@@ -594,9 +828,12 @@ function buildLawCallbackModalContent(sample) {
whiteSpace: "pre-wrap", whiteSpace: "pre-wrap",
wordBreak: "break-word", wordBreak: "break-word",
}, },
}, rejectedJson), },
rejectedJson,
),
]), ]),
]); ],
);
} }
function handleViewLawCallback(row) { function handleViewLawCallback(row) {
@@ -618,7 +855,9 @@ function handleViewLawCallback(row) {
function handleMockApprove(row) { function handleMockApprove(row) {
const singleAgreement = isSingleAgreementRecord(row); const singleAgreement = isSingleAgreementRecord(row);
Modal.confirm({ Modal.confirm({
title: singleAgreement ? "测试操作模拟内部审批通过" : "测试操作模拟审批通过", title: singleAgreement
? "测试操作模拟内部审批通过"
: "测试操作模拟审批通过",
content: singleAgreement content: singleAgreement
? `模拟单方协议【${row.contractName}】内部审批通过,状态将变为「待签署」。` ? `模拟单方协议【${row.contractName}】内部审批通过,状态将变为「待签署」。`
: `模拟法务审批通过合同【${row.contractName}】,状态将变为「待签署」。`, : `模拟法务审批通过合同【${row.contractName}】,状态将变为「待签署」。`,
@@ -640,12 +879,21 @@ function handleMockApprove(row) {
function handleMockReject(row) { function handleMockReject(row) {
const singleAgreement = isSingleAgreementRecord(row); const singleAgreement = isSingleAgreementRecord(row);
Modal.confirm({ Modal.confirm({
title: singleAgreement ? "测试操作模拟内部审批驳回" : "测试操作模拟审批驳回", title: singleAgreement
? "测试操作模拟内部审批驳回"
: "测试操作模拟审批驳回",
content: h("div", [ content: h("div", [
h("p", singleAgreement h(
"p",
singleAgreement
? `模拟单方协议【${row.contractName}】内部审批驳回,状态将回到「已驳回」。` ? `模拟单方协议【${row.contractName}】内部审批驳回,状态将回到「已驳回」。`
: `模拟法务审批驳回合同【${row.contractName}】,状态将回到「已驳回」。`), : `模拟法务审批驳回合同【${row.contractName}】,状态将回到「已驳回」。`,
h("p", { style: { color: "#999", fontSize: "12px" } }, "驳回后可重新编辑再提交"), ),
h(
"p",
{ style: { color: "#999", fontSize: "12px" } },
"驳回后可重新编辑再提交",
),
]), ]),
okText: "确认驳回", okText: "确认驳回",
cancelText: "取消", cancelText: "取消",
@@ -663,15 +911,23 @@ function handleMockReject(row) {
} }
function handleMockFinishSign(row) { function handleMockFinishSign(row) {
const singleAgreement = isSingleAgreementRecord(row);
Modal.confirm({ Modal.confirm({
title: "测试操作模拟签署完成", title: singleAgreement
content: `模拟合同【${row.contractName}】签署完成,状态将变为「待采集」。`, ? "测试操作模拟盖章完成"
: "测试操作模拟签署完成",
content: singleAgreement
? `模拟单方协议【${row.contractName}】内部业务章加盖完成,状态将变为「已完成」。`
: `模拟合同【${row.contractName}】签署完成,状态将变为「待采集」。`,
okText: "确认模拟", okText: "确认模拟",
cancelText: "取消", cancelText: "取消",
onOk: () => { onOk: () => {
const signDate = sliceDateValue(row.signDate) || todayString(); const signDate = sliceDateValue(row.signDate) || todayString();
const sealDate = sliceDateValue(row.sealDate) || signDate; const sealDate = sliceDateValue(row.sealDate) || signDate;
const activeDate = sliceDateValue(row.activeDate) || sliceDateValue(row.effectiveStart) || signDate; const activeDate =
sliceDateValue(row.activeDate) ||
sliceDateValue(row.effectiveStart) ||
signDate;
return finishLaunchSeal(row.id, { return finishLaunchSeal(row.id, {
sealContractId: row.sealContractId || `TEST-SEAL-${row.id}`, sealContractId: row.sealContractId || `TEST-SEAL-${row.id}`,
sealContractStatus: 2000, sealContractStatus: 2000,
@@ -679,16 +935,27 @@ function handleMockFinishSign(row) {
signedFilePath: row.contractTextFilePath || `mock-signed/${row.id}.pdf`, signedFilePath: row.contractTextFilePath || `mock-signed/${row.id}.pdf`,
signedFileType: "pdf", signedFileType: "pdf",
signedFileSize: row.contractTextFileSize || null, signedFileSize: row.contractTextFileSize || null,
signedOfflineReason: "测试按钮模拟签署完成真实签章接入后可删除此按钮", signedOfflineReason: singleAgreement
? "测试按钮模拟内部业务章加盖完成"
: "测试按钮模拟签署完成真实签章接入后可删除此按钮",
signDate: formatDateTimeValue(signDate), signDate: formatDateTimeValue(signDate),
sealDate: formatDateTimeValue(sealDate), sealDate: formatDateTimeValue(sealDate),
activeDate: formatDateTimeValue(activeDate), activeDate: formatDateTimeValue(activeDate),
}).then((res) => { }).then((res) => {
if (!isSuccessResponse(res)) { if (!isSuccessResponse(res)) {
message.error(getResponseMessage(res, "测试签署完成失败")); message.error(
getResponseMessage(
res,
singleAgreement ? "测试盖章完成失败" : "测试签署完成失败",
),
);
return; return;
} }
message.success("测试签署完成合同已流转到待采集"); message.success(
singleAgreement
? "测试盖章完成单方协议已办结"
: "测试签署完成合同已流转到待采集",
);
getList(); getList();
}); });
}, },
@@ -698,13 +965,16 @@ function handleMockFinishSign(row) {
function handleDownloadSigned(row) { function handleDownloadSigned(row) {
const filePath = row.signedFilePath; const filePath = row.signedFilePath;
const fileName = row.signedFileName || `${row.contractName || "合同"}-已签署版.pdf`; const fileName =
row.signedFileName || `${row.contractName || "合同"}-已签署版.pdf`;
if (!filePath) { if (!filePath) {
message.warning("暂无签署文件可下载"); message.warning("暂无签署文件可下载");
return; return;
} }
downloadLaunchFile(filePath).then((res) => { downloadLaunchFile(filePath)
const blob = new Blob([res.data]); .then((res) => {
const arrayBuffer = res?.data ?? res;
const blob = new Blob([arrayBuffer]);
const url = window.URL.createObjectURL(blob); const url = window.URL.createObjectURL(blob);
const link = document.createElement("a"); const link = document.createElement("a");
link.href = url; link.href = url;
@@ -713,7 +983,8 @@ function handleDownloadSigned(row) {
link.click(); link.click();
document.body.removeChild(link); document.body.removeChild(link);
window.URL.revokeObjectURL(url); window.URL.revokeObjectURL(url);
}).catch(() => { })
.catch(() => {
message.error("签署文件下载失败"); message.error("签署文件下载失败");
}); });
} }
@@ -741,7 +1012,8 @@ function handleSignBusinessUpdated() {
} }
function handleModalSave(payload) { function handleModalSave(payload) {
const requestFn = payload.mode === "edit" ? updateLaunchDraft : createLaunchDraft; const requestFn =
payload.mode === "edit" ? updateLaunchDraft : createLaunchDraft;
requestFn(payload.record).then((res) => { requestFn(payload.record).then((res) => {
if (!isSuccessResponse(res)) { if (!isSuccessResponse(res)) {
message.error(getResponseMessage(res, "保存合同失败")); message.error(getResponseMessage(res, "保存合同失败"));
@@ -792,8 +1064,14 @@ function handleTerminate(row) {
let terminateReason = ""; let terminateReason = "";
Modal.confirm({ Modal.confirm({
title: `确认终止合同【${row.contractName}】吗?`, title: `确认终止合同【${row.contractName}】吗?`,
content: h("div", { style: { display: "flex", flexDirection: "column", gap: "12px" } }, [ content: h(
h("div", "除已完成外任意环节都可终止当前仅终止业务系统内流程法务系统需分别操作"), "div",
{ style: { display: "flex", flexDirection: "column", gap: "12px" } },
[
h(
"div",
"除已完成外任意环节都可终止当前仅终止业务系统内流程法务系统需分别操作",
),
h(Input.TextArea, { h(Input.TextArea, {
rows: 4, rows: 4,
placeholder: "可选请输入终止原因", placeholder: "可选请输入终止原因",
@@ -801,7 +1079,8 @@ function handleTerminate(row) {
terminateReason = event?.target?.value || ""; terminateReason = event?.target?.value || "";
}, },
}), }),
]), ],
),
okText: "确认终止", okText: "确认终止",
cancelText: "取消", cancelText: "取消",
okType: "danger", okType: "danger",
+2 -1
View File
@@ -371,7 +371,8 @@ function handleExportExcel() {
pageNo: 1, pageNo: 1,
pageSize: 10000, pageSize: 10000,
}).then((data) => { }).then((data) => {
const blob = new Blob([data], { const arrayBuffer = data?.data ?? data;
const blob = new Blob([arrayBuffer], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
}); });
saveAs(blob, `合同日志_${Date.now()}.xlsx`); saveAs(blob, `合同日志_${Date.now()}.xlsx`);
@@ -559,8 +559,9 @@ async function handleTemplateFile(file) {
} }
const downloadedBuffer = await downloadTemplateFile(uploadedFilePath) const downloadedBuffer = await downloadTemplateFile(uploadedFilePath)
const downloadedArrayBuffer = downloadedBuffer?.data ?? downloadedBuffer
const previewBuffer = const previewBuffer =
downloadedBuffer.data instanceof ArrayBuffer ? downloadedBuffer.data : downloadedBuffer.data?.buffer downloadedArrayBuffer instanceof ArrayBuffer ? downloadedArrayBuffer : downloadedArrayBuffer?.buffer
if (!previewBuffer) { if (!previewBuffer) {
message.error('模板文件已上传,但回读渲染失败') message.error('模板文件已上传,但回读渲染失败')
@@ -798,7 +799,8 @@ async function handleSubmit(status) {
async function loadTemplateBufferFromRecord(record) { async function loadTemplateBufferFromRecord(record) {
if (record.templateFilePath) { if (record.templateFilePath) {
const response = await downloadTemplateFile(record.templateFilePath) const response = await downloadTemplateFile(record.templateFilePath)
templateArrayBuffer.value = response.data instanceof ArrayBuffer ? response.data.slice(0) : response.data const arrayBuffer = response?.data ?? response
templateArrayBuffer.value = arrayBuffer instanceof ArrayBuffer ? arrayBuffer.slice(0) : arrayBuffer
setUploadList(record.templateFileName) setUploadList(record.templateFileName)
} }
} }
+64 -18
View File
@@ -487,25 +487,34 @@
</div> </div>
<div class="step-card-body"> <div class="step-card-body">
<div class="complete-form-grid"> <div class="complete-form-grid">
<div class="complete-form-item">
<div class="complete-form-label">签订日期</div>
<a-date-picker <a-date-picker
v-model:value="completeSignDate" v-model:value="completeSignDate"
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD"
style="width: 100%" style="width: 100%"
placeholder="签订日期" placeholder="请选择签订日期"
/> />
</div>
<div class="complete-form-item">
<div class="complete-form-label">用印时间</div>
<a-date-picker <a-date-picker
v-model:value="completeSealDate" v-model:value="completeSealDate"
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD"
style="width: 100%" style="width: 100%"
placeholder="用印时间" placeholder="请选择用印时间"
/> />
</div>
<div class="complete-form-item">
<div class="complete-form-label">合同生效日期</div>
<a-date-picker <a-date-picker
v-model:value="completeActiveDate" v-model:value="completeActiveDate"
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD"
style="width: 100%" style="width: 100%"
placeholder="合同生效日期" placeholder="请选择合同生效日期"
/> />
</div> </div>
</div>
<div <div
class="btn btn-success" class="btn btn-success"
@@ -762,6 +771,23 @@ function formatDateTimeValue(value) {
return value ? `${value} 00:00:00` : null; return value ? `${value} 00:00:00` : null;
} }
function normalizePdfPreviewImages(list = []) {
return (Array.isArray(list) ? list : [])
.map((item) => {
if (typeof item === "string") return item;
if (!item || typeof item !== "object") return "";
return (
item.url ||
item.imageUrl ||
item.previewUrl ||
item.fileUrl ||
item.path ||
""
);
})
.filter(Boolean);
}
function syncCompletionDates(record = {}) { function syncCompletionDates(record = {}) {
completeSignDate.value = sliceDateValue(record?.signDate) || todayString(); completeSignDate.value = sliceDateValue(record?.signDate) || todayString();
completeSealDate.value = sliceDateValue(record?.sealDate) || completeSignDate.value; completeSealDate.value = sliceDateValue(record?.sealDate) || completeSignDate.value;
@@ -895,7 +921,11 @@ async function buildBusinessBodyUploadFile(record = {}) {
if (!bodyFile.filePath) { if (!bodyFile.filePath) {
return null; return null;
} }
const buffer = await downloadLaunchFile(bodyFile.filePath); const response = await downloadLaunchFile(bodyFile.filePath);
const buffer = response?.data ?? response;
if (!buffer) {
throw new Error("合同正文下载结果为空");
}
const type = const type =
String(bodyFile.fileType || "").toLowerCase() === "pdf" String(bodyFile.fileType || "").toLowerCase() === "pdf"
? "application/pdf" ? "application/pdf"
@@ -1108,7 +1138,7 @@ async function loadContract(cid) {
contractName.value = detail?.contractName || contractName.value; contractName.value = detail?.contractName || contractName.value;
contractStatus.value = contractStatus.value =
detail?.contractStatus ?? detail?.status ?? contractStatus.value; detail?.contractStatus ?? detail?.status ?? contractStatus.value;
pdfImages.value = preview?.pdfToImageList || []; pdfImages.value = normalizePdfPreviewImages(preview?.pdfToImageList);
imageWidth.value = Number(preview?.imageWidth) || 793; imageWidth.value = Number(preview?.imageWidth) || 793;
imageHeight.value = Number(preview?.imageHeight) || 1122; imageHeight.value = Number(preview?.imageHeight) || 1122;
updateBusinessStatusByContractStatus(contractStatus.value); updateBusinessStatusByContractStatus(contractStatus.value);
@@ -1144,7 +1174,7 @@ async function handleCreateContract() {
const data = getResponseResult(res); const data = getResponseResult(res);
contractId.value = data.contractId || ""; contractId.value = data.contractId || "";
pdfImages.value = data.pdfToImageList || []; pdfImages.value = normalizePdfPreviewImages(data.pdfToImageList);
imageWidth.value = Number(data.imageWidth) || 793; imageWidth.value = Number(data.imageWidth) || 793;
imageHeight.value = Number(data.imageHeight) || 1122; imageHeight.value = Number(data.imageHeight) || 1122;
contractStatus.value = 10; contractStatus.value = 10;
@@ -1676,7 +1706,7 @@ async function confirmTimestamp(item) {
item.status = "signed"; item.status = "signed";
item._signTime = data.signatureTime || ""; item._signTime = data.signatureTime || "";
if (data.pdfToImageList?.length) { if (data.pdfToImageList?.length) {
pdfImages.value = data.pdfToImageList; pdfImages.value = normalizePdfPreviewImages(data.pdfToImageList);
} }
if (data.contractStatus !== undefined) { if (data.contractStatus !== undefined) {
contractStatus.value = data.contractStatus; contractStatus.value = data.contractStatus;
@@ -1882,9 +1912,10 @@ watch(visible, (value) => {
} }
.info-item { .info-item {
display: flex; display: grid;
justify-content: space-between; grid-template-columns: 72px minmax(0, 1fr);
align-items: center; align-items: start;
column-gap: 12px;
padding: 12px 0; padding: 12px 0;
border-bottom: 1px dashed #eef2f6; border-bottom: 1px dashed #eef2f6;
font-size: 14px; font-size: 14px;
@@ -1896,10 +1927,16 @@ watch(visible, (value) => {
.info-item span { .info-item span {
color: #64748b; color: #64748b;
white-space: nowrap;
line-height: 1.6;
} }
.info-item strong { .info-item strong {
color: #0f172a; color: #0f172a;
min-width: 0;
overflow-wrap: anywhere;
word-break: break-word;
line-height: 1.6;
} }
.business-tag-row { .business-tag-row {
@@ -1933,9 +1970,10 @@ watch(visible, (value) => {
} }
.business-meta-item { .business-meta-item {
display: flex; display: grid;
justify-content: space-between; grid-template-columns: 72px minmax(0, 1fr);
gap: 12px; align-items: start;
column-gap: 12px;
font-size: 12px; font-size: 12px;
color: #64748b; color: #64748b;
padding-bottom: 8px; padding-bottom: 8px;
@@ -1947,14 +1985,22 @@ watch(visible, (value) => {
border-bottom: none; border-bottom: none;
} }
.business-meta-item span {
white-space: nowrap;
line-height: 1.6;
}
.business-meta-item strong { .business-meta-item strong {
flex: 1; min-width: 0;
text-align: right; text-align: left;
color: #0f172a; color: #0f172a;
font-weight: 600; font-weight: 600;
white-space: nowrap; white-space: normal;
overflow: hidden; overflow: visible;
text-overflow: ellipsis; text-overflow: initial;
overflow-wrap: anywhere;
word-break: break-word;
line-height: 1.6;
} }
.upload-mode-hint { .upload-mode-hint {