审批完成的合同,在合同签署界面进行在线签署。合同发起界面不能在线签署。
This commit is contained in:
@@ -3,7 +3,6 @@
|
|||||||
<a-layout-sider width="244" class="contract-sider">
|
<a-layout-sider width="244" class="contract-sider">
|
||||||
<div class="sider-brand">
|
<div class="sider-brand">
|
||||||
<div class="brand-badge">合同管理</div>
|
<div class="brand-badge">合同管理</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a-menu
|
<a-menu
|
||||||
@@ -16,6 +15,10 @@
|
|||||||
<form-outlined />
|
<form-outlined />
|
||||||
<span>发起合同</span>
|
<span>发起合同</span>
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
|
<a-menu-item key="/contract-console/sign">
|
||||||
|
<file-done-outlined />
|
||||||
|
<span>合同签署</span>
|
||||||
|
</a-menu-item>
|
||||||
<a-menu-item key="/contract-console/manifest">
|
<a-menu-item key="/contract-console/manifest">
|
||||||
<file-text-outlined />
|
<file-text-outlined />
|
||||||
<span>模板管理</span>
|
<span>模板管理</span>
|
||||||
@@ -39,9 +42,7 @@
|
|||||||
<a-layout-header class="contract-header">
|
<a-layout-header class="contract-header">
|
||||||
<div>
|
<div>
|
||||||
<div class="header-title">{{ currentTitle }}</div>
|
<div class="header-title">{{ currentTitle }}</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</a-layout-header>
|
</a-layout-header>
|
||||||
|
|
||||||
<a-layout-content class="contract-content">
|
<a-layout-content class="contract-content">
|
||||||
@@ -54,12 +55,22 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { FileTextOutlined, FormOutlined, ProfileOutlined, TeamOutlined } from '@ant-design/icons-vue'
|
import {
|
||||||
|
FileDoneOutlined,
|
||||||
|
FileTextOutlined,
|
||||||
|
FormOutlined,
|
||||||
|
ProfileOutlined,
|
||||||
|
TeamOutlined,
|
||||||
|
} from '@ant-design/icons-vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const selectedKey = computed(() => {
|
const selectedKey = computed(() => {
|
||||||
|
if (route.path.startsWith('/contract-console/sign')) {
|
||||||
|
return '/contract-console/sign'
|
||||||
|
}
|
||||||
|
|
||||||
if (route.path.startsWith('/contract-console/manifest')) {
|
if (route.path.startsWith('/contract-console/manifest')) {
|
||||||
return '/contract-console/manifest'
|
return '/contract-console/manifest'
|
||||||
}
|
}
|
||||||
@@ -114,19 +125,6 @@ function handleMenuClick({ key }) {
|
|||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sider-brand h2 {
|
|
||||||
font-size: 22px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #0f172a;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sider-brand p {
|
|
||||||
color: #64748b;
|
|
||||||
line-height: 1.8;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contract-menu {
|
.contract-menu {
|
||||||
border-inline-end: none;
|
border-inline-end: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -169,12 +167,6 @@ function handleMenuClick({ key }) {
|
|||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-desc {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #64748b;
|
|
||||||
line-height: 1.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contract-content {
|
.contract-content {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
|||||||
@@ -19,6 +19,14 @@ const routes = [
|
|||||||
title: '发起合同',
|
title: '发起合同',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'sign',
|
||||||
|
name: 'ContractSignManage',
|
||||||
|
component: () => import('@/views/contract/sign/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '合同签署',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'manifest',
|
path: 'manifest',
|
||||||
name: 'ContractTemplateManage',
|
name: 'ContractTemplateManage',
|
||||||
|
|||||||
@@ -228,7 +228,7 @@
|
|||||||
<a v-if="canMockReject(row)" @click="handleMockReject(row)">{{
|
<a v-if="canMockReject(row)" @click="handleMockReject(row)">{{
|
||||||
isSingleAgreementRecord(row) ? "审批驳回" : "测试驳回"
|
isSingleAgreementRecord(row) ? "审批驳回" : "测试驳回"
|
||||||
}}</a>
|
}}</a>
|
||||||
<a v-if="canOnlineSign(row)" @click="handleOnlineSign(row)">{{
|
<a v-if="SHOW_SIGN_ACTIONS_IN_LAUNCH && canOnlineSign(row)" @click="handleOnlineSign(row)">{{
|
||||||
isSingleAgreementRecord(row) ? "发起盖章" : "在线签订"
|
isSingleAgreementRecord(row) ? "发起盖章" : "在线签订"
|
||||||
}}</a>
|
}}</a>
|
||||||
<a
|
<a
|
||||||
@@ -236,11 +236,11 @@
|
|||||||
@click="handleDownloadSigned(row)"
|
@click="handleDownloadSigned(row)"
|
||||||
>下载签署文件</a
|
>下载签署文件</a
|
||||||
>
|
>
|
||||||
<a v-if="canUploadSigned(row)" @click="handleUploadSigned(row)">{{
|
<a v-if="SHOW_SIGN_ACTIONS_IN_LAUNCH && canUploadSigned(row)" @click="handleUploadSigned(row)">{{
|
||||||
isSingleAgreementRecord(row) ? "上传盖章版" : "上传已签署版本"
|
isSingleAgreementRecord(row) ? "上传盖章版" : "上传已签署版本"
|
||||||
}}</a>
|
}}</a>
|
||||||
<a
|
<a
|
||||||
v-if="canMockFinishSign(row)"
|
v-if="SHOW_SIGN_ACTIONS_IN_LAUNCH && canMockFinishSign(row)"
|
||||||
@click="handleMockFinishSign(row)"
|
@click="handleMockFinishSign(row)"
|
||||||
>{{
|
>{{
|
||||||
isSingleAgreementRecord(row) ? "测试盖章完成" : "测试签署完成"
|
isSingleAgreementRecord(row) ? "测试盖章完成" : "测试签署完成"
|
||||||
@@ -346,6 +346,9 @@ const contractRecordStatusOptions = [
|
|||||||
{ label: "已终止", value: "terminated" },
|
{ label: "已终止", value: "terminated" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 合同签署已拆到独立菜单,这里只隐藏入口,保留原有逻辑方便后续恢复。
|
||||||
|
const SHOW_SIGN_ACTIONS_IN_LAUNCH = false;
|
||||||
|
|
||||||
const formRef = ref(null);
|
const formRef = ref(null);
|
||||||
const queryContainer = ref(null);
|
const queryContainer = ref(null);
|
||||||
const pageWrapperRef = ref(null);
|
const pageWrapperRef = ref(null);
|
||||||
|
|||||||
@@ -0,0 +1,826 @@
|
|||||||
|
<template>
|
||||||
|
<section class="section-container">
|
||||||
|
<a-card
|
||||||
|
:bordered="false"
|
||||||
|
class="query-card"
|
||||||
|
:body-style="queryCardBodyStyle"
|
||||||
|
>
|
||||||
|
<div ref="queryContainer" class="section-query-container">
|
||||||
|
<a-form :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="signStatusOptions"
|
||||||
|
/>
|
||||||
|
</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-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="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="isElectron" title="签署方式" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<a-tag :color="String(row.isElectron || '1') === '1' ? 'processing' : 'default'">
|
||||||
|
{{ String(row.isElectron || '1') === '1' ? '电子合同' : '纸质合同' }}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
</vxe-column>
|
||||||
|
<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="660" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="table-actions">
|
||||||
|
<a @click="handleView(row)">查看</a>
|
||||||
|
<a v-if="canViewApprove(row)" @click="handleViewApprove(row)">审批记录</a>
|
||||||
|
<a v-if="canViewLawCallback(row)" @click="handleViewLawCallback(row)">回调data</a>
|
||||||
|
<a v-if="canOnlineSign(row)" @click="handleOnlineSign(row)">
|
||||||
|
{{ isSingleAgreementRecord(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="canDownloadSigned(row)" @click="handleDownloadSigned(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"
|
||||||
|
@business-updated="handleSignBusinessUpdated"
|
||||||
|
/>
|
||||||
|
<UploadSignedModal
|
||||||
|
ref="uploadSignedModalRef"
|
||||||
|
@done="handleSignBusinessUpdated"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { h, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
|
import { RedoOutlined, SearchOutlined } from '@ant-design/icons-vue'
|
||||||
|
import { message, Modal } from 'ant-design-vue'
|
||||||
|
import ContractPartyPlaceholderModal from '../launch/components/ContractPartyPlaceholderModal.vue'
|
||||||
|
import MixedSignModal from '../launch/components/mixedSignModal.vue'
|
||||||
|
import UploadSignedModal from '../launch/components/UploadSignedModal.vue'
|
||||||
|
import {
|
||||||
|
contractTypeOptions,
|
||||||
|
getContractTypeSimpleLabel,
|
||||||
|
} from '../launch/contractLaunchOptions.js'
|
||||||
|
import {
|
||||||
|
downloadLaunchFile,
|
||||||
|
finishLaunchSeal,
|
||||||
|
getLaunchDetail,
|
||||||
|
getLawApproveCallbackSample,
|
||||||
|
queryApproveViewUrl,
|
||||||
|
queryLaunchList,
|
||||||
|
uploadWatermarkFile,
|
||||||
|
} from '@/api/launch.js'
|
||||||
|
|
||||||
|
const SIGN_STAGE_STATUSES = ['pending_sign', 'signing']
|
||||||
|
const SIGN_LIST_QUERY_PAGE_SIZE = 200
|
||||||
|
const signStatusOptions = [
|
||||||
|
{ label: '待签署', value: 'pending_sign' },
|
||||||
|
{ label: '签署中', value: 'signing' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const contractModalRef = ref(null)
|
||||||
|
const mixedSignModalRef = ref(null)
|
||||||
|
const uploadSignedModalRef = ref(null)
|
||||||
|
const queryContainer = ref(null)
|
||||||
|
const pageWrapperRef = ref(null)
|
||||||
|
const tableRef = ref(null)
|
||||||
|
const toolbarRef = ref(null)
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const total = ref(0)
|
||||||
|
const tableHeight = ref(420)
|
||||||
|
const tableData = ref([])
|
||||||
|
const allRecords = 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 = {
|
||||||
|
pending_sign: '待签署',
|
||||||
|
signing: '签署中',
|
||||||
|
pending_collect: '待采集',
|
||||||
|
pending_archive: '待归档',
|
||||||
|
completed: '已完成',
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusColorMap = {
|
||||||
|
pending_sign: 'warning',
|
||||||
|
signing: 'processing',
|
||||||
|
pending_collect: 'cyan',
|
||||||
|
pending_archive: 'orange',
|
||||||
|
completed: 'success',
|
||||||
|
}
|
||||||
|
|
||||||
|
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 isSingleAgreementRecord(row) {
|
||||||
|
return String(row?.isSingleAgreement || '0') === '1'
|
||||||
|
}
|
||||||
|
|
||||||
|
function canOnlineSign(row) {
|
||||||
|
return (
|
||||||
|
!isSingleAgreementRecord(row) &&
|
||||||
|
String(row.isElectron || '1') === '1' &&
|
||||||
|
SIGN_STAGE_STATUSES.includes(row.status)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function canUploadSigned(row) {
|
||||||
|
return String(row.isElectron || '1') === '0' && SIGN_STAGE_STATUSES.includes(row.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
function canMockFinishSign(row) {
|
||||||
|
if (isSingleAgreementRecord(row)) {
|
||||||
|
return row.status === 'pending_sign'
|
||||||
|
}
|
||||||
|
return String(row.isElectron || '1') === '1' && SIGN_STAGE_STATUSES.includes(row.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
function canDownloadSigned(row) {
|
||||||
|
return Boolean(row.signedFilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
function canViewApprove(row) {
|
||||||
|
return !isSingleAgreementRecord(row) && Boolean(row.lawContractId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function canViewLawCallback(row) {
|
||||||
|
return !isSingleAgreementRecord(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildQueryPayload(status, pageNo = 1, pageSize = SIGN_LIST_QUERY_PAGE_SIZE) {
|
||||||
|
return {
|
||||||
|
pageNo,
|
||||||
|
pageSize,
|
||||||
|
contractName: queryParam.value.contractName,
|
||||||
|
contractCode: queryParam.value.contractCode,
|
||||||
|
contractType: queryParam.value.contractType,
|
||||||
|
status,
|
||||||
|
dateStart: queryParam.value.dateRange?.[0],
|
||||||
|
dateEnd: queryParam.value.dateRange?.[1],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchRecordsByStatus(status) {
|
||||||
|
let pageNo = 1
|
||||||
|
let totalCount = 0
|
||||||
|
const records = []
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const res = await queryLaunchList(buildQueryPayload(status, pageNo))
|
||||||
|
if (!isSuccessResponse(res)) {
|
||||||
|
throw new Error(getResponseMessage(res, '合同签署列表查询失败'))
|
||||||
|
}
|
||||||
|
const pageResult = getPageResult(res)
|
||||||
|
const data = Array.isArray(pageResult?.data) ? pageResult.data : []
|
||||||
|
totalCount = Number(pageResult?.totalSize ?? pageResult?.total ?? 0)
|
||||||
|
records.push(...data)
|
||||||
|
if (!data.length) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (totalCount > 0 && records.length >= totalCount) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (data.length < SIGN_LIST_QUERY_PAGE_SIZE) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
pageNo += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return records
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareRecord(a, b) {
|
||||||
|
const aTime = new Date(a?.createTime || a?.updateTime || 0).getTime()
|
||||||
|
const bTime = new Date(b?.createTime || b?.updateTime || 0).getTime()
|
||||||
|
if (aTime !== bTime) {
|
||||||
|
return bTime - aTime
|
||||||
|
}
|
||||||
|
return Number(b?.id || 0) - Number(a?.id || 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshTableData() {
|
||||||
|
total.value = allRecords.value.length
|
||||||
|
const pageNo = Number(queryParam.value.pageNo || 1)
|
||||||
|
const pageSize = Number(queryParam.value.pageSize || 10)
|
||||||
|
const startIndex = (pageNo - 1) * pageSize
|
||||||
|
tableData.value = allRecords.value.slice(startIndex, startIndex + pageSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getList() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const statuses = queryParam.value.status ? [queryParam.value.status] : SIGN_STAGE_STATUSES
|
||||||
|
const mergedMap = new Map()
|
||||||
|
for (const status of statuses) {
|
||||||
|
const records = await fetchRecordsByStatus(status)
|
||||||
|
records.forEach((item) => {
|
||||||
|
if (!mergedMap.has(item.id)) {
|
||||||
|
mergedMap.set(item.id, item)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
allRecords.value = Array.from(mergedMap.values()).sort(compareRecord)
|
||||||
|
refreshTableData()
|
||||||
|
} catch (error) {
|
||||||
|
tableData.value = []
|
||||||
|
allRecords.value = []
|
||||||
|
total.value = 0
|
||||||
|
message.error(error?.message || '合同签署列表查询失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleQuery() {
|
||||||
|
queryParam.value.pageNo = 1
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReset() {
|
||||||
|
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
|
||||||
|
refreshTableData()
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 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(async (res) => {
|
||||||
|
if (!isSuccessResponse(res) || !getDataResult(res)) {
|
||||||
|
message.error(getResponseMessage(res, '获取合同详情失败'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const detail = getDataResult(res)
|
||||||
|
if (!detail.contractTextFilePath) {
|
||||||
|
await handleWatermarkUpload(row)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mixedSignModalRef.value?.showModal({
|
||||||
|
...detail,
|
||||||
|
contractId: detail.sealContractId,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleWatermarkUpload(row) {
|
||||||
|
let file
|
||||||
|
const ok = await new Promise((resolve) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '法务水印文件缺失',
|
||||||
|
content: h('div', [
|
||||||
|
h(
|
||||||
|
'p',
|
||||||
|
{ style: 'margin-bottom: 12px' },
|
||||||
|
'法务审批回调未带回带水印的合同文件,请手动上传带水印的合同文件后继续在线签订。',
|
||||||
|
),
|
||||||
|
h('input', {
|
||||||
|
type: 'file',
|
||||||
|
accept: '.pdf,.doc,.docx',
|
||||||
|
style: 'margin-top: 8px; width: 100%',
|
||||||
|
onChange(e) {
|
||||||
|
file = e.target.files?.[0]
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
okText: '上传并继续',
|
||||||
|
cancelText: '取消',
|
||||||
|
async onOk() {
|
||||||
|
if (!file) {
|
||||||
|
message.warning('请先选择文件')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
try {
|
||||||
|
const res = await uploadWatermarkFile(row.id, formData)
|
||||||
|
if (!isSuccessResponse(res)) {
|
||||||
|
message.error(getResponseMessage(res, '上传水印文件失败'))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
message.success('水印文件上传成功')
|
||||||
|
resolve(true)
|
||||||
|
} catch {
|
||||||
|
message.error('上传水印文件失败')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
resolve(false)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if (!ok) return
|
||||||
|
getLaunchDetail(row.id).then((res) => {
|
||||||
|
if (!isSuccessResponse(res) || !getDataResult(res)) return
|
||||||
|
const detail = getDataResult(res)
|
||||||
|
mixedSignModalRef.value?.showModal({
|
||||||
|
...detail,
|
||||||
|
contractId: detail.sealContractId,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSignBusinessUpdated() {
|
||||||
|
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>
|
||||||
Reference in New Issue
Block a user