This commit is contained in:
2026-06-30 09:54:17 +08:00
parent 2326752b5d
commit 0b578df215
4 changed files with 508 additions and 99 deletions
+8
View File
@@ -121,6 +121,14 @@ export function mockCollectLaunch(id) {
});
}
export function saveContractCollectInfo(id, data) {
return request({
url: `/contract/launch/${id}/collect/save`,
method: "post",
data,
});
}
export function archiveLaunch(id, data = {}) {
return request({
url: `/contract/launch/${id}/archive`,
@@ -0,0 +1,348 @@
<template>
<a-modal
v-model:open="visible"
title="合同采集"
width="800px"
:mask-closable="false"
:confirm-loading="submitting"
ok-text="确认采集"
cancel-text="取消"
@ok="handleSubmit"
>
<div class="collect-body">
<a-descriptions :column="2" size="small" bordered class="contract-brief">
<a-descriptions-item label="合同名称">
{{ row?.contractName || "-" }}
</a-descriptions-item>
<a-descriptions-item label="合同编码">
{{ row?.contractCode || "-" }}
</a-descriptions-item>
</a-descriptions>
<a-divider style="margin: 16px 0" />
<a-form layout="vertical">
<div class="section-header">
<span class="section-title-icon">📜</span>
用印信息c_seal
</div>
<div class="field-row">
<a-form-item label="是否用印" required class="field-half">
<a-radio-group v-model:value="formState.sealIsSeal">
<a-radio value="1"></a-radio>
<a-radio value="0"></a-radio>
</a-radio-group>
</a-form-item>
<a-form-item
label="用印类型"
class="field-half"
>
<a-input
:value="sealTypeDisplay"
readonly
placeholder="沿用合同签订方式"
/>
</a-form-item>
</div>
<div class="field-row">
<a-form-item label="审批是否一致" required class="field-half">
<a-radio-group v-model:value="formState.sealApproveSame">
<a-radio value="1"></a-radio>
<a-radio value="0"></a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="用印数量" class="field-half">
<a-input-number
v-model:value="formState.sealQuantity"
:min="1"
:precision="0"
:disabled="formState.sealIsSeal !== '1'"
style="width: 100%"
placeholder="默认1份"
/>
</a-form-item>
</div>
<div class="field-row">
<a-form-item label="用印时间" required class="field-half">
<a-date-picker
v-model:value="formState.sealDate"
value-format="YYYY-MM-DD"
:disabled="formState.sealIsSeal !== '1'"
style="width: 100%"
placeholder="用印日期"
/>
</a-form-item>
</div>
<div class="section-header" style="margin-top: 20px">
<span class="section-title-icon"></span>
签订信息c_sign
</div>
<div class="field-row">
<a-form-item label="签订日期" required class="field-half">
<a-date-picker
v-model:value="formState.signDate"
value-format="YYYY-MM-DD"
style="width: 100%"
placeholder="请选择签订日期"
/>
</a-form-item>
<a-form-item label="合同生效日期" required class="field-half">
<a-date-picker
v-model:value="formState.activeDate"
value-format="YYYY-MM-DD"
style="width: 100%"
placeholder="必填,且需在合同期限内"
/>
</a-form-item>
</div>
<div class="field-row">
<a-form-item label="合同履行执行人" required class="field-half">
<a-input
v-model:value="formState.executorAccount"
placeholder="履行执行人账号"
/>
</a-form-item>
</div>
<div class="section-header" style="margin-top: 20px">
<span class="section-title-icon">📎</span>
合同签署文本
</div>
<a-form-item label="签署文件" required>
<a-upload-dragger
:file-list="signedFileList"
:before-upload="handleSignedFileUpload"
:show-upload-list="{ showRemoveIcon: true }"
@remove="handleRemoveSignedFile"
>
<div class="upload-text">
点击或拖拽上传签署完成的合同文件
</div>
<div class="upload-hint">
支持 .pdf 格式当前仅本地上传到业务系统文件服务
</div>
</a-upload-dragger>
</a-form-item>
</a-form>
</div>
</a-modal>
</template>
<script setup>
import { computed, reactive, ref } from "vue";
import { message } from "ant-design-vue";
import {
getLaunchDetail,
saveContractCollectInfo,
mockCollectLaunch,
uploadLaunchFile,
} from "@/api/launch.js";
const SUCCESS_CODE = "0000";
const visible = ref(false);
const submitting = ref(false);
const row = ref(null);
const signedFileList = ref([]);
const formState = reactive({
sealIsSeal: "1",
sealApproveSame: "1",
sealQuantity: 1,
sealDate: "",
signDate: "",
activeDate: "",
executorAccount: "",
signedFileName: "",
signedFilePath: "",
signedFileType: "",
signedFileSize: null,
});
const emit = defineEmits(["done"]);
const sealTypeDisplay = computed(() => {
return row.value?.sealTypeLabel || row.value?.sealType || "公章";
});
function resetForm(record) {
formState.sealIsSeal = record.sealIsSeal || "1";
formState.sealApproveSame = record.sealApproveSame || "1";
formState.sealQuantity = record.sealQuantity || 1;
formState.sealDate = record.sealDate || "";
formState.signDate = record.signDate || "";
formState.activeDate = record.activeDate || "";
formState.executorAccount = record.executorAccount || record.initiator || "";
formState.signedFileName = record.signedFileName || "";
formState.signedFilePath = record.signedFilePath || "";
formState.signedFileType = record.signedFileType || "";
formState.signedFileSize = record.signedFileSize || null;
if (record.signedFileName) {
signedFileList.value = [
{
uid: "signed_existing",
name: record.signedFileName,
status: "done",
size: record.signedFileSize || 0,
},
];
} else {
signedFileList.value = [];
}
}
async function showModal(record) {
row.value = record || {};
resetForm(record || {});
visible.value = true;
}
function buildKey(prefix) {
return `${prefix}_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`;
}
async function handleSignedFileUpload(file) {
const formData = new FormData();
formData.append("file", file);
try {
const res = await uploadLaunchFile(formData);
if (String(res?.code || "") !== SUCCESS_CODE) {
message.error(res?.message || res?.msg || "签署文件上传失败");
return false;
}
const fileItem = {
uid: buildKey("signed"),
name: file.name,
status: "done",
size: file.size,
fileName: file.name,
filePath: res?.result || "",
fileType: file.name.split(".").pop()?.toLowerCase() || "",
fileSize: file.size,
};
signedFileList.value = [fileItem];
formState.signedFileName = fileItem.fileName;
formState.signedFilePath = fileItem.filePath;
formState.signedFileType = fileItem.fileType;
formState.signedFileSize = fileItem.fileSize;
message.success(`已上传签署文件:${file.name}`);
} catch (error) {
message.error("签署文件上传失败");
}
return false;
}
function handleRemoveSignedFile() {
signedFileList.value = [];
formState.signedFileName = "";
formState.signedFilePath = "";
formState.signedFileType = "";
formState.signedFileSize = null;
}
function validateForm() {
if (!formState.sealIsSeal) return "请选择是否用印";
if (!formState.sealApproveSame) return "请选择审批是否一致";
if (formState.sealIsSeal === "1") {
if (!formState.sealDate) return "请选择用印时间";
if (!formState.sealQuantity || formState.sealQuantity < 1)
return "请填写用印数量";
}
if (!formState.signDate) return "请选择签订日期";
if (!formState.activeDate) return "请选择合同生效日期";
if (!formState.executorAccount?.trim()) return "请填写合同履行执行人";
if (!signedFileList.value.length) return "请上传签署完成的合同文件";
return "";
}
async function handleSubmit() {
const validateMessage = validateForm();
if (validateMessage) {
message.warning(validateMessage);
return;
}
submitting.value = true;
try {
// 1. 保存采集信息到合同记录
const saveRes = await saveContractCollectInfo(row.value.id, {
sealIsSeal: formState.sealIsSeal,
sealApproveSame: formState.sealApproveSame,
sealQuantity: formState.sealIsSeal === "1" ? formState.sealQuantity : null,
sealDate: formState.sealIsSeal === "1" ? formState.sealDate : null,
signDate: formState.signDate,
activeDate: formState.activeDate,
executorAccount: formState.executorAccount?.trim(),
signedFileName: formState.signedFileName,
signedFilePath: formState.signedFilePath,
signedFileType: formState.signedFileType,
signedFileSize: formState.signedFileSize,
});
if (String(saveRes?.code || "") !== SUCCESS_CODE) {
message.error(saveRes?.message || "保存采集信息失败");
return;
}
// 2. 触发采集流转(push to 法务)
const collectRes = await mockCollectLaunch(row.value.id);
if (String(collectRes?.code || "") !== SUCCESS_CODE) {
message.error(collectRes?.message || "合同采集失败");
return;
}
message.success("合同采集完成,已流转到待归档");
visible.value = false;
emit("done");
} catch (error) {
message.error("合同采集失败");
} finally {
submitting.value = false;
}
}
defineExpose({ showModal });
</script>
<style scoped>
.collect-body {
max-height: 72vh;
overflow-y: auto;
padding-right: 4px;
}
.contract-brief {
margin-bottom: 8px;
}
.field-row {
display: flex;
gap: 16px;
}
.field-half {
flex: 1;
min-width: 0;
}
.section-header {
display: flex;
align-items: center;
gap: 6px;
margin: 14px 0 12px;
font-weight: 600;
font-size: 15px;
color: #1f2937;
}
.section-title-icon {
font-size: 16px;
}
.upload-text {
color: #374151;
font-size: 14px;
}
.upload-hint {
color: #9ca3af;
font-size: 12px;
margin-top: 4px;
}
</style>
@@ -691,39 +691,7 @@
placeholder="选填,用于应收账款提醒"
/>
</a-form-item>
<a-form-item class="tight-item" label="签订日期">
<a-date-picker
v-model:value="detailState.signDate"
value-format="YYYY-MM-DD"
style="width: 100%"
placeholder="签署完成前需补齐"
/>
</a-form-item>
<a-form-item class="tight-item" label="用印时间">
<a-date-picker
v-model:value="detailState.sealDate"
value-format="YYYY-MM-DD"
style="width: 100%"
placeholder="签署完成前需补齐"
/>
</a-form-item>
<a-form-item class="tight-item" label="合同生效日期">
<a-date-picker
v-model:value="detailState.activeDate"
value-format="YYYY-MM-DD"
style="width: 100%"
placeholder="必填,且必须在合同期限范围内"
/>
</a-form-item>
<a-form-item class="tight-item" label="用印数量">
<a-input-number
v-model:value="detailState.sealQuantity"
:min="1"
:precision="0"
style="width: 100%"
placeholder="默认1份"
/>
</a-form-item>
</div>
<div class="boolean-grid">
@@ -794,20 +762,7 @@
<a-radio value="0">否</a-radio>
</a-radio-group>
</div>
<div class="boolean-row">
<span>是否用印</span>
<a-radio-group v-model:value="detailState.sealIsSeal">
<a-radio value="1">是</a-radio>
<a-radio value="0">否</a-radio>
</a-radio-group>
</div>
<div class="boolean-row">
<span>审批是否一致</span>
<a-radio-group v-model:value="detailState.sealApproveSame">
<a-radio value="1">是</a-radio>
<a-radio value="0">否</a-radio>
</a-radio-group>
</div>
</div>
<div class="extra-amount-section">
@@ -866,7 +821,7 @@
:data-source="detailState.performancePlans"
:pagination="false"
size="small"
:scroll="{ x: 820 }"
:scroll="{ x: 780 }"
row-key="id"
class="plan-table"
>
@@ -926,27 +881,20 @@
/>
</template>
</a-table-column>
<a-table-column
title="所属主体编码"
key="planSubjectCode"
width="160"
>
<template #default="{ record }">
<a-input
v-model:value="record.planSubjectCode"
placeholder="所属主体机构编码"
/>
</template>
</a-table-column>
<a-table-column
title="所属主体"
key="planSubjectName"
width="180"
width="200"
>
<template #default="{ record }">
<a-input
<a-select
v-model:value="record.planSubjectName"
placeholder="所属主体名称"
:options="ourParties.map(p => ({ label: `${p.companyName} (${p.subjectCode})`, value: p.companyName, subjectCode: p.subjectCode }))"
allow-clear
placeholder="从已选我方主体中选择"
:disabled="!ourParties.length"
style="width: 100%"
@change="(value, option) => { record.planSubjectCode = option?.subjectCode || ''; }"
/>
</template>
</a-table-column>
@@ -1471,12 +1419,6 @@ function createContractFormData() {
yfkAmount: 0,
bzjAmount: 0,
performancePlans: [],
signDate: "",
sealDate: "",
activeDate: "",
sealQuantity: 1,
sealIsSeal: "1",
sealApproveSame: "1",
};
}
@@ -1534,11 +1476,8 @@ function copyContractFormData(data = {}) {
: null
: null;
} else if (
key === "signDate" ||
key === "sealDate" ||
key === "invoiceDate" ||
key === "receiveDate" ||
key === "activeDate"
key === "receiveDate"
) {
form[key] = source[key] ? String(source[key]).slice(0, 10) : form[key];
} else if (key === "assistDept") {
@@ -2026,7 +1965,7 @@ function handleBodyConfigSave(payload = {}) {
syncUploadLists();
if (modalMode.value === "edit" && detailState.value.id) {
const errorMessage = validateState();
const errorMessage = validateDraft();
if (errorMessage) {
message.success(
"合同正文配置已保存到当前页面,请继续完善主信息后再保存合同草稿。",
@@ -2064,7 +2003,85 @@ function removePerformancePlan(id) {
detailState.value.performancePlans.filter((item) => item.id !== id);
}
function validateState() {
/** 草稿保存校验(宽松):仅校验必填核心字段 */
function validateDraft() {
if (
!ourParties.value.length ||
ourParties.value.some((party) => !party.companyName?.trim())
)
return "请确认我方信息";
if (
currentSelfSubjectCode.value &&
!ourParties.value.some(
(party) =>
String(party.subjectCode || "").trim() === currentSelfSubjectCode.value,
)
) {
return "当前登录主体必须包含在我方主体中";
}
if (
isSingleAgreementChecked.value &&
currentSelfSubjectCode.value &&
ourParties.value.some(
(party) =>
String(party.subjectCode || "").trim() !== currentSelfSubjectCode.value,
)
) {
return "单方协议仅允许使用当前登录主体";
}
if (!isSingleAgreementChecked.value && !counterpartyParties.value.length)
return "请至少添加一个相对方";
if (
!isSingleAgreementChecked.value &&
counterpartyParties.value.some((party) => !party.companyName?.trim())
)
return "请选择相对方";
if (!detailState.value.contractName?.trim()) return "请填写合同名称";
if (!detailState.value.contractTypeCode) return "请选择合同类型";
if (
detailState.value.contractPeriodType === "0" &&
(!detailState.value.effectiveStart || !detailState.value.effectiveEnd)
)
return "请选择固定期限起止日期";
if (
detailState.value.contractPeriodType === "1" &&
!detailState.value.periodExplain?.trim()
)
return "请填写期限说明";
if (!detailState.value.contractBodyFileName?.trim())
return "请先配置合同正文";
if (
!isSingleAgreementChecked.value &&
requiresAuthAttachment.value &&
!(detailState.value.authAttachmentList || []).length
)
return "请上传授权委托书附件";
// 履行计划:草稿保存时即要求至少一条且字段完整
if (!(detailState.value.performancePlans || []).length)
return "请至少添加一条履行计划";
for (const plan of detailState.value.performancePlans || []) {
if (!plan.planMatter?.trim()) return "请填写履行计划的履行事项";
if (!plan.planWhere?.trim()) return "请填写履行计划的履约条件";
if (!plan.planTime) return "请选择履行计划的节点日期";
if (
plan.planDay === null ||
plan.planDay === undefined ||
plan.planDay === ""
)
return "请填写履行计划的提醒提前天数";
if (
!["0", "3"].includes(detailState.value.valuationMode) &&
(plan.planMoney === null ||
plan.planMoney === undefined ||
plan.planMoney === "")
)
return "请填写履行计划的款项金额";
}
return "";
}
/** 提交法务校验(严格):补全 pushContract 所有必填项 + 履行计划 */
function validateSubmit() {
if (
!ourParties.value.length ||
ourParties.value.some((party) => !party.companyName?.trim())
@@ -2143,6 +2160,13 @@ function validateState() {
detailState.value.needAmendments === ""
)
return "请选择是否需修订条款";
// 以下为 pushContract 必填项补充校验(接口文档要求)
if (detailState.value.isMajorContract === undefined || detailState.value.isMajorContract === null || detailState.value.isMajorContract === "")
return "请选择是否重大合同";
if (detailState.value.isPublicTender === undefined || detailState.value.isPublicTender === null)
return "请选择是否公开招标";
if (detailState.value.isInvolveHongKongMacaoTaiwan === undefined || detailState.value.isInvolveHongKongMacaoTaiwan === null)
return "请选择是否涉及港澳台";
if (!detailState.value.paymentDirection) return "请选择收支方向";
if (!detailState.value.contractTypeCode) return "请选择合同类型编码";
if (!detailState.value.executorAccount?.trim()) return "请填写合同执行人";
@@ -2180,7 +2204,7 @@ function validateState() {
}
function handleSave() {
const errorMessage = validateState();
const errorMessage = validateDraft();
if (errorMessage) {
message.warning(errorMessage);
return;
@@ -2359,14 +2383,8 @@ function buildLaunchRecord() {
contractTextFilePath: detailState.value.contractBodyFilePath || "",
contractTextFileType: detailState.value.contractBodyFileType || "docx",
contractTextFileSize: detailState.value.contractBodyFileSize || null,
signDate: formatDateTimeValue(detailState.value.signDate),
sealDate: formatDateTimeValue(detailState.value.sealDate),
invoiceDate: formatDateTimeValue(detailState.value.invoiceDate),
receiveDate: formatDateTimeValue(detailState.value.receiveDate),
activeDate: formatDateTimeValue(detailState.value.activeDate),
sealQuantity: detailState.value.sealQuantity,
sealIsSeal: detailState.value.sealIsSeal || "1",
sealApproveSame: detailState.value.sealApproveSame || "1",
templateFieldValuesJson: JSON.stringify(
detailState.value.templateFieldValues || {},
),
+50 -15
View File
@@ -247,7 +247,7 @@
}}</a
>
<a v-if="canCollect(row)" @click="handleMockCollect(row)">{{
isSingleAgreementRecord(row) ? "测试生效" : "测试采集"
isSingleAgreementRecord(row) ? "测试生效" : "合同采集"
}}</a>
<a v-if="canArchive(row)" @click="handleArchive(row)">{{
isSingleAgreementRecord(row) ? "办结" : "归档"
@@ -278,6 +278,10 @@
ref="contractModalRef"
@save="handleModalSave"
/>
<ContractCollectModal
ref="contractCollectModalRef"
@done="handleCollectDone"
/>
<MixedSignModal
ref="mixedSignModalRef"
@business-updated="handleSignBusinessUpdated"
@@ -303,6 +307,7 @@ import {
} 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";
@@ -318,7 +323,6 @@ import {
getLaunchDetail,
getLawApproveCallbackSample,
mockApproveLaunch,
mockCollectLaunch,
mockRejectLaunch,
finishLaunchSeal,
queryApproveViewUrl,
@@ -347,6 +351,7 @@ 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);
@@ -699,6 +704,41 @@ function handleChange(row) {
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", [
@@ -1032,24 +1072,19 @@ function handleModalSave(payload) {
}
function handleMockCollect(row) {
Modal.confirm({
title: "测试操作模拟合同采集",
content: `模拟将合同【${row.contractName}】信息采集同步至法务系统,状态将变为「待归档」。`,
okText: "确认模拟",
cancelText: "取消",
onOk: () =>
mockCollectLaunch(row.id).then((res) => {
if (!isSuccessResponse(res)) {
message.error(getResponseMessage(res, "模拟合同采集失败"));
getLaunchDetail(row.id).then((res) => {
if (!isSuccessResponse(res) || !getDataResult(res)) {
message.error(getResponseMessage(res, "获取合同详情失败"));
return;
}
message.success("测试合同采集完成合同已流转到待归档");
getList();
}),
icon: null,
contractCollectModalRef.value?.showModal(getDataResult(res));
});
}
function handleCollectDone() {
getList();
}
function handleArchive(row) {
getLaunchDetail(row.id).then((res) => {
if (!isSuccessResponse(res) || !getDataResult(res)) {