1256 lines
36 KiB
Vue
1256 lines
36 KiB
Vue
<template>
|
||
<section class="section-container">
|
||
<a-card
|
||
:bordered="false"
|
||
class="query-card"
|
||
:body-style="queryCardBodyStyle"
|
||
>
|
||
<div ref="queryContainer" class="section-query-container">
|
||
<a-form ref="formRef" :model="queryParam" layout="inline">
|
||
<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-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-form-item>
|
||
</a-col>
|
||
<a-col :md="6" :sm="24">
|
||
<a-form-item label="合同类型">
|
||
<a-select
|
||
v-model:value="queryParam.contractType"
|
||
allow-clear
|
||
placeholder="请选择合同类型"
|
||
:options="contractTypeOptions"
|
||
/>
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :md="6" :sm="24">
|
||
<a-form-item label="当前状态">
|
||
<a-select
|
||
v-model:value="queryParam.status"
|
||
allow-clear
|
||
placeholder="请选择状态"
|
||
:options="contractRecordStatusOptions"
|
||
/>
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :md="12" :sm="24">
|
||
<a-form-item label="发起日期">
|
||
<a-range-picker
|
||
v-model:value="queryParam.dateRange"
|
||
value-format="YYYY-MM-DD"
|
||
style="width: 100%"
|
||
/>
|
||
</a-form-item>
|
||
</a-col>
|
||
</a-row>
|
||
</a-form>
|
||
</div>
|
||
</a-card>
|
||
|
||
<a-card
|
||
:bordered="false"
|
||
class="table-card"
|
||
:body-style="tableCardBodyStyle"
|
||
>
|
||
<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
|
||
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>
|
||
|
||
<vxe-table
|
||
ref="tableRef"
|
||
border
|
||
:height="tableHeight"
|
||
:row-config="{ isHover: true, height: 44 }"
|
||
:column-config="{ resizable: true, minWidth: 120 }"
|
||
:custom-config="{ storage: true }"
|
||
:scroll-y="{ enabled: true, gt: 0, mode: 'wheel' }"
|
||
:checkbox-config="{ range: true }"
|
||
:data="tableData"
|
||
:loading="loading"
|
||
: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"
|
||
>
|
||
<template #default="{ row }">
|
||
{{
|
||
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="status" title="状态" width="120">
|
||
<template #default="{ row }">
|
||
<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 title="操作" width="720" fixed="right">
|
||
<template #default="{ row }">
|
||
<div class="table-actions">
|
||
<a @click="handleView(row)">查看</a>
|
||
<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)">{{
|
||
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>
|
||
</template>
|
||
</vxe-column>
|
||
</vxe-table>
|
||
|
||
<a-pagination
|
||
id="pageWrapper"
|
||
ref="pageWrapperRef"
|
||
v-model:pageSize="queryParam.pageSize"
|
||
v-model:current="queryParam.pageNo"
|
||
class="table-pagination"
|
||
show-size-changer
|
||
:page-size-options="['10', '20', '50', '100']"
|
||
:total="total"
|
||
:show-total="(value) => `共 ${value} 条`"
|
||
@change="onShowSizeChange"
|
||
@show-size-change="onShowSizeChange"
|
||
/>
|
||
</a-card>
|
||
|
||
<ContractPartyPlaceholderModal
|
||
ref="contractModalRef"
|
||
@save="handleModalSave"
|
||
/>
|
||
<ContractCollectModal
|
||
ref="contractCollectModalRef"
|
||
@done="handleCollectDone"
|
||
/>
|
||
<MixedSignModal
|
||
ref="mixedSignModalRef"
|
||
@business-updated="handleSignBusinessUpdated"
|
||
/>
|
||
<UploadSignedModal
|
||
ref="uploadSignedModalRef"
|
||
@done="handleSignBusinessUpdated"
|
||
/>
|
||
<ContractArchiveModal
|
||
ref="contractArchiveModalRef"
|
||
@done="handleArchiveDone"
|
||
/>
|
||
</section>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { h, nextTick, onBeforeUnmount, onMounted, ref } from "vue";
|
||
import {
|
||
DeleteOutlined,
|
||
PlusOutlined,
|
||
RedoOutlined,
|
||
SearchOutlined,
|
||
} from "@ant-design/icons-vue";
|
||
import { Input, message, Modal } from "ant-design-vue";
|
||
import ContractPartyPlaceholderModal from "./components/ContractPartyPlaceholderModal.vue";
|
||
import ContractCollectModal from "./components/ContractCollectModal.vue";
|
||
import MixedSignModal from "./components/mixedSignModal.vue";
|
||
import UploadSignedModal from "./components/UploadSignedModal.vue";
|
||
import ContractArchiveModal from "./components/ContractArchiveModal.vue";
|
||
import {
|
||
contractTypeOptions,
|
||
getContractTypeSimpleLabel,
|
||
} from "./contractLaunchOptions.js";
|
||
import {
|
||
copyLaunchDraft,
|
||
createLaunchChangeDraft,
|
||
createLaunchDraft,
|
||
deleteLaunchRecord,
|
||
getLaunchDetail,
|
||
getLawApproveCallbackSample,
|
||
mockApproveLaunch,
|
||
mockRejectLaunch,
|
||
finishLaunchSeal,
|
||
queryApproveViewUrl,
|
||
queryLaunchList,
|
||
submitLaunchRecord,
|
||
downloadLaunchFile,
|
||
terminateLaunchRecord,
|
||
updateLaunchDraft,
|
||
} from "@/api/launch.js";
|
||
|
||
const contractRecordStatusOptions = [
|
||
{ label: "草稿", value: "draft" },
|
||
{ label: "审批中", value: "reviewing" },
|
||
{ label: "审批驳回", value: "rejected" },
|
||
{ label: "待签署", value: "pending_sign" },
|
||
{ label: "签署中", value: "signing" },
|
||
{ label: "待采集", value: "pending_collect" },
|
||
{ label: "待归档", value: "pending_archive" },
|
||
{ label: "已完成", value: "completed" },
|
||
{ label: "已终止", value: "terminated" },
|
||
];
|
||
|
||
const formRef = ref(null);
|
||
const queryContainer = ref(null);
|
||
const pageWrapperRef = ref(null);
|
||
const tableRef = ref(null);
|
||
const toolbarRef = ref(null);
|
||
const contractModalRef = ref(null);
|
||
const contractCollectModalRef = ref(null);
|
||
const mixedSignModalRef = ref(null);
|
||
const uploadSignedModalRef = ref(null);
|
||
const contractArchiveModalRef = ref(null);
|
||
|
||
const total = ref(0);
|
||
const loading = ref(false);
|
||
const tableHeight = ref(420);
|
||
const tableData = ref([]);
|
||
const selectedRows = ref([]);
|
||
const selectedRowKeys = ref([]);
|
||
|
||
const queryParam = ref({
|
||
pageNo: 1,
|
||
pageSize: 10,
|
||
contractName: undefined,
|
||
contractCode: undefined,
|
||
contractType: undefined,
|
||
status: undefined,
|
||
dateRange: [],
|
||
});
|
||
|
||
const queryCardBodyStyle = {
|
||
padding: "20px 24px 6px",
|
||
};
|
||
|
||
const tableCardBodyStyle = {
|
||
display: "flex",
|
||
flexFlow: "column nowrap",
|
||
minHeight: "0",
|
||
padding: "8px 12px 6px",
|
||
};
|
||
|
||
const statusLabelMap = {
|
||
draft: "草稿",
|
||
reviewing: "审批中",
|
||
rejected: "审批驳回",
|
||
pending_sign: "待签署",
|
||
signing: "签署中",
|
||
pending_collect: "待采集",
|
||
pending_archive: "待归档",
|
||
completed: "已完成",
|
||
terminated: "已终止",
|
||
};
|
||
|
||
const statusColorMap = {
|
||
draft: "default",
|
||
reviewing: "processing",
|
||
rejected: "error",
|
||
pending_sign: "warning",
|
||
signing: "purple",
|
||
pending_collect: "cyan",
|
||
pending_archive: "orange",
|
||
completed: "success",
|
||
terminated: "default",
|
||
};
|
||
|
||
function isSuccessResponse(res) {
|
||
return String(res?.code || "") === "0000";
|
||
}
|
||
|
||
function getPageResult(res) {
|
||
return res?.result || {};
|
||
}
|
||
|
||
function getDataResult(res) {
|
||
return res?.result || null;
|
||
}
|
||
|
||
function getResponseMessage(res, fallback = "操作失败") {
|
||
return res?.message || res?.msg || fallback;
|
||
}
|
||
|
||
function sliceDateValue(value) {
|
||
return value ? String(value).slice(0, 10) : "";
|
||
}
|
||
|
||
function todayString() {
|
||
const now = new Date();
|
||
const month = String(now.getMonth() + 1).padStart(2, "0");
|
||
const day = String(now.getDate()).padStart(2, "0");
|
||
return `${now.getFullYear()}-${month}-${day}`;
|
||
}
|
||
|
||
function formatDateTimeValue(value) {
|
||
return value ? `${value} 00:00:00` : null;
|
||
}
|
||
|
||
function connectToolbar() {
|
||
const table = tableRef.value;
|
||
const toolbar = toolbarRef.value;
|
||
if (table && toolbar && table.connect) {
|
||
table.connect(toolbar);
|
||
}
|
||
}
|
||
|
||
function getStatusLabel(status) {
|
||
return statusLabelMap[status] || status;
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
function canDelete(row) {
|
||
return ["draft", "rejected"].includes(row.status);
|
||
}
|
||
|
||
function canSubmit(row) {
|
||
return ["draft", "rejected"].includes(row.status);
|
||
}
|
||
|
||
function canChange(row) {
|
||
// 变更/补充协议:仅已完成/待采集/待归档的合同允许基于它发起变更
|
||
return ["completed", "pending_archive", "pending_collect"].includes(
|
||
row.status,
|
||
);
|
||
}
|
||
|
||
function canMockApprove(row) {
|
||
return row.status === "reviewing";
|
||
}
|
||
|
||
function canMockReject(row) {
|
||
return row.status === "reviewing";
|
||
}
|
||
|
||
function canOnlineSign(row) {
|
||
// 在线签订只对电子合同开放;纸质/线下签署走“上传已签署版本”兜底。
|
||
return (
|
||
!isSingleAgreementRecord(row) &&
|
||
String(row.isElectron || "1") === "1" &&
|
||
["pending_sign", "signing"].includes(row.status)
|
||
);
|
||
}
|
||
|
||
function canMockFinishSign(row) {
|
||
if (isSingleAgreementRecord(row)) {
|
||
return row.status === "pending_sign";
|
||
}
|
||
return (
|
||
String(row.isElectron || "1") === "1" &&
|
||
["pending_sign", "signing"].includes(row.status)
|
||
);
|
||
}
|
||
|
||
function canUploadSigned(row) {
|
||
// 纸质合同(0)才显示上传已签署版本;电子合同走在线签订
|
||
return String(row.isElectron || "1") === "0" && ["pending_sign", "signing"].includes(row.status);
|
||
}
|
||
|
||
function canCollect(row) {
|
||
return !isSingleAgreementRecord(row) && row.status === "pending_collect";
|
||
}
|
||
|
||
function canArchive(row) {
|
||
return !isSingleAgreementRecord(row) && row.status === "pending_archive";
|
||
}
|
||
|
||
function isSingleAgreementRecord(row) {
|
||
return String(row?.isSingleAgreement || "0") === "1";
|
||
}
|
||
|
||
function canViewApprove(row) {
|
||
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)
|
||
);
|
||
}
|
||
|
||
function canDownloadSigned(row) {
|
||
return Boolean(row.signedFilePath);
|
||
}
|
||
|
||
function canTerminate(row) {
|
||
return row.status !== "completed" && row.status !== "terminated";
|
||
}
|
||
|
||
function selectChangeEvent({ records }) {
|
||
selectedRows.value = records;
|
||
selectedRowKeys.value = records.map((item) => item.id);
|
||
}
|
||
|
||
function buildQueryPayload() {
|
||
return {
|
||
pageNo: queryParam.value.pageNo,
|
||
pageSize: queryParam.value.pageSize,
|
||
contractName: queryParam.value.contractName,
|
||
contractCode: queryParam.value.contractCode,
|
||
contractType: queryParam.value.contractType,
|
||
status: queryParam.value.status,
|
||
dateStart: queryParam.value.dateRange?.[0],
|
||
dateEnd: queryParam.value.dateRange?.[1],
|
||
};
|
||
}
|
||
|
||
function getList() {
|
||
loading.value = true;
|
||
queryLaunchList(buildQueryPayload())
|
||
.then((res) => {
|
||
if (!isSuccessResponse(res)) {
|
||
tableData.value = [];
|
||
total.value = 0;
|
||
message.error(getResponseMessage(res, "合同发起列表查询失败"));
|
||
return;
|
||
}
|
||
const pageResult = getPageResult(res);
|
||
tableData.value = pageResult.data || [];
|
||
total.value = pageResult.totalSize || 0;
|
||
})
|
||
.finally(() => {
|
||
loading.value = false;
|
||
nextTick(() => {
|
||
connectToolbar();
|
||
syncTableHeight();
|
||
});
|
||
});
|
||
}
|
||
|
||
function handleQuery() {
|
||
queryParam.value.pageNo = 1;
|
||
getList();
|
||
}
|
||
|
||
function handleReset() {
|
||
formRef.value?.resetFields?.();
|
||
Object.assign(queryParam.value, {
|
||
pageNo: 1,
|
||
pageSize: 10,
|
||
contractName: undefined,
|
||
contractCode: undefined,
|
||
contractType: undefined,
|
||
status: undefined,
|
||
dateRange: [],
|
||
});
|
||
getList();
|
||
}
|
||
|
||
function onShowSizeChange(current, pageSize) {
|
||
queryParam.value.pageNo = current;
|
||
queryParam.value.pageSize = pageSize;
|
||
getList();
|
||
}
|
||
|
||
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)) {
|
||
message.error(getResponseMessage(res, "获取合同详情失败"));
|
||
return;
|
||
}
|
||
contractModalRef.value?.showModal(getDataResult(res), "view");
|
||
});
|
||
}
|
||
|
||
function handleEdit(row) {
|
||
getLaunchDetail(row.id).then((res) => {
|
||
if (!isSuccessResponse(res) || !getDataResult(res)) {
|
||
message.error(getResponseMessage(res, "获取合同详情失败"));
|
||
return;
|
||
}
|
||
contractModalRef.value?.showModal(getDataResult(res), "edit");
|
||
});
|
||
}
|
||
|
||
function handleCopy(row) {
|
||
Modal.confirm({
|
||
title: "确认复制合同",
|
||
content: `将复制合同【${row.contractName}】生成一份新草稿,复制后自动进入编辑状态。`,
|
||
okText: "确认复制",
|
||
cancelText: "取消",
|
||
onOk: () =>
|
||
copyLaunchDraft(row.id).then((res) => {
|
||
if (!isSuccessResponse(res) || !getDataResult(res)) {
|
||
message.error(getResponseMessage(res, "复制合同失败"));
|
||
return;
|
||
}
|
||
const copiedRecord = getDataResult(res);
|
||
message.success("合同已复制为新草稿,请继续完善后保存");
|
||
getList();
|
||
contractModalRef.value?.showModal(copiedRecord, "edit");
|
||
}),
|
||
icon: null,
|
||
});
|
||
}
|
||
|
||
function handleChange(row) {
|
||
Modal.confirm({
|
||
title: "确认发起变更合同",
|
||
content: h("div", [
|
||
h("p", `将基于【${row.contractName}】发起一份变更/补充协议,`),
|
||
h("p", "自动关联主合同编码,生成新草稿。原合同不受影响。"),
|
||
]),
|
||
okText: "确认变更",
|
||
cancelText: "取消",
|
||
onOk: () =>
|
||
createLaunchChangeDraft(row.id).then((res) => {
|
||
if (!isSuccessResponse(res) || !getDataResult(res)) {
|
||
message.error(getResponseMessage(res, "发起变更合同失败"));
|
||
return;
|
||
}
|
||
const changedRecord = getDataResult(res);
|
||
message.success("已生成变更草稿,并自动关联主合同编码");
|
||
getList();
|
||
contractModalRef.value?.showModal(changedRecord, "edit");
|
||
}),
|
||
icon: null,
|
||
});
|
||
}
|
||
|
||
function handleSubmit(row) {
|
||
const singleAgreement = isSingleAgreementRecord(row);
|
||
// 非单方协议:提交法务前校验必填项完整性
|
||
if (!singleAgreement) {
|
||
return getLaunchDetail(row.id).then((res) => {
|
||
if (!isSuccessResponse(res) || !getDataResult(res)) {
|
||
message.error(getResponseMessage(res, "获取合同详情失败"));
|
||
return;
|
||
}
|
||
const detail = getDataResult(res);
|
||
const missingFields = [];
|
||
if (!detail.contractTypeCode) missingFields.push("合同类型");
|
||
if (!detail.valuationMode) missingFields.push("计价方式");
|
||
if (!detail.currencyName) missingFields.push("币种");
|
||
if (!detail.sealType) missingFields.push("签订方式");
|
||
if (!detail.isElectron) missingFields.push("签署方式");
|
||
if (!detail.paymentDirection) missingFields.push("收支方向");
|
||
if (!detail.textSource) missingFields.push("文本来源");
|
||
if (!detail.contractSource) missingFields.push("合同来源");
|
||
if (!detail.executorAccount) missingFields.push("合同执行人");
|
||
if (detail.isMajorContract === null || detail.isMajorContract === undefined) missingFields.push("是否重大合同");
|
||
if (detail.isOpenBidding === null || detail.isOpenBidding === undefined) missingFields.push("是否公开招标");
|
||
if (detail.isSgat === null || detail.isSgat === undefined) missingFields.push("是否涉及港澳台");
|
||
if (!detail.performancePlans || !detail.performancePlans.length) missingFields.push("履行计划");
|
||
if (!detail.contractTextFileName) missingFields.push("合同正文");
|
||
if (missingFields.length) {
|
||
message.warning(`以下必填项不完整,请先在编辑草稿中补全后再提交:\n${missingFields.join("、")}`);
|
||
return;
|
||
}
|
||
showSubmitConfirm(row, detail, false);
|
||
});
|
||
}
|
||
// 单方协议:直接提交内部审批
|
||
showSubmitConfirm(row, null, true);
|
||
}
|
||
|
||
function showSubmitConfirm(row, detail, singleAgreement) {
|
||
Modal.confirm({
|
||
title: singleAgreement ? "确认提交内部审批" : "确认提交法务审批",
|
||
content: h("div", [
|
||
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 ? "提交内部审批失败" : "提交法务失败",
|
||
),
|
||
);
|
||
return;
|
||
}
|
||
message.success(
|
||
singleAgreement ? "已提交内部审批,等待审批" : "已提交法务,等待审批",
|
||
);
|
||
getList();
|
||
}),
|
||
icon: null,
|
||
});
|
||
}
|
||
|
||
function handleViewApprove(row) {
|
||
if (!row.lawContractId) {
|
||
message.warning("当前暂无法务合同ID,审批记录地址尚未生成");
|
||
return;
|
||
}
|
||
|
||
queryApproveViewUrl({
|
||
contractId: String(row.lawContractId),
|
||
ivUser: row.initiator || "admin",
|
||
}).then((res) => {
|
||
if (!isSuccessResponse(res)) {
|
||
message.error(getResponseMessage(res, "获取审批记录查看地址失败"));
|
||
return;
|
||
}
|
||
const approveViewUrl = res?.result?.approveViewUrl;
|
||
if (!approveViewUrl) {
|
||
message.warning("审批记录地址为空");
|
||
return;
|
||
}
|
||
window.open(approveViewUrl, "_blank");
|
||
});
|
||
}
|
||
|
||
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,
|
||
),
|
||
]),
|
||
],
|
||
);
|
||
}
|
||
|
||
function handleViewLawCallback(row) {
|
||
getLawApproveCallbackSample(row.id).then((res) => {
|
||
if (!isSuccessResponse(res) || !getDataResult(res)) {
|
||
message.error(getResponseMessage(res, "获取法务回调样例失败"));
|
||
return;
|
||
}
|
||
const sample = getDataResult(res);
|
||
Modal.info({
|
||
title: `法务回调 data 结构:${row.contractName}`,
|
||
width: 900,
|
||
okText: "知道了",
|
||
content: buildLawCallbackModalContent(sample),
|
||
});
|
||
});
|
||
}
|
||
|
||
function handleMockApprove(row) {
|
||
const singleAgreement = isSingleAgreementRecord(row);
|
||
Modal.confirm({
|
||
title: singleAgreement
|
||
? "测试操作:模拟内部审批通过"
|
||
: "测试操作:模拟审批通过",
|
||
content: singleAgreement
|
||
? `模拟单方协议【${row.contractName}】内部审批通过,状态将变为「待签署」。`
|
||
: `模拟法务审批通过合同【${row.contractName}】,状态将变为「待签署」。`,
|
||
okText: "确认模拟",
|
||
cancelText: "取消",
|
||
onOk: () =>
|
||
mockApproveLaunch(row.id, {}).then((res) => {
|
||
if (!isSuccessResponse(res)) {
|
||
message.error(getResponseMessage(res, "模拟审批通过失败"));
|
||
return;
|
||
}
|
||
message.success("测试审批通过完成,合同已流转到待签署");
|
||
getList();
|
||
}),
|
||
icon: null,
|
||
});
|
||
}
|
||
|
||
function handleMockReject(row) {
|
||
const singleAgreement = isSingleAgreementRecord(row);
|
||
Modal.confirm({
|
||
title: singleAgreement
|
||
? "测试操作:模拟内部审批驳回"
|
||
: "测试操作:模拟审批驳回",
|
||
content: h("div", [
|
||
h(
|
||
"p",
|
||
singleAgreement
|
||
? `模拟单方协议【${row.contractName}】内部审批驳回,状态将回到「已驳回」。`
|
||
: `模拟法务审批驳回合同【${row.contractName}】,状态将回到「已驳回」。`,
|
||
),
|
||
h(
|
||
"p",
|
||
{ style: { color: "#999", fontSize: "12px" } },
|
||
"驳回后可重新编辑再提交。",
|
||
),
|
||
]),
|
||
okText: "确认驳回",
|
||
cancelText: "取消",
|
||
onOk: () =>
|
||
mockRejectLaunch(row.id, {}).then((res) => {
|
||
if (!isSuccessResponse(res)) {
|
||
message.error(getResponseMessage(res, "模拟审批驳回失败"));
|
||
return;
|
||
}
|
||
message.success("测试审批驳回完成,合同已流转到审批驳回");
|
||
getList();
|
||
}),
|
||
icon: null,
|
||
});
|
||
}
|
||
|
||
function handleMockFinishSign(row) {
|
||
const singleAgreement = isSingleAgreementRecord(row);
|
||
Modal.confirm({
|
||
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;
|
||
return finishLaunchSeal(row.id, {
|
||
sealContractId: row.sealContractId || `TEST-SEAL-${row.id}`,
|
||
sealContractStatus: 2000,
|
||
signedFileName: `${row.contractName || "合同"}-测试已签署.pdf`,
|
||
signedFilePath: row.contractTextFilePath || `mock-signed/${row.id}.pdf`,
|
||
signedFileType: "pdf",
|
||
signedFileSize: row.contractTextFileSize || null,
|
||
signedOfflineReason: singleAgreement
|
||
? "测试按钮模拟内部业务章加盖完成"
|
||
: "测试按钮模拟签署完成,真实签章接入后可删除此按钮",
|
||
signDate: formatDateTimeValue(signDate),
|
||
sealDate: formatDateTimeValue(sealDate),
|
||
activeDate: formatDateTimeValue(activeDate),
|
||
}).then((res) => {
|
||
if (!isSuccessResponse(res)) {
|
||
message.error(
|
||
getResponseMessage(
|
||
res,
|
||
singleAgreement ? "测试盖章完成失败" : "测试签署完成失败",
|
||
),
|
||
);
|
||
return;
|
||
}
|
||
message.success(
|
||
singleAgreement
|
||
? "测试盖章完成,单方协议已办结"
|
||
: "测试签署完成,合同已流转到待采集",
|
||
);
|
||
getList();
|
||
});
|
||
},
|
||
icon: null,
|
||
});
|
||
}
|
||
|
||
function handleDownloadSigned(row) {
|
||
const filePath = row.signedFilePath;
|
||
const fileName =
|
||
row.signedFileName || `${row.contractName || "合同"}-已签署版.pdf`;
|
||
if (!filePath) {
|
||
message.warning("暂无签署文件可下载");
|
||
return;
|
||
}
|
||
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) {
|
||
uploadSignedModalRef.value?.showModal(row);
|
||
}
|
||
|
||
function handleOnlineSign(row) {
|
||
getLaunchDetail(row.id).then((res) => {
|
||
if (!isSuccessResponse(res) || !getDataResult(res)) {
|
||
message.error(getResponseMessage(res, "获取合同详情失败"));
|
||
return;
|
||
}
|
||
const detail = getDataResult(res);
|
||
mixedSignModalRef.value?.showModal({
|
||
...detail,
|
||
contractId: detail.sealContractId,
|
||
});
|
||
});
|
||
}
|
||
|
||
function handleSignBusinessUpdated() {
|
||
getList();
|
||
}
|
||
|
||
function handleModalSave(payload) {
|
||
const requestFn =
|
||
payload.mode === "edit" ? updateLaunchDraft : createLaunchDraft;
|
||
requestFn(payload.record).then((res) => {
|
||
if (!isSuccessResponse(res)) {
|
||
message.error(getResponseMessage(res, "保存合同失败"));
|
||
return;
|
||
}
|
||
message.success(
|
||
payload.successMessage ||
|
||
(payload.mode === "edit" ? "合同修改成功" : "合同草稿保存成功"),
|
||
);
|
||
getList();
|
||
});
|
||
}
|
||
|
||
function handleMockCollect(row) {
|
||
getLaunchDetail(row.id).then((res) => {
|
||
if (!isSuccessResponse(res) || !getDataResult(res)) {
|
||
message.error(getResponseMessage(res, "获取合同详情失败"));
|
||
return;
|
||
}
|
||
contractCollectModalRef.value?.showModal(getDataResult(res));
|
||
});
|
||
}
|
||
|
||
function handleCollectDone() {
|
||
getList();
|
||
}
|
||
|
||
function handleArchive(row) {
|
||
getLaunchDetail(row.id).then((res) => {
|
||
if (!isSuccessResponse(res) || !getDataResult(res)) {
|
||
message.error(getResponseMessage(res, "获取归档详情失败"));
|
||
return;
|
||
}
|
||
contractArchiveModalRef.value?.showModal(getDataResult(res));
|
||
});
|
||
}
|
||
|
||
function handleArchiveDone() {
|
||
getList();
|
||
}
|
||
|
||
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 || "";
|
||
},
|
||
}),
|
||
],
|
||
),
|
||
okText: "确认终止",
|
||
cancelText: "取消",
|
||
okType: "danger",
|
||
onOk: () =>
|
||
terminateLaunchRecord(row.id, { terminateReason }).then((res) => {
|
||
if (!isSuccessResponse(res)) {
|
||
message.error(getResponseMessage(res, "终止合同失败"));
|
||
return;
|
||
}
|
||
message.success("合同已终止");
|
||
getList();
|
||
}),
|
||
icon: null,
|
||
});
|
||
}
|
||
|
||
function handleDelete(row) {
|
||
Modal.confirm({
|
||
title: `确认删除合同【${row.contractName}】吗?`,
|
||
content: "仅草稿或审批驳回状态支持删除。",
|
||
okText: "删除",
|
||
cancelText: "取消",
|
||
okType: "danger",
|
||
onOk: () => {
|
||
deleteLaunchRecord(row.id).then((res) => {
|
||
if (!isSuccessResponse(res)) {
|
||
message.error(getResponseMessage(res, "删除失败"));
|
||
return;
|
||
}
|
||
message.success("删除成功");
|
||
getList();
|
||
});
|
||
},
|
||
});
|
||
}
|
||
|
||
function handleBatchDelete() {
|
||
const undeletable = selectedRows.value.filter((item) => !canDelete(item));
|
||
if (undeletable.length) {
|
||
message.warning("仅草稿或审批驳回状态支持删除,请重新选择");
|
||
return;
|
||
}
|
||
Modal.confirm({
|
||
title: `确认删除选中的 ${selectedRowKeys.value.length} 条记录吗?`,
|
||
content: "仅草稿或审批驳回状态支持删除。",
|
||
okText: "删除",
|
||
cancelText: "取消",
|
||
okType: "danger",
|
||
onOk: async () => {
|
||
for (const id of selectedRowKeys.value) {
|
||
await deleteLaunchRecord(id);
|
||
}
|
||
message.success("批量删除成功");
|
||
selectedRows.value = [];
|
||
selectedRowKeys.value = [];
|
||
getList();
|
||
},
|
||
});
|
||
}
|
||
|
||
function syncTableHeight() {
|
||
nextTick(() => {
|
||
const viewportHeight = window.innerHeight;
|
||
const queryHeight = queryContainer.value?.offsetHeight || 0;
|
||
const pageHeight = pageWrapperRef.value?.$el?.offsetHeight || 56;
|
||
tableHeight.value = Math.max(
|
||
320,
|
||
viewportHeight - queryHeight - pageHeight - 320,
|
||
);
|
||
});
|
||
}
|
||
|
||
onMounted(() => {
|
||
handleQuery();
|
||
nextTick(() => {
|
||
connectToolbar();
|
||
syncTableHeight();
|
||
});
|
||
window.addEventListener("resize", syncTableHeight);
|
||
});
|
||
|
||
onBeforeUnmount(() => {
|
||
window.removeEventListener("resize", syncTableHeight);
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
.section-container {
|
||
height: calc(100vh - 116px);
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.query-card {
|
||
flex: 0 0 auto;
|
||
}
|
||
|
||
.table-card {
|
||
flex: 1 1 auto;
|
||
min-height: 0;
|
||
}
|
||
|
||
.section-query-container :deep(.ant-form-item) {
|
||
display: flex;
|
||
width: 100%;
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.section-query-container :deep(.ant-form-item .ant-form-item-control) {
|
||
flex: 1;
|
||
}
|
||
|
||
.table-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.table-pagination {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
padding: 12px 4px 2px;
|
||
}
|
||
|
||
@media (max-width: 960px) {
|
||
.section-container {
|
||
height: auto;
|
||
}
|
||
}
|
||
</style>
|