单方协议

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
+410 -131
View File
@@ -10,17 +10,29 @@
<a-row :gutter="16" style="width: 100%">
<a-col :md="6" :sm="24">
<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-col>
<a-col :md="6" :sm="24">
<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-col>
<a-col :md="6" :sm="24">
<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-col>
<a-col :md="6" :sm="24">
@@ -55,10 +67,36 @@
<vxe-toolbar ref="toolbarRef" custom>
<template #buttons>
<a-space wrap>
<a-button type="primary" :icon="h(SearchOutlined)" @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 danger :icon="h(DeleteOutlined)" :disabled="!selectedRowKeys.length" @click="handleBatchDelete">删除</a-button>
<a-button
type="primary"
:icon="h(SearchOutlined)"
@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>
</template>
</vxe-toolbar>
@@ -74,33 +112,97 @@
:checkbox-config="{ range: true }"
:data="tableData"
:loading="loading"
:loading-config="{ icon: 'vxe-icon-indicator roll', text: '正在拼命加载中...' }"
:loading-config="{
icon: 'vxe-icon-indicator roll',
text: '正在拼命加载中...',
}"
@checkbox-range-change="selectChangeEvent"
@checkbox-change="selectChangeEvent"
@checkbox-all="selectChangeEvent"
>
<vxe-column type="checkbox" width="56" />
<vxe-column type="seq" title="序号" width="68" />
<vxe-column field="contractCode" 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">
<vxe-column
field="contractCode"
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 }">
{{ getContractTypeSimpleLabel(row.contractTypeCode || row.contractType) }}
{{
getContractTypeSimpleLabel(
row.contractTypeCode || row.contractType,
)
}}
</template>
</vxe-column>
<vxe-column field="templateName" title="所选模板" width="180" show-overflow="title" />
<vxe-column field="counterpartySummary" title="合同相对方" width="260" show-overflow="title" />
<vxe-column
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">
<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>
</vxe-column>
<vxe-column field="approveMessage" title="审批说明" width="220" 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
field="approveMessage"
title="审批说明"
width="220"
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">
<template #default="{ row }">
<div class="table-actions">
@@ -108,17 +210,47 @@
<a @click="handleCopy(row)">复制</a>
<a v-if="canChange(row)" @click="handleChange(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="canViewApprove(row)" @click="handleViewApprove(row)">审批记录</a>
<a v-if="canViewLawCallback(row)" @click="handleViewLawCallback(row)">回调data</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)">在线签订</a>
<a v-if="canDownloadSigned(row)" @click="handleDownloadSigned(row)">下载签署文件</a>
<a v-if="canUploadSigned(row)" @click="handleUploadSigned(row)">上传已签署版本</a>
<a v-if="canMockFinishSign(row)" @click="handleMockFinishSign(row)">测试签署完成</a>
<a v-if="canCollect(row)" @click="handleMockCollect(row)">测试采集</a>
<a v-if="canArchive(row)" @click="handleArchive(row)">归档</a>
<a v-if="canSubmit(row)" @click="handleSubmit(row)">{{
isSingleAgreementRecord(row) ? "提交审批" : "提交法务"
}}</a>
<a v-if="canViewApprove(row)" @click="handleViewApprove(row)"
>审批记录</a
>
<a
v-if="canViewLawCallback(row)"
@click="handleViewLawCallback(row)"
>回调data</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="canDelete(row)" @click="handleDelete(row)">删除</a>
</div>
@@ -141,10 +273,22 @@
/>
</a-card>
<ContractPartyPlaceholderModal ref="contractModalRef" @save="handleModalSave" />
<MixedSignModal ref="mixedSignModalRef" @business-updated="handleSignBusinessUpdated" />
<UploadSignedModal ref="uploadSignedModalRef" @done="handleSignBusinessUpdated" />
<ContractArchiveModal ref="contractArchiveModalRef" @done="handleArchiveDone" />
<ContractPartyPlaceholderModal
ref="contractModalRef"
@save="handleModalSave"
/>
<MixedSignModal
ref="mixedSignModalRef"
@business-updated="handleSignBusinessUpdated"
/>
<UploadSignedModal
ref="uploadSignedModalRef"
@done="handleSignBusinessUpdated"
/>
<ContractArchiveModal
ref="contractArchiveModalRef"
@done="handleArchiveDone"
/>
</section>
</template>
@@ -302,6 +446,13 @@ function getStatusColor(status) {
return statusColorMap[status] || "default";
}
function getStatusLabelByRow(row) {
if (isSingleAgreementRecord(row) && row?.status === "pending_sign") {
return "待盖章";
}
return getStatusLabel(row?.status);
}
function canEdit(row) {
return ["draft", "rejected"].includes(row.status);
}
@@ -316,7 +467,9 @@ function canSubmit(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) {
@@ -329,11 +482,21 @@ function canMockReject(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) {
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) {
@@ -341,11 +504,11 @@ function canUploadSigned(row) {
}
function canCollect(row) {
return row.status === "pending_collect";
return !isSingleAgreementRecord(row) && row.status === "pending_collect";
}
function canArchive(row) {
return row.status === "pending_archive";
return !isSingleAgreementRecord(row) && row.status === "pending_archive";
}
function isSingleAgreementRecord(row) {
@@ -353,14 +516,34 @@ function isSingleAgreementRecord(row) {
}
function canViewApprove(row) {
return !isSingleAgreementRecord(row)
&& Boolean(row.lawContractId)
&& ["reviewing", "rejected", "pending_sign", "signing", "pending_collect", "pending_archive", "completed"].includes(row.status);
return (
!isSingleAgreementRecord(row) &&
Boolean(row.lawContractId) &&
[
"reviewing",
"rejected",
"pending_sign",
"signing",
"pending_collect",
"pending_archive",
"completed",
].includes(row.status)
);
}
function canViewLawCallback(row) {
return !isSingleAgreementRecord(row)
&& ["reviewing", "rejected", "pending_sign", "signing", "pending_collect", "pending_archive", "completed"].includes(row.status);
return (
!isSingleAgreementRecord(row) &&
[
"reviewing",
"rejected",
"pending_sign",
"signing",
"pending_collect",
"pending_archive",
"completed",
].includes(row.status)
);
}
function canDownloadSigned(row) {
@@ -441,6 +624,10 @@ function handleCreate() {
contractModalRef.value?.showModal({}, "create");
}
function handleCreateSingleAgreement() {
contractModalRef.value?.showModal({ isSingleAgreement: "1" }, "create");
}
function handleView(row) {
getLaunchDetail(row.id).then((res) => {
if (!isSuccessResponse(res) || !getDataResult(res)) {
@@ -511,20 +698,34 @@ function handleSubmit(row) {
Modal.confirm({
title: singleAgreement ? "确认提交内部审批" : "确认提交法务审批",
content: h("div", [
h("p", singleAgreement
? `确定将单方协议【${row.contractName}】提交内部审批吗?`
: `确定将合同【${row.contractName}】提交法务系统审批吗?`),
h("p", { style: { color: "#999", fontSize: "12px" } }, "提交后合同进入审批中状态审批完成前不可编辑"),
h(
"p",
singleAgreement
? `确定将单方协议【${row.contractName}】提交内部审批吗?`
: `确定将合同【${row.contractName}】提交法务系统审批吗?`,
),
h(
"p",
{ style: { color: "#999", fontSize: "12px" } },
"提交后合同进入审批中状态审批完成前不可编辑",
),
]),
okText: "确认提交",
cancelText: "取消",
onOk: () =>
submitLaunchRecord(row.id).then((res) => {
if (!isSuccessResponse(res)) {
message.error(getResponseMessage(res, singleAgreement ? "提交内部审批失败" : "提交法务失败"));
message.error(
getResponseMessage(
res,
singleAgreement ? "提交内部审批失败" : "提交法务失败",
),
);
return;
}
message.success(singleAgreement ? "已提交内部审批等待审批" : "已提交法务等待审批");
message.success(
singleAgreement ? "已提交内部审批等待审批" : "已提交法务等待审批",
);
getList();
}),
icon: null,
@@ -555,48 +756,84 @@ function handleViewApprove(row) {
}
function buildLawCallbackModalContent(sample) {
const approvedJson = JSON.stringify({ data: sample?.approvedData || {} }, 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", { 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", { style: { marginBottom: "6px", fontWeight: 600 } }, "审批通过 data"),
h("pre", {
style: {
margin: 0,
maxHeight: "260px",
overflow: "auto",
padding: "12px",
borderRadius: "8px",
background: "#f6f8fa",
fontSize: "12px",
lineHeight: "1.6",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
},
}, approvedJson),
]),
h("div", [
h("div", { style: { marginBottom: "6px", fontWeight: 600 } }, "审批驳回 data"),
h("pre", {
style: {
margin: 0,
maxHeight: "220px",
overflow: "auto",
padding: "12px",
borderRadius: "8px",
background: "#f6f8fa",
fontSize: "12px",
lineHeight: "1.6",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
},
}, rejectedJson),
]),
]);
const approvedJson = JSON.stringify(
{ data: sample?.approvedData || {} },
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",
{ 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",
{ style: { marginBottom: "6px", fontWeight: 600 } },
"审批通过 data",
),
h(
"pre",
{
style: {
margin: 0,
maxHeight: "260px",
overflow: "auto",
padding: "12px",
borderRadius: "8px",
background: "#f6f8fa",
fontSize: "12px",
lineHeight: "1.6",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
},
},
approvedJson,
),
]),
h("div", [
h(
"div",
{ style: { marginBottom: "6px", fontWeight: 600 } },
"审批驳回 data",
),
h(
"pre",
{
style: {
margin: 0,
maxHeight: "220px",
overflow: "auto",
padding: "12px",
borderRadius: "8px",
background: "#f6f8fa",
fontSize: "12px",
lineHeight: "1.6",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
},
},
rejectedJson,
),
]),
],
);
}
function handleViewLawCallback(row) {
@@ -618,7 +855,9 @@ function handleViewLawCallback(row) {
function handleMockApprove(row) {
const singleAgreement = isSingleAgreementRecord(row);
Modal.confirm({
title: singleAgreement ? "测试操作模拟内部审批通过" : "测试操作模拟审批通过",
title: singleAgreement
? "测试操作模拟内部审批通过"
: "测试操作模拟审批通过",
content: singleAgreement
? `模拟单方协议【${row.contractName}】内部审批通过,状态将变为「待签署」。`
: `模拟法务审批通过合同【${row.contractName}】,状态将变为「待签署」。`,
@@ -640,12 +879,21 @@ function handleMockApprove(row) {
function handleMockReject(row) {
const singleAgreement = isSingleAgreementRecord(row);
Modal.confirm({
title: singleAgreement ? "测试操作模拟内部审批驳回" : "测试操作模拟审批驳回",
title: singleAgreement
? "测试操作模拟内部审批驳回"
: "测试操作模拟审批驳回",
content: h("div", [
h("p", singleAgreement
? `模拟单方协议【${row.contractName}】内部审批驳回,状态将回到「已驳回」。`
: `模拟法务审批驳回合同【${row.contractName}】,状态将回到「已驳回」。`),
h("p", { style: { color: "#999", fontSize: "12px" } }, "驳回后可重新编辑再提交"),
h(
"p",
singleAgreement
? `模拟单方协议【${row.contractName}】内部审批驳回,状态将回到「已驳回」。`
: `模拟法务审批驳回合同【${row.contractName}】,状态将回到「已驳回」。`,
),
h(
"p",
{ style: { color: "#999", fontSize: "12px" } },
"驳回后可重新编辑再提交",
),
]),
okText: "确认驳回",
cancelText: "取消",
@@ -663,15 +911,23 @@ function handleMockReject(row) {
}
function handleMockFinishSign(row) {
const singleAgreement = isSingleAgreementRecord(row);
Modal.confirm({
title: "测试操作模拟签署完成",
content: `模拟合同【${row.contractName}】签署完成,状态将变为「待采集」。`,
title: singleAgreement
? "测试操作模拟盖章完成"
: "测试操作模拟签署完成",
content: singleAgreement
? `模拟单方协议【${row.contractName}】内部业务章加盖完成,状态将变为「已完成」。`
: `模拟合同【${row.contractName}】签署完成,状态将变为「待采集」。`,
okText: "确认模拟",
cancelText: "取消",
onOk: () => {
const signDate = sliceDateValue(row.signDate) || todayString();
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, {
sealContractId: row.sealContractId || `TEST-SEAL-${row.id}`,
sealContractStatus: 2000,
@@ -679,16 +935,27 @@ function handleMockFinishSign(row) {
signedFilePath: row.contractTextFilePath || `mock-signed/${row.id}.pdf`,
signedFileType: "pdf",
signedFileSize: row.contractTextFileSize || null,
signedOfflineReason: "测试按钮模拟签署完成真实签章接入后可删除此按钮",
signedOfflineReason: singleAgreement
? "测试按钮模拟内部业务章加盖完成"
: "测试按钮模拟签署完成真实签章接入后可删除此按钮",
signDate: formatDateTimeValue(signDate),
sealDate: formatDateTimeValue(sealDate),
activeDate: formatDateTimeValue(activeDate),
}).then((res) => {
if (!isSuccessResponse(res)) {
message.error(getResponseMessage(res, "测试签署完成失败"));
message.error(
getResponseMessage(
res,
singleAgreement ? "测试盖章完成失败" : "测试签署完成失败",
),
);
return;
}
message.success("测试签署完成合同已流转到待采集");
message.success(
singleAgreement
? "测试盖章完成单方协议已办结"
: "测试签署完成合同已流转到待采集",
);
getList();
});
},
@@ -698,24 +965,28 @@ function handleMockFinishSign(row) {
function handleDownloadSigned(row) {
const filePath = row.signedFilePath;
const fileName = row.signedFileName || `${row.contractName || "合同"}-已签署版.pdf`;
const fileName =
row.signedFileName || `${row.contractName || "合同"}-已签署版.pdf`;
if (!filePath) {
message.warning("暂无签署文件可下载");
return;
}
downloadLaunchFile(filePath).then((res) => {
const blob = new Blob([res.data]);
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
}).catch(() => {
message.error("签署文件下载失败");
});
downloadLaunchFile(filePath)
.then((res) => {
const arrayBuffer = res?.data ?? res;
const blob = new Blob([arrayBuffer]);
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
})
.catch(() => {
message.error("签署文件下载失败");
});
}
function handleUploadSigned(row) {
@@ -741,7 +1012,8 @@ function handleSignBusinessUpdated() {
}
function handleModalSave(payload) {
const requestFn = payload.mode === "edit" ? updateLaunchDraft : createLaunchDraft;
const requestFn =
payload.mode === "edit" ? updateLaunchDraft : createLaunchDraft;
requestFn(payload.record).then((res) => {
if (!isSuccessResponse(res)) {
message.error(getResponseMessage(res, "保存合同失败"));
@@ -792,16 +1064,23 @@ function handleTerminate(row) {
let terminateReason = "";
Modal.confirm({
title: `确认终止合同【${row.contractName}】吗?`,
content: h("div", { style: { display: "flex", flexDirection: "column", gap: "12px" } }, [
h("div", "除已完成外任意环节都可终止当前仅终止业务系统内流程法务系统需分别操作"),
h(Input.TextArea, {
rows: 4,
placeholder: "可选请输入终止原因",
onChange: (event) => {
terminateReason = event?.target?.value || "";
},
}),
]),
content: h(
"div",
{ style: { display: "flex", flexDirection: "column", gap: "12px" } },
[
h(
"div",
"除已完成外任意环节都可终止当前仅终止业务系统内流程法务系统需分别操作",
),
h(Input.TextArea, {
rows: 4,
placeholder: "可选请输入终止原因",
onChange: (event) => {
terminateReason = event?.target?.value || "";
},
}),
],
),
okText: "确认终止",
cancelText: "取消",
okType: "danger",