归档功能完善

This commit is contained in:
2026-06-08 16:15:58 +08:00
parent 92fd726e2b
commit 974f6c7a5b
2 changed files with 461 additions and 20 deletions
@@ -0,0 +1,447 @@
<template>
<a-modal
v-model:open="visible"
title="合同归档"
width="920px"
:mask-closable="false"
:confirm-loading="submitting"
ok-text="确认归档"
cancel-text="取消"
@ok="handleSubmit"
>
<div class="archive-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-item label="法务合同编号">
{{ row?.lawContractCode || "-" }}
</a-descriptions-item>
<a-descriptions-item label="当前状态">
{{ row?.status || "-" }}
</a-descriptions-item>
</a-descriptions>
<a-divider style="margin: 16px 0" />
<a-form layout="vertical">
<div class="archive-grid">
<a-form-item label="归档接收部门编码" required>
<a-input
v-model:value="formState.archiveReceiveDept"
placeholder="默认带出当前组织编码,可按实际调整"
/>
</a-form-item>
<a-form-item label="移交人账号" required>
<a-input
v-model:value="formState.archiveTransferor"
placeholder="请输入移交人账号"
/>
</a-form-item>
</div>
<div class="section-header">
<span>纸质档案材料</span>
<a-button type="link" @click="addPaper">新增纸质档案</a-button>
</div>
<div
v-for="(paper, index) in formState.archivePapers"
:key="paper._key"
class="archive-card"
>
<div class="archive-card-header">
<span>纸质档案 {{ index + 1 }}</span>
<a-button
v-if="formState.archivePapers.length > 1"
type="link"
danger
@click="removePaper(paper._key)"
>
删除
</a-button>
</div>
<div class="archive-grid archive-grid-paper">
<a-form-item label="档案名称" required>
<a-input v-model:value="paper.archivesName" placeholder="如:合同文本" />
</a-form-item>
<a-form-item label="份数" required>
<a-input v-model:value="paper.archivesNum" placeholder="如:1" />
</a-form-item>
<a-form-item label="归档时间" required class="field-span-2">
<a-date-picker
v-model:value="paper.archivesTime"
show-time
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
placeholder="请选择归档时间"
/>
</a-form-item>
</div>
</div>
<div class="section-header">
<span>电子档案材料</span>
<a-button type="link" @click="addEle">新增电子档案</a-button>
</div>
<div
v-for="(ele, index) in formState.archiveEles"
:key="ele._key"
class="archive-card"
>
<div class="archive-card-header">
<span>电子档案 {{ index + 1 }}</span>
<a-button
v-if="formState.archiveEles.length > 1"
type="link"
danger
@click="removeEle(ele._key)"
>
删除
</a-button>
</div>
<a-form-item label="档案名称" required>
<a-input v-model:value="ele.archivesName" placeholder="如:合同扫描件" />
</a-form-item>
<a-form-item label="归档附件" required>
<a-upload-dragger
:file-list="ele.approvalAttachs"
:before-upload="(file) => handleEleAttachUpload(file, ele)"
:show-upload-list="{ showRemoveIcon: true }"
@remove="(file) => handleRemoveEleAttach(ele, file)"
multiple
>
<div class="upload-text">点击或拖拽上传电子档案附件</div>
<div class="upload-hint">支持多个附件,当前仅本地上传到业务系统文件服务</div>
</a-upload-dragger>
</a-form-item>
</div>
</a-form>
</div>
</a-modal>
</template>
<script setup>
import { reactive, ref } from "vue";
import dayjs from "dayjs";
import { message } from "ant-design-vue";
import { archiveLaunch, getLaunchSelfProfile, uploadLaunchFile } from "@/api/launch.js";
const SUCCESS_CODE = "0000";
const visible = ref(false);
const submitting = ref(false);
const row = ref(null);
const selfProfile = ref(null);
const formState = reactive({
archiveReceiveDept: "",
archiveTransferor: "",
archivePapers: [],
archiveEles: [],
});
const emit = defineEmits(["done"]);
function buildKey(prefix) {
return `${prefix}_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`;
}
function normalizeArchiveFile(item = {}) {
const fileName = item.name || item.fileName || "";
const fileType = item.fileType || resolveFileType(fileName);
return {
uid: item.uid || buildKey("archive_file"),
name: fileName,
status: item.status || "done",
size: item.size ?? item.fileSize ?? 0,
fileName,
filePath: item.filePath || "",
fileType,
fileSize: item.size ?? item.fileSize ?? 0,
};
}
function createPaperItem(source = {}) {
return {
id: source.id || null,
_key: source._key || buildKey("paper"),
archivesName: source.archivesName || "",
archivesNum: source.archivesNum || "",
archivesTime: source.archivesTime || dayjs().format("YYYY-MM-DD HH:mm:ss"),
};
}
function createEleItem(source = {}) {
return {
id: source.id || null,
_key: source._key || buildKey("ele"),
archivesName: source.archivesName || "",
approvalAttachs: (source.approvalAttachs || []).map((item) =>
normalizeArchiveFile(item),
),
};
}
function resolveFileType(fileName = "") {
const suffix = fileName.split(".").pop()?.toLowerCase() || "";
return suffix;
}
function buildSignedArchiveFile(record = {}) {
const signedFromList = (record.fileList || []).find(
(item) => item.fileCategory === "signed" && item.filePath,
);
const fileName =
record.signedFileName ||
signedFromList?.fileName ||
"";
const filePath =
record.signedFilePath ||
signedFromList?.filePath ||
"";
if (!fileName || !filePath) {
return null;
}
return normalizeArchiveFile({
uid: buildKey("archive_file"),
name: fileName,
status: "done",
size: record.signedFileSize ?? signedFromList?.fileSize ?? 0,
fileName,
filePath,
fileType: record.signedFileType || signedFromList?.fileType || resolveFileType(fileName),
fileSize: record.signedFileSize ?? signedFromList?.fileSize ?? 0,
});
}
function buildDefaultEles(record = {}) {
if (record.archiveEles?.length) {
return record.archiveEles.map((item) => createEleItem(item));
}
const signedFile = buildSignedArchiveFile(record);
if (!signedFile) {
return [createEleItem()];
}
return [
createEleItem({
archivesName: "已签署合同文件",
approvalAttachs: [signedFile],
}),
];
}
function resetForm(record = {}, profile = {}) {
formState.archiveReceiveDept =
record.archiveReceiveDept || record.orgId || profile.id || "";
formState.archiveTransferor =
record.archiveTransferor || record.initiator || record.createBy || "";
formState.archivePapers =
record.archivePapers?.length
? record.archivePapers.map((item) => createPaperItem(item))
: [createPaperItem()];
formState.archiveEles = buildDefaultEles(record);
}
async function showModal(record) {
row.value = record || {};
selfProfile.value = await getLaunchSelfProfile();
resetForm(record || {}, selfProfile.value || {});
visible.value = true;
}
function addPaper() {
formState.archivePapers.push(createPaperItem());
}
function removePaper(key) {
formState.archivePapers = formState.archivePapers.filter((item) => item._key !== key);
}
function addEle() {
formState.archiveEles.push(createEleItem());
}
function removeEle(key) {
formState.archiveEles = formState.archiveEles.filter((item) => item._key !== key);
}
async function handleEleAttachUpload(file, ele) {
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;
}
ele.approvalAttachs.push(
normalizeArchiveFile({
uid: buildKey("archive_file"),
name: file.name,
status: "done",
size: file.size,
fileName: file.name,
filePath: res?.result || "",
fileType: resolveFileType(file.name),
fileSize: file.size,
}),
);
message.success(`已上传附件:${file.name}`);
} catch (error) {
message.error("归档附件上传失败");
}
return false;
}
function handleRemoveEleAttach(ele, file) {
ele.approvalAttachs = ele.approvalAttachs.filter(
(item) => item.uid !== file.uid && item.filePath !== file.filePath,
);
}
function validateForm() {
if (!String(formState.archiveReceiveDept || "").trim()) {
return "请填写归档接收部门编码";
}
if (!String(formState.archiveTransferor || "").trim()) {
return "请填写移交人账号";
}
if (!formState.archivePapers.length) {
return "请至少填写一条纸质档案材料";
}
for (const paper of formState.archivePapers) {
if (!String(paper.archivesName || "").trim()) {
return "请填写纸质档案名称";
}
if (!String(paper.archivesNum || "").trim()) {
return "请填写纸质档案份数";
}
if (!paper.archivesTime) {
return "请填写纸质档案归档时间";
}
}
if (!formState.archiveEles.length) {
return "请至少填写一条电子档案材料";
}
for (const ele of formState.archiveEles) {
if (!String(ele.archivesName || "").trim()) {
return "请填写电子档案名称";
}
if (!(ele.approvalAttachs || []).length) {
return "请上传电子档案附件";
}
}
return "";
}
function buildPayload() {
return {
archiveReceiveDept: String(formState.archiveReceiveDept || "").trim(),
archiveTransferor: String(formState.archiveTransferor || "").trim(),
archivePapers: formState.archivePapers.map((paper) => ({
id: paper.id || null,
archivesName: String(paper.archivesName || "").trim(),
archivesNum: String(paper.archivesNum || "").trim(),
archivesTime: paper.archivesTime,
})),
archiveEles: formState.archiveEles.map((ele) => ({
id: ele.id || null,
archivesName: String(ele.archivesName || "").trim(),
approvalAttachs: ele.approvalAttachs.map((file) => ({
fileName: file.fileName || file.name,
filePath: file.filePath,
fileType: file.fileType,
fileSize: file.fileSize ?? file.size ?? 0,
})),
})),
};
}
async function handleSubmit() {
const validateMessage = validateForm();
if (validateMessage) {
message.warning(validateMessage);
return;
}
submitting.value = true;
try {
const res = await archiveLaunch(row.value.id, buildPayload());
if (String(res?.code || "") !== SUCCESS_CODE) {
message.error(res?.message || res?.msg || "归档失败");
return;
}
message.success("归档完成当前为本地模拟流转法务接口暂未实际调用");
visible.value = false;
emit("done");
} catch (error) {
message.error("归档失败");
} finally {
submitting.value = false;
}
}
defineExpose({ showModal });
</script>
<style scoped>
.archive-body {
max-height: 72vh;
overflow-y: auto;
padding-right: 4px;
}
.contract-brief {
margin-bottom: 8px;
}
.archive-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.archive-grid-paper .field-span-2 {
grid-column: 1 / -1;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin: 14px 0 8px;
font-weight: 600;
color: #1f2937;
}
.archive-card {
border: 1px solid #e5e7eb;
border-radius: 10px;
padding: 12px 14px 6px;
background: #fafcff;
margin-bottom: 12px;
}
.archive-card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
font-weight: 500;
}
.upload-text {
color: #374151;
font-size: 14px;
}
.upload-hint {
color: #9ca3af;
font-size: 12px;
margin-top: 4px;
}
</style>
+14 -20
View File
@@ -116,7 +116,7 @@
<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="canArchive(row)" @click="handleArchive(row)">归档</a>
<a v-if="canTerminate(row)" @click="handleTerminate(row)">终止</a>
<a v-if="canDelete(row)" @click="handleDelete(row)">删除</a>
</div>
@@ -142,6 +142,7 @@
<ContractPartyPlaceholderModal ref="contractModalRef" @save="handleModalSave" />
<MixedSignModal ref="mixedSignModalRef" @business-updated="handleSignBusinessUpdated" />
<UploadSignedModal ref="uploadSignedModalRef" @done="handleSignBusinessUpdated" />
<ContractArchiveModal ref="contractArchiveModalRef" @done="handleArchiveDone" />
</section>
</template>
@@ -157,9 +158,9 @@ import { Input, message, Modal } from "ant-design-vue";
import ContractPartyPlaceholderModal from "./components/ContractPartyPlaceholderModal.vue";
import MixedSignModal from "./components/mixedSignModal.vue";
import UploadSignedModal from "./components/UploadSignedModal.vue";
import ContractArchiveModal from "./components/ContractArchiveModal.vue";
import { getContractTypeSimpleLabel } from "./contractLaunchOptions.js";
import {
archiveLaunch,
copyLaunchDraft,
createLaunchChangeDraft,
createLaunchDraft,
@@ -197,6 +198,7 @@ const toolbarRef = ref(null);
const contractModalRef = ref(null);
const mixedSignModalRef = ref(null);
const uploadSignedModalRef = ref(null);
const contractArchiveModalRef = ref(null);
const total = ref(0);
const loading = ref(false);
@@ -685,27 +687,19 @@ function handleMockCollect(row) {
}
function handleArchive(row) {
Modal.confirm({
title: "确认归档合同",
content: h("div", [
h("p", `确认将合同【${row.contractName}】归档?`),
h("p", { style: { color: "#999", fontSize: "12px" } }, "归档后合同进入已完成状态全生命周期结束"),
]),
okText: "确认归档",
cancelText: "取消",
onOk: () =>
archiveLaunch(row.id, {}).then((res) => {
if (!isSuccessResponse(res)) {
message.error(getResponseMessage(res, "测试归档失败"));
return;
}
message.success("测试归档完成合同已流转到已完成");
getList();
}),
icon: null,
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({