This commit is contained in:
2026-06-01 16:06:53 +08:00
parent 86ab21308e
commit 74911c5ca2
5 changed files with 657 additions and 3 deletions
+53 -2
View File
@@ -101,11 +101,12 @@
<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="580" fixed="right">
<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 @click="handleChange(row)">变更</a>
<a v-if="canEdit(row)" @click="handleEdit(row)">编辑</a>
<a v-if="canSubmit(row)" @click="handleSubmit(row)">提交法务</a>
<a v-if="canViewApprove(row)" @click="handleViewApprove(row)">审批记录</a>
@@ -117,6 +118,7 @@
<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="canTerminate(row)" @click="handleTerminate(row)">终止</a>
<a v-if="canDelete(row)" @click="handleDelete(row)">删除</a>
</div>
</template>
@@ -151,13 +153,14 @@ import {
RedoOutlined,
SearchOutlined,
} from "@ant-design/icons-vue";
import { message, Modal } from "ant-design-vue";
import { Input, message, Modal } from "ant-design-vue";
import ContractPartyPlaceholderModal from "./components/ContractPartyPlaceholderModal.vue";
import MixedSignModal from "./components/mixedSignModal.vue";
import { getContractTypeSimpleLabel } from "./contractLaunchOptions.js";
import {
archiveLaunch,
copyLaunchDraft,
createLaunchChangeDraft,
createLaunchDraft,
deleteLaunchRecord,
getLaunchDetail,
@@ -168,6 +171,7 @@ import {
queryApproveViewUrl,
queryLaunchList,
submitLaunchRecord,
terminateLaunchRecord,
updateLaunchDraft,
} from "@/api/launch.js";
@@ -315,6 +319,10 @@ function canViewApprove(row) {
return ["reviewing", "pending_sign", "signing", "pending_collect", "pending_archive", "completed"].includes(row.status);
}
function canTerminate(row) {
return row.status !== "completed" && row.status !== "terminated";
}
function selectChangeEvent({ records }) {
selectedRows.value = records;
selectedRowKeys.value = records.map((item) => item.id);
@@ -418,6 +426,19 @@ function handleCopy(row) {
});
}
function handleChange(row) {
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");
});
}
function handleSubmit(row) {
submitLaunchRecord(row.id).then((res) => {
if (!isSuccessResponse(res)) {
@@ -548,6 +569,36 @@ function handleArchive(row) {
});
}
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}】吗?`,
+547
View File
@@ -0,0 +1,547 @@
<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-input v-model:value="queryParam.operatorName" allow-clear placeholder="请输入操作人" />
</a-form-item>
</a-col>
<a-col :md="6" :sm="24">
<a-form-item label="操作类型">
<a-select
v-model:value="queryParam.actionType"
allow-clear
placeholder="请选择操作类型"
:options="actionTypeOptions"
/>
</a-form-item>
</a-col>
<a-col :md="6" :sm="24">
<a-form-item label="操作结果">
<a-select
v-model:value="queryParam.actionStatus"
allow-clear
placeholder="请选择操作结果"
:options="actionStatusOptions"
/>
</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 :icon="h(FileExcelOutlined)" @click="handleExportExcel">导出 Excel</a-button>
<a-button :icon="h(FilePdfOutlined)" @click="handleExportPdf">导出 PDF</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' }"
:data="tableData"
:loading="loading"
:loading-config="{ icon: 'vxe-icon-indicator roll', text: '正在拼命加载中...' }"
>
<vxe-column type="seq" title="序号" width="68" />
<vxe-column field="contractCode" title="合同编码" width="170" show-overflow="title" />
<vxe-column field="contractName" title="合同名称" width="220" show-overflow="title" />
<vxe-column field="contractStatus" title="合同状态" width="110">
<template #default="{ row }">
<a-tag :color="getStatusColor(row.contractStatus)">{{ getStatusLabel(row.contractStatus) }}</a-tag>
</template>
</vxe-column>
<vxe-column field="actionName" title="操作名称" width="160" show-overflow="title" />
<vxe-column field="actionType" title="操作类型" width="130">
<template #default="{ row }">
{{ getActionTypeLabel(row.actionType) }}
</template>
</vxe-column>
<vxe-column field="actionStatus" title="操作结果" width="110">
<template #default="{ row }">
<a-tag :color="getActionStatusColor(row.actionStatus)">{{ getActionStatusLabel(row.actionStatus) }}</a-tag>
</template>
</vxe-column>
<vxe-column field="operatorName" title="操作人" width="120" show-overflow="title" />
<vxe-column field="operatorPhone" title="联系电话" width="140" show-overflow="title" />
<vxe-column field="actionTime" title="操作时间" width="170" show-overflow="title" />
<vxe-column field="actionContent" title="操作内容" width="260" show-overflow="title" />
<vxe-column title="详情" width="90" fixed="right">
<template #default="{ row }">
<a @click="handleViewDetail(row)">查看</a>
</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>
<a-modal v-model:open="detailVisible" title="日志详情" width="860px" :footer="null">
<div v-if="currentLog" class="log-detail">
<div class="detail-grid">
<div><span>合同名称:</span><strong>{{ currentLog.contractName || "-" }}</strong></div>
<div><span>合同编码:</span><strong>{{ currentLog.contractCode || "-" }}</strong></div>
<div><span>操作名称:</span><strong>{{ currentLog.actionName || "-" }}</strong></div>
<div><span>操作结果:</span><strong>{{ getActionStatusLabel(currentLog.actionStatus) }}</strong></div>
<div><span>操作人:</span><strong>{{ currentLog.operatorName || "-" }}</strong></div>
<div><span>联系电话:</span><strong>{{ currentLog.operatorPhone || "-" }}</strong></div>
<div><span>操作时间:</span><strong>{{ currentLog.actionTime || "-" }}</strong></div>
<div><span>合同状态:</span><strong>{{ getStatusLabel(currentLog.contractStatus) }}</strong></div>
</div>
<div class="detail-section">
<div class="detail-title">操作内容</div>
<a-textarea :value="currentLog.actionContent || '-'" :rows="4" readonly />
</div>
<div class="detail-section">
<div class="detail-title">原始报文</div>
<a-textarea :value="prettyPayload(currentLog.callbackPayload)" :rows="12" readonly />
</div>
</div>
</a-modal>
</section>
</template>
<script setup>
import { h, nextTick, onBeforeUnmount, onMounted, ref } from "vue";
import { FileExcelOutlined, FilePdfOutlined, RedoOutlined, SearchOutlined } from "@ant-design/icons-vue";
import { message } from "ant-design-vue";
import { saveAs } from "file-saver";
import { exportContractLogExcel, queryContractLogAll, queryContractLogList } from "@/api/launch.js";
const formRef = ref(null);
const queryContainer = ref(null);
const pageWrapperRef = ref(null);
const tableRef = ref(null);
const toolbarRef = ref(null);
const total = ref(0);
const loading = ref(false);
const tableHeight = ref(420);
const tableData = ref([]);
const detailVisible = ref(false);
const currentLog = ref(null);
const actionTypeOptions = [
{ label: "保存", value: "save" },
{ label: "复制", value: "copy" },
{ label: "变更", value: "change" },
{ label: "提交法务", value: "submit" },
{ label: "审批通过", value: "approve" },
{ label: "审批驳回", value: "reject" },
{ label: "开始签署", value: "seal_start" },
{ label: "签署完成", value: "seal_finish" },
{ label: "合同采集", value: "collect" },
{ label: "合同归档", value: "archive" },
{ label: "终止合同", value: "terminate" },
{ label: "删除合同", value: "delete" },
];
const actionStatusOptions = [
{ label: "成功", value: "success" },
{ label: "失败", value: "failed" },
{ label: "处理中", value: "pending" },
];
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",
};
const actionStatusLabelMap = {
success: "成功",
failed: "失败",
pending: "处理中",
};
const actionStatusColorMap = {
success: "success",
failed: "error",
pending: "processing",
};
const queryParam = ref({
pageNo: 1,
pageSize: 10,
contractName: undefined,
contractCode: undefined,
operatorName: undefined,
actionType: undefined,
actionStatus: undefined,
dateRange: [],
});
const queryCardBodyStyle = {
padding: "20px 24px 6px",
};
const tableCardBodyStyle = {
display: "flex",
flexFlow: "column nowrap",
minHeight: "0",
padding: "8px 12px 6px",
};
function isSuccessResponse(res) {
return String(res?.code || "") === "0000";
}
function getPageResult(res) {
return res?.result || {};
}
function getResponseMessage(res, fallback = "操作失败") {
return res?.message || res?.msg || fallback;
}
function connectToolbar() {
const table = tableRef.value;
const toolbar = toolbarRef.value;
if (table && toolbar && table.connect) {
table.connect(toolbar);
}
}
function buildQueryPayload() {
return {
pageNo: queryParam.value.pageNo,
pageSize: queryParam.value.pageSize,
contractName: queryParam.value.contractName,
contractCode: queryParam.value.contractCode,
operatorName: queryParam.value.operatorName,
actionType: queryParam.value.actionType,
actionStatus: queryParam.value.actionStatus,
dateStart: queryParam.value.dateRange?.[0],
dateEnd: queryParam.value.dateRange?.[1],
};
}
function getActionTypeLabel(value) {
return actionTypeOptions.find((item) => item.value === value)?.label || value || "-";
}
function getActionStatusLabel(value) {
return actionStatusLabelMap[value] || value || "-";
}
function getActionStatusColor(value) {
return actionStatusColorMap[value] || "default";
}
function getStatusLabel(value) {
return statusLabelMap[value] || value || "-";
}
function getStatusColor(value) {
return statusColorMap[value] || "default";
}
function prettyPayload(payload) {
if (!payload) {
return "-";
}
try {
return JSON.stringify(JSON.parse(payload), null, 2);
} catch (error) {
return payload;
}
}
function getList() {
loading.value = true;
queryContractLogList(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,
operatorName: undefined,
actionType: undefined,
actionStatus: undefined,
dateRange: [],
});
getList();
}
function onShowSizeChange(current, pageSize) {
queryParam.value.pageNo = current;
queryParam.value.pageSize = pageSize;
getList();
}
function handleViewDetail(row) {
currentLog.value = row;
detailVisible.value = true;
}
function handleExportExcel() {
exportContractLogExcel({
...buildQueryPayload(),
pageNo: 1,
pageSize: 10000,
}).then((data) => {
const blob = new Blob([data], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});
saveAs(blob, `合同日志_${Date.now()}.xlsx`);
message.success("日志 Excel 已导出");
});
}
function handleExportPdf() {
queryContractLogAll({
...buildQueryPayload(),
pageNo: 1,
pageSize: 10000,
}).then((res) => {
if (!isSuccessResponse(res)) {
message.error(getResponseMessage(res, "日志 PDF 导出失败"));
return;
}
const list = res?.result || [];
const printWindow = window.open("", "_blank");
if (!printWindow) {
message.warning("浏览器拦截了新窗口请允许弹窗后重试");
return;
}
const rowsHtml = list.map((item, index) => `
<tr>
<td>${index + 1}</td>
<td>${item.contractCode || ""}</td>
<td>${item.contractName || ""}</td>
<td>${getActionTypeLabel(item.actionType)}</td>
<td>${item.actionName || ""}</td>
<td>${getActionStatusLabel(item.actionStatus)}</td>
<td>${item.operatorName || ""}</td>
<td>${item.actionTime || ""}</td>
<td>${item.actionContent || ""}</td>
</tr>
`).join("");
printWindow.document.write(`
<html>
<head>
<title>合同日志导出</title>
<style>
body { font-family: Arial, sans-serif; padding: 24px; }
h1 { font-size: 20px; margin-bottom: 16px; }
table { width: 100%; border-collapse: collapse; table-layout: fixed; }
th, td { border: 1px solid #d9d9d9; padding: 8px; font-size: 12px; word-break: break-all; vertical-align: top; }
th { background: #f5f5f5; }
</style>
</head>
<body>
<h1>合同日志导出</h1>
<table>
<thead>
<tr>
<th>序号</th>
<th>合同编码</th>
<th>合同名称</th>
<th>操作类型</th>
<th>操作名称</th>
<th>操作结果</th>
<th>操作人</th>
<th>操作时间</th>
<th>操作内容</th>
</tr>
</thead>
<tbody>${rowsHtml}</tbody>
</table>
</body>
</html>
`);
printWindow.document.close();
printWindow.focus();
printWindow.print();
});
}
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-pagination {
display: flex;
justify-content: flex-end;
padding: 12px 4px 2px;
}
.log-detail {
display: flex;
flex-direction: column;
gap: 16px;
}
.detail-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px 18px;
}
.detail-grid span {
color: #64748b;
}
.detail-grid strong {
color: #0f172a;
}
.detail-section {
display: flex;
flex-direction: column;
gap: 8px;
}
.detail-title {
font-size: 14px;
font-weight: 600;
color: #111827;
}
@media (max-width: 960px) {
.section-container {
height: auto;
}
.detail-grid {
grid-template-columns: 1fr;
}
}
</style>