首次提交

This commit is contained in:
2026-05-26 10:10:32 +08:00
parent 02e1486d39
commit a6f7ad12da
45 changed files with 27435 additions and 0 deletions
+577
View File
@@ -0,0 +1,577 @@
<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
size="small"
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
size="small"
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.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
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="150"
show-overflow="title"
/>
<vxe-column
field="contractName"
title="合同名称"
width="220"
show-overflow="title"
/>
<vxe-column
field="contractType"
title="合同类型"
width="110"
show-overflow="title"
/>
<vxe-column
field="templateName"
title="所选模板"
width="150"
show-overflow="title"
/>
<vxe-column
field="counterpartySummary"
title="合同相对方"
width="260"
show-overflow="title"
/>
<vxe-column
field="status"
title="状态"
width="120"
show-overflow="title"
>
<template #default="{ row }">
<a-tag :color="getStatusColor(row.status)">
{{ getStatusLabel(row.status) }}
</a-tag>
</template>
</vxe-column>
<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="260" fixed="right">
<template #default="{ row }">
<div class="table-actions">
<a @click="handleView(row)">查看</a>
<a v-if="canEdit(row)" @click="handleEdit(row)">编辑</a>
<a v-if="canOnlineSign(row)" @click="handleOnlineSign(row)">
{{ row.status === "completed" ? "查看签署" : "在线签订" }}
</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" />
<MixedSignModal ref="mixedSignModalRef" />
</section>
</template>
<script setup>
import { h, nextTick, onBeforeUnmount, onMounted, ref } from "vue";
import {
DeleteOutlined,
PlusOutlined,
RedoOutlined,
SearchOutlined,
} from "@ant-design/icons-vue";
import { message, Modal } from "ant-design-vue";
import ContractPartyPlaceholderModal from "./components/ContractPartyPlaceholderModal.vue";
import MixedSignModal from "./components/mixedSignModal.vue";
import { deleteLaunchRecord, queryLaunchList } from "./api.js";
// 页面内字典(后续可由后端接口替换)
const contractRecordStatusOptions = [
{ label: "草稿", value: "draft" },
{ label: "审批中", value: "reviewing" },
{ label: "审批驳回", value: "rejected" },
{ label: "待签署", value: "pending_sign" },
{ label: "签署中", value: "signing" },
{ label: "已完成", value: "completed" },
]
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 mixedSignModalRef = ref(null);
const total = ref(0);
const loading = ref(false);
const tableHeight = ref(420);
const tableData = ref([]);
const selectedRows = ref([]);
const selectedRowKeys = ref([]);
const allRecords = ref([]);
const queryParam = ref({
pageNo: 1,
pageSize: 10,
contractName: undefined,
contractCode: undefined,
contractType: undefined,
status: undefined,
queryScope: undefined,
templateKey: undefined,
dateRange: [],
});
const queryCardBodyStyle = {
padding: "20px 24px 6px",
};
const tableCardBodyStyle = {
display: "flex",
flexFlow: "column nowrap",
minHeight: "0",
padding: "8px 12px 6px",
};
const templateQueryOptions = [
{ label: "采购合同模板", value: "purchase" },
{ label: "销售合同模板", value: "sales" },
];
const statusLabelMap = {
draft: "草稿",
reviewing: "审批中",
rejected: "审批驳回",
pending_sign: "待签署",
signing: "签署中",
completed: "已完成",
};
const statusColorMap = {
draft: "default",
reviewing: "processing",
rejected: "error",
pending_sign: "warning",
signing: "purple",
completed: "success",
};
const scopeLabelMap = {
personal: "个人级",
company: "公司级",
admin: "管理员级",
};
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 getScopeLabel(scope) {
return scopeLabelMap[scope] || scope;
}
function canEdit(row) {
return ["draft", "rejected"].includes(row.status);
}
function canDelete(row) {
return ["draft", "rejected"].includes(row.status);
}
function canOnlineSign(row) {
return ["pending_sign", "signing", "completed"].includes(row.status);
}
function selectChangeEvent({ records }) {
selectedRows.value = records;
selectedRowKeys.value = records.map((item) => item.id);
}
function filterRecords(records) {
return records.filter((item) => {
const matchName =
!queryParam.value.contractName ||
item.contractName.includes(queryParam.value.contractName);
const matchCode =
!queryParam.value.contractCode ||
item.contractCode.includes(queryParam.value.contractCode);
const matchType =
!queryParam.value.contractType ||
item.contractType === queryParam.value.contractType;
const matchStatus =
!queryParam.value.status || item.status === queryParam.value.status;
const matchScope =
!queryParam.value.queryScope ||
item.queryScope === queryParam.value.queryScope;
const matchTemplate =
!queryParam.value.templateKey ||
item.templateKey === queryParam.value.templateKey;
const matchDate = (() => {
const range = queryParam.value.dateRange || [];
if (!range.length) {
return true;
}
const day = item.createTime.slice(0, 10);
return day >= range[0] && day <= range[1];
})();
return (
matchName &&
matchCode &&
matchType &&
matchStatus &&
matchScope &&
matchTemplate &&
matchDate
);
});
}
function getList() {
loading.value = true;
queryLaunchList().then((records) => {
// TODO: 这里后续直接使用合同发起列表接口返回值
allRecords.value = records;
const filtered = filterRecords(allRecords.value);
const start = (queryParam.value.pageNo - 1) * queryParam.value.pageSize;
const end = start + queryParam.value.pageSize;
total.value = filtered.length;
tableData.value = filtered.slice(start, end);
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,
queryScope: undefined,
templateKey: undefined,
dateRange: [],
});
getList();
}
function onShowSizeChange(current, pageSize) {
queryParam.value.pageNo = current;
queryParam.value.pageSize = pageSize;
getList();
}
function handleCreate() {
contractModalRef.value?.showModal({}, "create");
}
function handleView(row) {
contractModalRef.value?.showModal(row, "view");
}
function handleEdit(row) {
contractModalRef.value?.showModal(row, "edit");
}
function handleOnlineSign(row) {
if (!row.sealContractId && !row.signContractId && !row.contractId) {
message.info(
"当前未接入签署详情接口将进入在线签署弹窗并保留合同业务信息你可以继续上传文件后发起签署",
);
}
mixedSignModalRef.value?.showModal(row);
}
function deleteRowsByIds(ids) {
// TODO: 这里后续替换为删除接口后再刷新列表
allRecords.value = allRecords.value.filter((item) => !ids.includes(item.id));
selectedRows.value = [];
selectedRowKeys.value = [];
getList();
}
function handleDelete(row) {
Modal.confirm({
title: `确认删除合同【${row.contractName}】吗?`,
content: "当前仅删除本地 mock 数据便于后续替换真实接口",
okText: "删除",
cancelText: "取消",
okType: "danger",
onOk: () => {
deleteLaunchRecord([row.id]).then(() => {
deleteRowsByIds([row.id]);
message.success("已删除发起记录");
});
},
});
}
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: () => {
deleteLaunchRecord(selectedRowKeys.value).then(() => {
deleteRowsByIds(selectedRowKeys.value);
message.success("批量删除成功");
});
},
});
}
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;
}
.table-pagination {
display: flex;
justify-content: flex-end;
padding: 12px 4px 2px;
}
@media (max-width: 960px) {
.section-container {
height: auto;
}
}
</style>