首次提交
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
/**
|
||||
* 合同发起模块 - API 层
|
||||
*
|
||||
* 当前使用 mock 数据模拟接口返回。
|
||||
* 【接入后端】:直接替换每个函数内部实现为 axios 请求即可,函数签名不改。
|
||||
*/
|
||||
|
||||
// ========== 合同发起列表 mock 数据 ==========
|
||||
|
||||
const mockLaunchRecords = [
|
||||
{
|
||||
id: 'INIT-202605-001',
|
||||
// 合同编码
|
||||
contractCode: 'HT-2026-0008',
|
||||
// 合同名称
|
||||
contractName: '智慧园区设备采购合同',
|
||||
// 合同类型
|
||||
contractType: '采购合同',
|
||||
// 合同金额(元)
|
||||
contractAmount: 368000,
|
||||
// 有效期起 YYYY-MM-DD
|
||||
effectiveStart: '2026-05-10',
|
||||
// 有效期止 YYYY-MM-DD
|
||||
effectiveEnd: '2027-05-09',
|
||||
// 付款方式
|
||||
paymentMethod: '银行转账(30%预付款,70%验收后支付)',
|
||||
// 所选模板标识
|
||||
templateKey: 'purchase',
|
||||
// 所选模板名称
|
||||
templateName: '采购合同模板',
|
||||
// 合同标的
|
||||
contractSubject: '园区门禁一体机、访客终端及部署实施服务',
|
||||
// 业务备注
|
||||
businessRemark: '首次发起,等待业务确认后提交法务。',
|
||||
// 发起人
|
||||
initiator: '张晓明',
|
||||
// 所属部门
|
||||
initiatorDept: '供应链运营部',
|
||||
// 发起人公司
|
||||
companyName: '浙港物流平台有限公司',
|
||||
// 状态: draft/reviewing/rejected/pending_sign/signing/completed
|
||||
status: 'draft',
|
||||
// 查询级别: personal/company/admin
|
||||
queryScope: 'personal',
|
||||
// 创建时间
|
||||
createTime: '2026-05-10 09:20:00',
|
||||
// 更新时间
|
||||
updateTime: '2026-05-13 14:30:00',
|
||||
// 附件列表
|
||||
attachmentList: [
|
||||
{ uid: 'a1', name: '技术方案V1.pdf', size: 283011, status: 'done' },
|
||||
{ uid: 'a2', name: '补充协议草案.docx', size: 125880, status: 'done' },
|
||||
],
|
||||
// 合同方信息(详见下方 partyState 结构)
|
||||
partyState: {
|
||||
isThreePartyContract: false,
|
||||
partyA: {
|
||||
companyName: '浙港物流平台有限公司',
|
||||
contactPhone: '',
|
||||
registeredAddress: '',
|
||||
bankAccount: '',
|
||||
bankName: '',
|
||||
contactName: '',
|
||||
legalRepresentative: '',
|
||||
creditCode: '',
|
||||
},
|
||||
partyB: {
|
||||
companyName: '宁波港船务货运代理有限公司',
|
||||
companyType: '供应商',
|
||||
contactPhone: '15757460062',
|
||||
registeredAddress: '',
|
||||
bankAccount: '',
|
||||
bankName: '',
|
||||
contactName: '周经理',
|
||||
legalRepresentative: '',
|
||||
creditCode: '',
|
||||
},
|
||||
partyC: { companyName: '' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'INIT-202605-002',
|
||||
contractCode: 'XS-2026-0015',
|
||||
contractName: '保税燃料油销售合同',
|
||||
contractType: '销售合同',
|
||||
contractAmount: 1580000,
|
||||
effectiveStart: '2026-05-01',
|
||||
effectiveEnd: '2026-12-31',
|
||||
paymentMethod: '银行转账(按月结算)',
|
||||
templateKey: 'sales',
|
||||
templateName: '销售合同模板',
|
||||
contractSubject: '保税燃料油销售及船舶供油服务',
|
||||
businessRemark: '已提交法务,等待审批中。',
|
||||
initiator: '王倩',
|
||||
initiatorDept: '国际贸易事业部',
|
||||
companyName: '浙港物流平台有限公司',
|
||||
status: 'reviewing',
|
||||
queryScope: 'company',
|
||||
createTime: '2026-05-08 16:15:00',
|
||||
updateTime: '2026-05-14 08:30:00',
|
||||
attachmentList: [
|
||||
{ uid: 'a3', name: '报价确认函.pdf', size: 90211, status: 'done' },
|
||||
],
|
||||
partyState: {
|
||||
isThreePartyContract: false,
|
||||
partyA: {
|
||||
companyName: '宁波浙港供应链科技有限公司',
|
||||
contactPhone: '13900000088',
|
||||
contactName: '陈经理',
|
||||
},
|
||||
partyB: {
|
||||
companyName: '上海远洋能源有限公司',
|
||||
companyType: '客户',
|
||||
legalRepresentative: '孙先生',
|
||||
contactName: '吴经理',
|
||||
contactPhone: '13600000015',
|
||||
contactEmail: 'sales@oceanenergy.com',
|
||||
bankName: '招商银行上海分行',
|
||||
bankAccount: '621483000000000015',
|
||||
},
|
||||
partyC: { companyName: '' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'INIT-202605-003',
|
||||
contractCode: 'FW-2026-0021',
|
||||
contractName: '数字化平台运维服务合同',
|
||||
contractType: '服务合同',
|
||||
contractAmount: 520000,
|
||||
effectiveStart: '2026-04-15',
|
||||
effectiveEnd: '2027-04-14',
|
||||
paymentMethod: '银行转账(一次性结清)',
|
||||
templateKey: 'purchase',
|
||||
templateName: '采购合同模板',
|
||||
contractSubject: '合同管理板块运维与驻场实施服务',
|
||||
businessRemark: '法务要求补充服务SLA附件后重新提交。',
|
||||
initiator: '李娜',
|
||||
initiatorDept: '数字化办公室',
|
||||
companyName: '浙港物流平台有限公司',
|
||||
status: 'rejected',
|
||||
queryScope: 'company',
|
||||
createTime: '2026-04-15 10:25:00',
|
||||
updateTime: '2026-05-07 18:05:00',
|
||||
attachmentList: [
|
||||
{ uid: 'a4', name: '实施范围说明.pdf', size: 70123, status: 'done' },
|
||||
],
|
||||
partyState: {
|
||||
isThreePartyContract: false,
|
||||
partyA: { companyName: '浙港物流平台有限公司' },
|
||||
partyB: {
|
||||
companyName: '杭州智云实施服务有限公司',
|
||||
companyType: '供应商',
|
||||
contactName: '刘经理',
|
||||
contactPhone: '13888886666',
|
||||
contactEmail: 'liu@cloudservice.com',
|
||||
},
|
||||
partyC: { companyName: '' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'INIT-202605-004',
|
||||
contractCode: 'KJ-2026-0011',
|
||||
contractName: '港口智能设备年度框架合同',
|
||||
contractType: '框架合同',
|
||||
contractAmount: 980000,
|
||||
effectiveStart: '2026-03-01',
|
||||
effectiveEnd: '2027-02-28',
|
||||
paymentMethod: '银行转账(按月结算)',
|
||||
templateKey: 'purchase',
|
||||
templateName: '采购合同模板',
|
||||
contractSubject: '港口智能硬件、集成服务及年度维保合作',
|
||||
businessRemark: '审批完成,待发起线上签署。',
|
||||
initiator: '周立',
|
||||
initiatorDept: '装备采购中心',
|
||||
companyName: '浙港物流平台有限公司',
|
||||
status: 'pending_sign',
|
||||
queryScope: 'admin',
|
||||
createTime: '2026-03-01 11:12:00',
|
||||
updateTime: '2026-05-11 09:45:00',
|
||||
attachmentList: [
|
||||
{ uid: 'a5', name: '年度采购清单.xlsx', size: 154933, status: 'done' },
|
||||
],
|
||||
partyState: {
|
||||
isThreePartyContract: true,
|
||||
partyA: { companyName: '浙港物流平台有限公司' },
|
||||
partyB: {
|
||||
companyName: '宁波保税区港航设备有限公司',
|
||||
companyType: '合作伙伴',
|
||||
contactName: '赵主管',
|
||||
contactPhone: '13500000021',
|
||||
},
|
||||
partyC: {
|
||||
companyName: '浙江临港数智科技有限公司',
|
||||
companyType: '合作伙伴',
|
||||
creditCode: '91330000MAABCDE123',
|
||||
legalRepresentative: '钱先生',
|
||||
registeredAddress: '浙江省杭州市滨江区江南大道2888号',
|
||||
contactName: '何经理',
|
||||
contactPhone: '13700000078',
|
||||
contactAddress: '浙江省杭州市滨江区江南大道2888号',
|
||||
contactEmail: 'hz@example.com',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'INIT-202605-005',
|
||||
contractCode: 'XS-2026-0022',
|
||||
contractName: '沿海航线供应服务合同',
|
||||
contractType: '销售合同',
|
||||
contractAmount: 760000,
|
||||
effectiveStart: '2026-02-18',
|
||||
effectiveEnd: '2026-11-30',
|
||||
paymentMethod: '银行转账(按月结算)',
|
||||
templateKey: 'sales',
|
||||
templateName: '销售合同模板',
|
||||
contractSubject: '沿海航线供应链服务与结算支持',
|
||||
businessRemark: '甲方已签章,等待乙方签署。',
|
||||
initiator: '徐晨',
|
||||
initiatorDept: '客户服务中心',
|
||||
companyName: '浙港物流平台有限公司',
|
||||
status: 'signing',
|
||||
queryScope: 'personal',
|
||||
createTime: '2026-02-18 13:40:00',
|
||||
updateTime: '2026-05-12 17:16:00',
|
||||
attachmentList: [],
|
||||
partyState: {
|
||||
isThreePartyContract: false,
|
||||
partyA: {
|
||||
companyName: '宁波浙港供应链科技有限公司',
|
||||
contactPhone: '13900000088',
|
||||
contactName: '陈经理',
|
||||
},
|
||||
partyB: {
|
||||
companyName: '上海远洋能源有限公司',
|
||||
companyType: '客户',
|
||||
legalRepresentative: '孙先生',
|
||||
contactName: '吴经理',
|
||||
contactPhone: '13600000015',
|
||||
contactEmail: 'sales@oceanenergy.com',
|
||||
bankName: '招商银行上海分行',
|
||||
bankAccount: '621483000000000015',
|
||||
},
|
||||
partyC: { companyName: '' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'INIT-202605-006',
|
||||
contractCode: 'CG-2026-0033',
|
||||
contractName: '港区安防升级采购合同',
|
||||
contractType: '采购合同',
|
||||
contractAmount: 2860000,
|
||||
effectiveStart: '2026-01-10',
|
||||
effectiveEnd: '2026-12-31',
|
||||
paymentMethod: '银行转账(30%预付款,70%验收后支付)',
|
||||
templateKey: 'purchase',
|
||||
templateName: '采购合同模板',
|
||||
contractSubject: '港区安防摄像头、门禁和施工实施采购',
|
||||
businessRemark: '合同已签署完毕并推送法务归档。',
|
||||
initiator: '高翔',
|
||||
initiatorDept: '工程保障部',
|
||||
companyName: '浙港物流平台有限公司',
|
||||
status: 'completed',
|
||||
queryScope: 'admin',
|
||||
createTime: '2026-01-10 08:45:00',
|
||||
updateTime: '2026-05-09 16:28:00',
|
||||
attachmentList: [
|
||||
{ uid: 'a6', name: '验收报告.pdf', size: 188221, status: 'done' },
|
||||
],
|
||||
partyState: {
|
||||
isThreePartyContract: false,
|
||||
partyA: { companyName: '浙港物流平台有限公司' },
|
||||
partyB: {
|
||||
companyName: '宁波港船务货运代理有限公司',
|
||||
companyType: '供应商',
|
||||
contactPhone: '15757460062',
|
||||
contactName: '周经理',
|
||||
},
|
||||
partyC: { companyName: '' },
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
// ========== API 函数(接入后端时替换内部实现) ==========
|
||||
|
||||
/** 查询合同发起列表 */
|
||||
export function queryLaunchList() {
|
||||
return Promise.resolve(mockLaunchRecords)
|
||||
}
|
||||
|
||||
/** 查询合同发起详情 */
|
||||
export function getLaunchDetail(id) {
|
||||
const record = mockLaunchRecords.find((item) => item.id === id) || null
|
||||
return Promise.resolve(record)
|
||||
}
|
||||
|
||||
/** 保存合同发起 */
|
||||
export function saveLaunchRecord(record) {
|
||||
return Promise.resolve(record)
|
||||
}
|
||||
|
||||
/** 提交合同到法务 */
|
||||
export function submitLaunchRecord(record) {
|
||||
return Promise.resolve(record)
|
||||
}
|
||||
|
||||
/** 删除合同发起记录 */
|
||||
export function deleteLaunchRecord(idList) {
|
||||
return Promise.resolve(idList)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<BaseMixedSignModal ref="innerRef" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import BaseMixedSignModal from '../../../seal/mixedSignModal.vue'
|
||||
|
||||
const innerRef = ref(null)
|
||||
|
||||
function showModal(...args) {
|
||||
return innerRef.value?.showModal(...args)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showModal,
|
||||
})
|
||||
</script>
|
||||
@@ -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>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,422 @@
|
||||
<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="8" :sm="24">
|
||||
<a-form-item label="模板名称">
|
||||
<a-input
|
||||
v-model:value="queryParam.templateName"
|
||||
allow-clear
|
||||
placeholder="请输入模板名称"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<a-col :md="8" :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="8" :sm="24">
|
||||
<a-form-item label="模板状态">
|
||||
<a-select
|
||||
v-model:value="queryParam.status"
|
||||
allow-clear
|
||||
placeholder="请选择模板状态"
|
||||
:options="templateStatusOptions"
|
||||
/>
|
||||
</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-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="templateCode" title="模板编码" width="150" show-overflow="title" />
|
||||
<vxe-column field="templateName" title="模板名称" width="180" show-overflow="title" />
|
||||
<vxe-column field="contractType" title="合同类型" width="120" show-overflow="title" />
|
||||
<vxe-column field="source" title="模板来源" width="110" show-overflow="title" />
|
||||
<vxe-column field="version" title="版本" width="90" />
|
||||
<vxe-column field="status" title="状态" width="110">
|
||||
<template #default="{ row }">
|
||||
<a-tag :color="getStatusColor(row.status)">
|
||||
{{ getStatusLabel(row.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="variableCount" title="变量数" width="90" />
|
||||
<vxe-column field="publishScopeText" title="发布范围" width="220" show-overflow="title" />
|
||||
<vxe-column field="templateFileName" title="模板文件" width="220" show-overflow="title" />
|
||||
<vxe-column field="updatedBy" title="最后修改人" width="120" show-overflow="title" />
|
||||
<vxe-column field="updatedTime" title="最后修改时间" width="170" show-overflow="title" />
|
||||
<vxe-column field="description" title="说明" width="260" show-overflow="title" />
|
||||
<vxe-column title="操作" width="220" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="table-actions">
|
||||
<a @click="handleView(row)">查看</a>
|
||||
<a @click="handleEdit(row)">编辑</a>
|
||||
<a @click="toggleStatus(row)">
|
||||
{{ row.status === 'published' ? '停用' : '发布' }}
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
</vxe-column>
|
||||
</vxe-table>
|
||||
|
||||
<a-pagination
|
||||
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>
|
||||
|
||||
<ContractTemplateManageModal
|
||||
ref="templateModalRef"
|
||||
@submit="handleTemplateSubmit"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { h, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { PlusOutlined, RedoOutlined, SearchOutlined } from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import ContractTemplateManageModal from './components/ContractTemplateManageModal.vue'
|
||||
import {
|
||||
changeManifestStatus,
|
||||
getManifestDetail,
|
||||
queryManifestList,
|
||||
saveManifest,
|
||||
} from '@/api/manifest.js'
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 页面内字典(后续可由后端接口替换)
|
||||
const contractTypeOptions = [
|
||||
{ label: '买卖合同', value: '买卖合同' },
|
||||
{ label: '采购合同', value: '采购合同' },
|
||||
{ label: '销售合同', value: '销售合同' },
|
||||
{ label: '服务合同', value: '服务合同' },
|
||||
{ label: '框架合同', value: '框架合同' },
|
||||
{ label: '供用电、水、气、热力合同', value: '供用电、水、气、热力合同' },
|
||||
{ label: '赠与合同', value: '赠与合同' },
|
||||
{ label: '借款合同', value: '借款合同' },
|
||||
{ label: '保证合同', value: '保证合同' },
|
||||
{ label: '租赁合同', value: '租赁合同' },
|
||||
{ label: '融资租赁合同', value: '融资租赁合同' },
|
||||
{ label: '保理合同', value: '保理合同' },
|
||||
{ label: '承揽合同', value: '承揽合同' },
|
||||
{ label: '建设工程合同', value: '建设工程合同' },
|
||||
{ label: '运输合同', value: '运输合同' },
|
||||
{ label: '技术合同', value: '技术合同' },
|
||||
{ label: '保管合同', value: '保管合同' },
|
||||
{ label: '仓储合同', value: '仓储合同' },
|
||||
{ label: '委托合同', value: '委托合同' },
|
||||
{ label: '物业服务合同', value: '物业服务合同' },
|
||||
{ label: '行纪合同', value: '行纪合同' },
|
||||
{ label: '中介合同', value: '中介合同' },
|
||||
{ label: '合伙合同', value: '合伙合同' },
|
||||
{ label: '用工合同', value: '用工合同' },
|
||||
{ label: '业务合同', value: '业务合同' },
|
||||
{ label: '其他合同', value: '其他合同' },
|
||||
]
|
||||
|
||||
const templateStatusOptions = [
|
||||
{ label: '已发布', value: 'published' },
|
||||
{ label: '草稿', value: 'draft' },
|
||||
{ label: '已停用', value: 'disabled' },
|
||||
]
|
||||
|
||||
const formRef = ref(null)
|
||||
const queryContainer = ref(null)
|
||||
const pageWrapperRef = ref(null)
|
||||
const tableRef = ref(null)
|
||||
const toolbarRef = ref(null)
|
||||
const templateModalRef = ref(null)
|
||||
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const tableHeight = ref(420)
|
||||
const tableData = ref([])
|
||||
|
||||
const queryParam = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
templateName: undefined,
|
||||
contractType: undefined,
|
||||
status: undefined,
|
||||
})
|
||||
|
||||
const queryCardBodyStyle = {
|
||||
padding: '20px 24px 6px',
|
||||
}
|
||||
|
||||
const tableCardBodyStyle = {
|
||||
display: 'flex',
|
||||
flexFlow: 'column nowrap',
|
||||
minHeight: '0',
|
||||
padding: '8px 12px 6px',
|
||||
}
|
||||
|
||||
const templateStatusLabelMap = {
|
||||
published: '已发布',
|
||||
draft: '草稿',
|
||||
disabled: '已停用',
|
||||
}
|
||||
|
||||
const templateStatusColorMap = {
|
||||
published: 'success',
|
||||
draft: 'gold',
|
||||
disabled: 'default',
|
||||
}
|
||||
|
||||
function connectToolbar() {
|
||||
const table = tableRef.value
|
||||
const toolbar = toolbarRef.value
|
||||
|
||||
if (table && toolbar && table.connect) {
|
||||
table.connect(toolbar)
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusLabel(status) {
|
||||
return templateStatusLabelMap[status] || status
|
||||
}
|
||||
|
||||
function getStatusColor(status) {
|
||||
return templateStatusColorMap[status] || 'default'
|
||||
}
|
||||
|
||||
function getList() {
|
||||
loading.value = true
|
||||
|
||||
queryManifestList(queryParam.value)
|
||||
.then((res) => {
|
||||
if (!isSuccessResponse(res)) {
|
||||
tableData.value = []
|
||||
total.value = 0
|
||||
message.error(getResponseMessage(res, '模板列表查询失败'))
|
||||
return
|
||||
}
|
||||
const pageResult = getPageResult(res)
|
||||
tableData.value = pageResult.records || []
|
||||
total.value = pageResult.total || 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,
|
||||
templateName: undefined,
|
||||
contractType: undefined,
|
||||
status: undefined,
|
||||
})
|
||||
getList()
|
||||
}
|
||||
|
||||
function onShowSizeChange(current, pageSize) {
|
||||
queryParam.value.pageNo = current
|
||||
queryParam.value.pageSize = pageSize
|
||||
getList()
|
||||
}
|
||||
|
||||
function handleCreate() {
|
||||
templateModalRef.value?.showModal({}, 'create')
|
||||
}
|
||||
|
||||
function handleEdit(row) {
|
||||
getManifestDetail(row.id).then((res) => {
|
||||
if (!isSuccessResponse(res) || !getDataResult(res)) {
|
||||
message.error(getResponseMessage(res, '获取模板详情失败'))
|
||||
return
|
||||
}
|
||||
templateModalRef.value?.showModal(getDataResult(res), 'edit')
|
||||
})
|
||||
}
|
||||
|
||||
function handleView(row) {
|
||||
getManifestDetail(row.id).then((res) => {
|
||||
if (!isSuccessResponse(res) || !getDataResult(res)) {
|
||||
message.error(getResponseMessage(res, '获取模板详情失败'))
|
||||
return
|
||||
}
|
||||
templateModalRef.value?.showModal(getDataResult(res), 'view')
|
||||
})
|
||||
}
|
||||
|
||||
function handleTemplateSubmit({ mode, record, successMessage }) {
|
||||
saveManifest(record, mode).then((res) => {
|
||||
if (!isSuccessResponse(res)) {
|
||||
message.error(getResponseMessage(res, '模板保存失败'))
|
||||
return
|
||||
}
|
||||
message.success(successMessage || '保存成功')
|
||||
getList()
|
||||
})
|
||||
}
|
||||
|
||||
function toggleStatus(row) {
|
||||
const nextStatus = row.status === 'published' ? 'disabled' : 'published'
|
||||
changeManifestStatus(row.id, nextStatus).then((res) => {
|
||||
if (!isSuccessResponse(res)) {
|
||||
message.error(getResponseMessage(res, '状态修改失败'))
|
||||
return
|
||||
}
|
||||
message.success(`模板已${nextStatus === 'disabled' ? '停用' : '发布'}:${row.templateName}`)
|
||||
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;
|
||||
}
|
||||
|
||||
.table-pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 12px 4px 2px;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.section-container {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,628 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:open="visible"
|
||||
:title="modalTitle"
|
||||
width="1100px"
|
||||
:mask-closable="false"
|
||||
:footer="null"
|
||||
:body-style="{
|
||||
padding: 0,
|
||||
maxHeight: 'calc(100vh - 180px)',
|
||||
overflow: 'hidden',
|
||||
}"
|
||||
centered
|
||||
>
|
||||
<div class="partner-modal-layout">
|
||||
<a-form layout="vertical" :disabled="isViewMode" class="partner-form">
|
||||
<a-card size="small" title="基础信息" class="section-card">
|
||||
<a-alert
|
||||
type="info"
|
||||
show-icon
|
||||
class="section-alert"
|
||||
message="填写说明"
|
||||
description="境内组织需要填写统一社会信用代码、法定代表人、注册地址、注册资本;境外组织需要填写 TIN 号、国家、注册地址、注册资本;组织类型联系人必填。"
|
||||
/>
|
||||
<div class="grid-two">
|
||||
<a-form-item label="相对方编码">
|
||||
<a-input :value="formState.oppositeCode || '保存后自动生成'" readonly />
|
||||
</a-form-item>
|
||||
<a-form-item label="法务相对方ID">
|
||||
<a-input
|
||||
:value="formState.oppositeId || '未同步前为空'"
|
||||
readonly
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="公司名称" required>
|
||||
<a-input
|
||||
v-model:value="formState.customerNameC"
|
||||
placeholder="请输入公司名称"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="公司类型" required>
|
||||
<a-select
|
||||
v-model:value="formState.relativeType"
|
||||
:options="optionSets.relativeTypeOptions"
|
||||
placeholder="请选择公司类型"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="公司性质" required>
|
||||
<a-select
|
||||
v-model:value="formState.oppositeCharacter"
|
||||
:options="optionSets.oppositeCharacterOptions"
|
||||
placeholder="请选择公司性质"
|
||||
@change="handleOppositeCharacterChange"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="组织性质" required>
|
||||
<a-select
|
||||
v-model:value="formState.natureOfEnterpriseOrg"
|
||||
:options="optionSets.natureOfEnterpriseOrgOptions"
|
||||
placeholder="请选择组织性质"
|
||||
@change="handleNatureOfEnterpriseOrgChange"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
v-if="showEnterpriseNatureLev"
|
||||
label="组织性质企业二级"
|
||||
required
|
||||
>
|
||||
<a-select
|
||||
v-model:value="formState.natureOfEnterpriseOrgLev"
|
||||
:options="optionSets.natureOfEnterpriseOrgLevOptions"
|
||||
placeholder="请选择组织性质企业二级"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
v-if="showGatFlag"
|
||||
label="是否涉港澳台"
|
||||
>
|
||||
<a-radio-group v-model:value="formState.gatFlag">
|
||||
<a-radio value="Y">是</a-radio>
|
||||
<a-radio value="N">否</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="是否集团内" required>
|
||||
<a-radio-group v-model:value="formState.groupFlag">
|
||||
<a-radio value="Y">是</a-radio>
|
||||
<a-radio value="N">否</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
v-if="showCustomerTaxno"
|
||||
label="统一社会信用代码"
|
||||
required
|
||||
>
|
||||
<a-input
|
||||
v-model:value="formState.customerTaxno"
|
||||
placeholder="请输入统一社会信用代码"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
v-if="showCertificateFields"
|
||||
label="自然人证件类型"
|
||||
:required="isDomesticPerson"
|
||||
>
|
||||
<a-select
|
||||
v-model:value="formState.certificateType"
|
||||
:options="optionSets.certificateTypeOptions"
|
||||
placeholder="请选择证件类型"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
v-if="showCertificateFields"
|
||||
label="自然人证件号码"
|
||||
:required="isDomesticPerson"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="formState.certificateNum"
|
||||
placeholder="请输入证件号码"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="showTinCode" label="TIN号" required>
|
||||
<a-input v-model:value="formState.tinCode" placeholder="请输入TIN号" />
|
||||
</a-form-item>
|
||||
<a-form-item v-if="showCountry" label="国家" required>
|
||||
<a-select
|
||||
v-model:value="formState.country"
|
||||
:options="optionSets.countryOptions"
|
||||
placeholder="请选择国家"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
v-if="showLegalPersonName"
|
||||
label="法定代表人或负责人"
|
||||
required
|
||||
>
|
||||
<a-input
|
||||
v-model:value="formState.legalPersonName"
|
||||
placeholder="请输入法定代表人或负责人"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
v-if="showCustomerRegaddr"
|
||||
class="field-span-2"
|
||||
label="注册地址"
|
||||
required
|
||||
>
|
||||
<a-input
|
||||
v-model:value="formState.customerRegaddr"
|
||||
placeholder="请输入注册地址"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
v-if="showRegCapital"
|
||||
label="注册资本"
|
||||
required
|
||||
>
|
||||
<a-input
|
||||
v-model:value="formState.regCapital"
|
||||
placeholder="请输入注册资本"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="邮箱地址">
|
||||
<a-input
|
||||
v-model:value="formState.emailAddress"
|
||||
placeholder="请输入邮箱地址"
|
||||
/>
|
||||
</a-form-item>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<a-card size="small" class="section-card">
|
||||
<template #title>
|
||||
<div class="card-title-row">
|
||||
<span>联系人信息</span>
|
||||
<a-tag v-if="needContacts" color="orange">组织类型必填</a-tag>
|
||||
<a-tag v-else color="default">个人类型可选</a-tag>
|
||||
</div>
|
||||
</template>
|
||||
<div class="contact-toolbar" v-if="!isViewMode">
|
||||
<a-button type="dashed" @click="addContact">新增联系人</a-button>
|
||||
</div>
|
||||
<div v-if="!formState.contacts.length" class="empty-contact">
|
||||
暂无联系人信息
|
||||
</div>
|
||||
<div
|
||||
v-for="(contact, index) in formState.contacts"
|
||||
:key="contact.uid"
|
||||
class="contact-card"
|
||||
>
|
||||
<div class="contact-card-header">
|
||||
<strong>联系人{{ index + 1 }}</strong>
|
||||
<a-button
|
||||
v-if="!isViewMode"
|
||||
type="link"
|
||||
danger
|
||||
@click="removeContact(contact.uid)"
|
||||
>
|
||||
删除
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="grid-two">
|
||||
<a-form-item label="姓名" :required="needContacts">
|
||||
<a-input v-model:value="contact.contactName" placeholder="请输入姓名" />
|
||||
</a-form-item>
|
||||
<a-form-item label="联系电话" :required="needContacts">
|
||||
<a-input
|
||||
v-model:value="contact.contactPhone"
|
||||
placeholder="请输入联系电话"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item class="field-span-2" label="通讯地址" :required="needContacts">
|
||||
<a-input
|
||||
v-model:value="contact.postalAddress"
|
||||
placeholder="请输入通讯地址"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item class="field-span-2" label="电子邮箱" :required="needContacts">
|
||||
<a-input
|
||||
v-model:value="contact.email"
|
||||
placeholder="请输入电子邮箱"
|
||||
/>
|
||||
</a-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<a-card size="small" title="同步信息" class="section-card">
|
||||
<div class="grid-two">
|
||||
<a-form-item label="同步状态">
|
||||
<a-input :value="syncStatusLabel" readonly />
|
||||
</a-form-item>
|
||||
<a-form-item label="创建时间">
|
||||
<a-input :value="formState.createTime || '-'" readonly />
|
||||
</a-form-item>
|
||||
<a-form-item label="同步时间">
|
||||
<a-input :value="formState.syncTime || '-'" readonly />
|
||||
</a-form-item>
|
||||
<a-form-item label="同步说明">
|
||||
<a-input :value="formState.syncMessage || '-'" readonly />
|
||||
</a-form-item>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-form>
|
||||
|
||||
<div class="modal-footer">
|
||||
<a-space>
|
||||
<a-button v-if="!isViewMode" @click="resetForm">重置</a-button>
|
||||
<a-button @click="visible = false">{{ isViewMode ? "关闭" : "取消" }}</a-button>
|
||||
<a-button v-if="!isViewMode" type="primary" @click="handleSave">
|
||||
保存
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { message } from "ant-design-vue";
|
||||
|
||||
const props = defineProps({
|
||||
optionSets: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["save"]);
|
||||
|
||||
function createContact() {
|
||||
return {
|
||||
uid: `contact_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`,
|
||||
id: null,
|
||||
partnerRecordId: null,
|
||||
contactName: "",
|
||||
contactPhone: "",
|
||||
postalAddress: "",
|
||||
email: "",
|
||||
sortOrder: 1,
|
||||
delFlag: "0",
|
||||
remark: "",
|
||||
};
|
||||
}
|
||||
|
||||
function createFormData() {
|
||||
return {
|
||||
customerSysid: null,
|
||||
oppositeCode: "",
|
||||
oppositeId: "",
|
||||
customerNameC: "",
|
||||
relativeType: undefined,
|
||||
oppositeCharacter: undefined,
|
||||
natureOfEnterpriseOrg: undefined,
|
||||
natureOfEnterpriseOrgLev: undefined,
|
||||
gatFlag: "N",
|
||||
groupFlag: "N",
|
||||
customerTaxno: "",
|
||||
certificateType: undefined,
|
||||
certificateNum: "",
|
||||
tinCode: "",
|
||||
country: undefined,
|
||||
countryName: "",
|
||||
legalPersonName: "",
|
||||
customerRegaddr: "",
|
||||
registeredCapital: undefined,
|
||||
regCapital: "",
|
||||
oppositeStatus: "EFFECTUAL",
|
||||
isSgat: "0",
|
||||
isJtn: "0",
|
||||
isDeleted: "N",
|
||||
comments: "",
|
||||
remark: "",
|
||||
emailAddress: "",
|
||||
contacts: [],
|
||||
syncStatus: "unsynced",
|
||||
createTime: "",
|
||||
syncTime: "",
|
||||
syncMessage: "",
|
||||
};
|
||||
}
|
||||
|
||||
function copyFormData(data = {}) {
|
||||
const source = data || {};
|
||||
return {
|
||||
...createFormData(),
|
||||
...source,
|
||||
contacts: Array.isArray(source.contacts)
|
||||
? source.contacts.map((item) => ({
|
||||
...createContact(),
|
||||
...item,
|
||||
}))
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
function isBlankContact(contact = {}) {
|
||||
return !contact.contactName?.trim() &&
|
||||
!contact.contactPhone?.trim() &&
|
||||
!contact.postalAddress?.trim() &&
|
||||
!contact.email?.trim();
|
||||
}
|
||||
|
||||
const visible = ref(false);
|
||||
const modalMode = ref("create");
|
||||
const originalState = ref(createFormData());
|
||||
const formState = ref(createFormData());
|
||||
|
||||
const isViewMode = computed(() => modalMode.value === "view");
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
if (modalMode.value === "view") return "查看相对方";
|
||||
if (modalMode.value === "edit") return "编辑相对方";
|
||||
return "新增相对方";
|
||||
});
|
||||
|
||||
const isDomesticOrg = computed(() => formState.value.oppositeCharacter === "1");
|
||||
const isDomesticPerson = computed(() => formState.value.oppositeCharacter === "2");
|
||||
const isForeignOrg = computed(() => formState.value.oppositeCharacter === "3");
|
||||
const isForeignPerson = computed(() => formState.value.oppositeCharacter === "4");
|
||||
|
||||
const showGatFlag = computed(() =>
|
||||
["1", "2"].includes(formState.value.oppositeCharacter),
|
||||
);
|
||||
const showEnterpriseNatureLev = computed(
|
||||
() => formState.value.natureOfEnterpriseOrg === "QY",
|
||||
);
|
||||
const showCustomerTaxno = computed(() => isDomesticOrg.value);
|
||||
const showCertificateFields = computed(
|
||||
() => isDomesticPerson.value || isForeignPerson.value,
|
||||
);
|
||||
const showTinCode = computed(() => isForeignOrg.value);
|
||||
const showCountry = computed(
|
||||
() => isForeignOrg.value || isForeignPerson.value,
|
||||
);
|
||||
const showLegalPersonName = computed(() => isDomesticOrg.value);
|
||||
const showCustomerRegaddr = computed(() => isDomesticOrg.value || isForeignOrg.value);
|
||||
const showRegCapital = computed(() => isDomesticOrg.value || isForeignOrg.value);
|
||||
const needContacts = computed(() => isDomesticOrg.value || isForeignOrg.value);
|
||||
|
||||
const syncStatusLabel = computed(() => {
|
||||
const list = props.optionSets.syncStatusOptions || [];
|
||||
return (
|
||||
list.find((item) => item.value === formState.value.syncStatus)?.label ||
|
||||
formState.value.syncStatus ||
|
||||
"未同步"
|
||||
);
|
||||
});
|
||||
|
||||
function addContact() {
|
||||
formState.value.contacts.push(createContact());
|
||||
}
|
||||
|
||||
function removeContact(uid) {
|
||||
formState.value.contacts = formState.value.contacts.filter(
|
||||
(item) => item.uid !== uid,
|
||||
);
|
||||
if (!formState.value.contacts.length) {
|
||||
formState.value.contacts = needContacts.value ? [createContact()] : [];
|
||||
}
|
||||
}
|
||||
|
||||
function handleOppositeCharacterChange() {
|
||||
formState.value.customerTaxno = "";
|
||||
formState.value.certificateType = undefined;
|
||||
formState.value.certificateNum = "";
|
||||
formState.value.tinCode = "";
|
||||
formState.value.country = undefined;
|
||||
formState.value.countryName = "";
|
||||
formState.value.legalPersonName = "";
|
||||
formState.value.customerRegaddr = "";
|
||||
formState.value.registeredCapital = undefined;
|
||||
formState.value.regCapital = "";
|
||||
formState.value.gatFlag = "N";
|
||||
if (needContacts.value && !formState.value.contacts.length) {
|
||||
formState.value.contacts = [createContact()];
|
||||
}
|
||||
if (!needContacts.value) {
|
||||
formState.value.contacts = formState.value.contacts.filter(
|
||||
(item) => !isBlankContact(item),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function handleNatureOfEnterpriseOrgChange() {
|
||||
if (formState.value.natureOfEnterpriseOrg !== "QY") {
|
||||
formState.value.natureOfEnterpriseOrgLev = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function validateContacts() {
|
||||
if (!needContacts.value) {
|
||||
return "";
|
||||
}
|
||||
if (!formState.value.contacts.length) {
|
||||
return "请至少维护一个联系人";
|
||||
}
|
||||
for (const contact of formState.value.contacts) {
|
||||
if (!contact.contactName?.trim()) return "请填写联系人姓名";
|
||||
if (!contact.contactPhone?.trim()) return "请填写联系人联系电话";
|
||||
if (!contact.postalAddress?.trim()) return "请填写联系人通讯地址";
|
||||
if (!contact.email?.trim()) return "请填写联系人电子邮箱";
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(contact.email.trim())) {
|
||||
return "联系人电子邮箱格式不正确";
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function validateForm() {
|
||||
if (!formState.value.customerNameC?.trim()) return "请填写公司名称";
|
||||
if (!formState.value.relativeType) return "请选择公司类型";
|
||||
if (!formState.value.oppositeCharacter) return "请选择公司性质";
|
||||
if (!formState.value.natureOfEnterpriseOrg) return "请选择组织性质";
|
||||
if (showEnterpriseNatureLev.value && !formState.value.natureOfEnterpriseOrgLev)
|
||||
return "请选择组织性质企业二级";
|
||||
if (!formState.value.groupFlag) return "请选择是否集团内";
|
||||
if (isDomesticOrg.value && !formState.value.customerTaxno?.trim())
|
||||
return "请填写统一社会信用代码";
|
||||
if (isDomesticPerson.value && !formState.value.certificateType)
|
||||
return "请选择自然人证件类型";
|
||||
if (isDomesticPerson.value && !formState.value.certificateNum?.trim())
|
||||
return "请填写自然人证件号码";
|
||||
if (isForeignOrg.value && !formState.value.tinCode?.trim()) return "请填写TIN号";
|
||||
if (isForeignOrg.value && !formState.value.country) return "请选择国家";
|
||||
if (isForeignPerson.value && !formState.value.country) return "请选择国家";
|
||||
if (showLegalPersonName.value && !formState.value.legalPersonName?.trim())
|
||||
return "请填写法定代表人或负责人";
|
||||
if (showCustomerRegaddr.value && !formState.value.customerRegaddr?.trim())
|
||||
return "请填写注册地址";
|
||||
if (showRegCapital.value && !formState.value.regCapital?.trim())
|
||||
return "请填写注册资本";
|
||||
return validateContacts();
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formState.value = copyFormData(originalState.value);
|
||||
}
|
||||
|
||||
function normalizeFormBeforeSave() {
|
||||
formState.value.customerNameC = formState.value.customerNameC?.trim?.() || "";
|
||||
formState.value.customerTaxno = formState.value.customerTaxno?.trim?.() || "";
|
||||
formState.value.certificateNum = formState.value.certificateNum?.trim?.() || "";
|
||||
formState.value.tinCode = formState.value.tinCode?.trim?.() || "";
|
||||
formState.value.legalPersonName = formState.value.legalPersonName?.trim?.() || "";
|
||||
formState.value.customerRegaddr = formState.value.customerRegaddr?.trim?.() || "";
|
||||
formState.value.regCapital = formState.value.regCapital?.trim?.() || "";
|
||||
formState.value.comments = formState.value.comments?.trim?.() || "";
|
||||
formState.value.remark = formState.value.remark?.trim?.() || "";
|
||||
formState.value.emailAddress = formState.value.emailAddress?.trim?.() || "";
|
||||
formState.value.countryName =
|
||||
(props.optionSets.countryOptions || []).find(
|
||||
(item) => item.value === formState.value.country,
|
||||
)?.label || "";
|
||||
formState.value.isSgat = formState.value.gatFlag === "Y" ? "1" : "0";
|
||||
formState.value.isJtn = formState.value.groupFlag === "Y" ? "1" : "0";
|
||||
|
||||
formState.value.contacts = (formState.value.contacts || []).map((item) => ({
|
||||
...item,
|
||||
contactName: item.contactName?.trim?.() || "",
|
||||
contactPhone: item.contactPhone?.trim?.() || "",
|
||||
postalAddress: item.postalAddress?.trim?.() || "",
|
||||
email: item.email?.trim?.() || "",
|
||||
remark: item.remark?.trim?.() || "",
|
||||
}));
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
normalizeFormBeforeSave();
|
||||
const errorMessage = validateForm();
|
||||
if (errorMessage) {
|
||||
message.warning(errorMessage);
|
||||
return;
|
||||
}
|
||||
const payload = copyFormData(formState.value);
|
||||
payload.contacts = needContacts.value
|
||||
? payload.contacts
|
||||
: payload.contacts.filter((item) => !isBlankContact(item));
|
||||
emit("save", payload);
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function showModal(record = {}, mode = "create") {
|
||||
modalMode.value = mode || "create";
|
||||
const source =
|
||||
modalMode.value === "create" ? createFormData() : copyFormData(record);
|
||||
originalState.value = copyFormData(source);
|
||||
formState.value = copyFormData(source);
|
||||
visible.value = true;
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showModal,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.partner-modal-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: calc(100vh - 180px);
|
||||
}
|
||||
|
||||
.partner-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 16px 20px 0;
|
||||
}
|
||||
|
||||
.section-card {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.section-alert {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.grid-two {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px 16px;
|
||||
}
|
||||
|
||||
.field-span-2 {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.card-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.contact-toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.contact-card {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 10px;
|
||||
padding: 14px;
|
||||
background: #fafcff;
|
||||
}
|
||||
|
||||
.contact-card + .contact-card {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.contact-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.empty-contact {
|
||||
padding: 24px 0;
|
||||
text-align: center;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex: 0 0 auto;
|
||||
padding: 12px 20px 20px;
|
||||
background: #fff;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.grid-two {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.field-span-2 {
|
||||
grid-column: span 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,579 @@
|
||||
<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.customerNameC"
|
||||
allow-clear
|
||||
placeholder="请输入公司名称"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :md="6" :sm="24">
|
||||
<a-form-item label="公司类型">
|
||||
<a-select
|
||||
v-model:value="queryParam.relativeType"
|
||||
:options="optionSets.relativeTypeOptions"
|
||||
allow-clear
|
||||
placeholder="请选择公司类型"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :md="6" :sm="24">
|
||||
<a-form-item label="公司性质">
|
||||
<a-select
|
||||
v-model:value="queryParam.oppositeCharacter"
|
||||
:options="optionSets.oppositeCharacterOptions"
|
||||
allow-clear
|
||||
placeholder="请选择公司性质"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :md="6" :sm="24">
|
||||
<a-form-item label="是否集团内">
|
||||
<a-select
|
||||
v-model:value="queryParam.groupFlag"
|
||||
:options="optionSets.booleanOptions"
|
||||
allow-clear
|
||||
placeholder="请选择"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :md="6" :sm="24">
|
||||
<a-form-item label="同步状态">
|
||||
<a-select
|
||||
v-model:value="queryParam.syncStatus"
|
||||
:options="optionSets.syncStatusOptions"
|
||||
allow-clear
|
||||
placeholder="请选择同步状态"
|
||||
/>
|
||||
</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="oppositeCode"
|
||||
title="相对方编码"
|
||||
width="170"
|
||||
show-overflow="title"
|
||||
/>
|
||||
<vxe-column
|
||||
field="oppositeId"
|
||||
title="法务相对方ID"
|
||||
width="150"
|
||||
show-overflow="title"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.oppositeId || "未同步" }}
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column
|
||||
field="customerNameC"
|
||||
title="公司名称"
|
||||
width="220"
|
||||
show-overflow="title"
|
||||
/>
|
||||
<vxe-column field="relativeType" title="公司类型" width="140">
|
||||
<template #default="{ row }">
|
||||
{{ getLabel(optionSets.relativeTypeOptions, row.relativeType) }}
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="oppositeCharacter" title="公司性质" width="140">
|
||||
<template #default="{ row }">
|
||||
{{ getLabel(optionSets.oppositeCharacterOptions, row.oppositeCharacter) }}
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="natureOfEnterpriseOrg" title="组织性质" width="140">
|
||||
<template #default="{ row }">
|
||||
{{ getLabel(optionSets.natureOfEnterpriseOrgOptions, row.natureOfEnterpriseOrg) }}
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="groupFlag" title="是否集团内" width="110">
|
||||
<template #default="{ row }">
|
||||
{{ row.groupFlag === "Y" ? "是" : "否" }}
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="gatFlag" title="涉港澳台" width="110">
|
||||
<template #default="{ row }">
|
||||
{{ row.gatFlag === "Y" ? "是" : "否" }}
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="identityNo" title="信用代码 / 证件号" width="220" show-overflow="title">
|
||||
<template #default="{ row }">
|
||||
{{ row.customerTaxno || row.certificateNum || row.tinCode || "-" }}
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="country" title="国家" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ getLabel(optionSets.countryOptions, row.country) || "-" }}
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="contactSummary" title="联系人信息" width="220" show-overflow="title">
|
||||
<template #default="{ row }">
|
||||
{{ getContactSummary(row.contacts) }}
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="syncStatus" title="同步状态" width="120">
|
||||
<template #default="{ row }">
|
||||
<a-tag :color="getSyncStatusColor(row.syncStatus)">
|
||||
{{ getLabel(optionSets.syncStatusOptions, row.syncStatus) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column
|
||||
field="createTime"
|
||||
title="创建时间"
|
||||
width="170"
|
||||
show-overflow="title"
|
||||
/>
|
||||
<vxe-column title="操作" width="240" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="table-actions">
|
||||
<a @click="handleView(row)">查看</a>
|
||||
<a @click="handleEdit(row)">编辑</a>
|
||||
<a @click="handleMockSync(row)">模拟同步</a>
|
||||
<a @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>
|
||||
|
||||
<PartnerModal
|
||||
ref="partnerModalRef"
|
||||
:option-sets="optionSets"
|
||||
@save="handleModalSave"
|
||||
/>
|
||||
</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 PartnerModal from "./components/PartnerModal.vue";
|
||||
import {queryPartnerList,getPartnerInfo,createPartner,updatePartner,deletePartnerByIds,mockSyncPartner} from '@/api/partner.js'
|
||||
|
||||
function isSuccessResponse(res) {
|
||||
return String(res?.code || "") === "0000";
|
||||
}
|
||||
|
||||
function getPageResult(res) {
|
||||
return res?.result || {};
|
||||
}
|
||||
|
||||
function getDataResult(res) {
|
||||
return res?.result || null;
|
||||
}
|
||||
|
||||
const optionSets = {
|
||||
relativeTypeOptions: [
|
||||
{ label: "客户", value: "client" },
|
||||
{ label: "供应商", value: "supplier" },
|
||||
{ label: "合作伙伴", value: "Partner" },
|
||||
{ label: "客户及供应商", value: "clientAndSup" },
|
||||
{ label: "其他", value: "other" },
|
||||
],
|
||||
oppositeCharacterOptions: [
|
||||
{ label: "境内组织", value: "1" },
|
||||
{ label: "境内个人", value: "2" },
|
||||
{ label: "境外组织", value: "3" },
|
||||
{ label: "境外个人", value: "4" },
|
||||
],
|
||||
natureOfEnterpriseOrgOptions: [
|
||||
{ label: "政府", value: "ZY" },
|
||||
{ label: "企业", value: "QY" },
|
||||
{ label: "其他组织", value: "QT" },
|
||||
],
|
||||
natureOfEnterpriseOrgLevOptions: [
|
||||
{ label: "国有企业", value: "GNQY" },
|
||||
{ label: "民营企业", value: "MYQY" },
|
||||
{ label: "外资企业", value: "WZQY" },
|
||||
{ label: "合资企业", value: "HZQY" },
|
||||
{ label: "国有全资和控股企业", value: "GYQZHKGQY" },
|
||||
{ label: "其他", value: "QT" },
|
||||
],
|
||||
certificateTypeOptions: [
|
||||
{ label: "身份证", value: "idCard" },
|
||||
{ label: "护照", value: "passport" },
|
||||
{ label: "其他", value: "other" },
|
||||
],
|
||||
countryOptions: [
|
||||
{ label: "中国", value: "GJ156" },
|
||||
{ label: "日本", value: "GJ392" },
|
||||
{ label: "芬兰", value: "GJ246" },
|
||||
{ label: "美国", value: "GJ840" },
|
||||
{ label: "加拿大", value: "GJ124" },
|
||||
{ label: "英国", value: "GJ826" },
|
||||
],
|
||||
booleanOptions: [
|
||||
{ label: "是", value: "Y" },
|
||||
{ label: "否", value: "N" },
|
||||
],
|
||||
syncStatusOptions: [
|
||||
{ label: "待同步", value: "unsynced" },
|
||||
{ label: "同步成功", value: "synced" },
|
||||
{ label: "同步失败", value: "failed" },
|
||||
],
|
||||
};
|
||||
|
||||
function copyRecord(record) {
|
||||
return {
|
||||
...record,
|
||||
contacts: (record.contacts || []).map((item) => ({ ...item })),
|
||||
};
|
||||
}
|
||||
|
||||
function getLabel(options, value) {
|
||||
return options.find((item) => item.value === value)?.label || value || "";
|
||||
}
|
||||
|
||||
function getContactSummary(contacts = []) {
|
||||
if (!contacts.length) return "暂无";
|
||||
const first = contacts[0];
|
||||
return `${first.contactName || "-"} / ${first.contactPhone || "-"}`;
|
||||
}
|
||||
|
||||
const formRef = ref(null);
|
||||
const queryContainer = ref(null);
|
||||
const pageWrapperRef = ref(null);
|
||||
const tableRef = ref(null);
|
||||
const toolbarRef = ref(null);
|
||||
const partnerModalRef = ref(null);
|
||||
|
||||
const total = ref(0);
|
||||
const loading = ref(false);
|
||||
const tableHeight = ref(420);
|
||||
const tableData = ref([]);
|
||||
const selectedRows = ref([]);
|
||||
const selectedRowKeys = ref([]);
|
||||
const currentModalMode = ref("create");
|
||||
|
||||
const queryParam = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
customerNameC: undefined,
|
||||
relativeType: undefined,
|
||||
oppositeCharacter: undefined,
|
||||
groupFlag: undefined,
|
||||
syncStatus: undefined,
|
||||
});
|
||||
|
||||
const queryCardBodyStyle = {
|
||||
padding: "20px 24px 6px",
|
||||
};
|
||||
|
||||
const tableCardBodyStyle = {
|
||||
display: "flex",
|
||||
flexFlow: "column nowrap",
|
||||
minHeight: "0",
|
||||
padding: "8px 12px 6px",
|
||||
};
|
||||
|
||||
function connectToolbar() {
|
||||
const table = tableRef.value;
|
||||
const toolbar = toolbarRef.value;
|
||||
if (table && toolbar && table.connect) {
|
||||
table.connect(toolbar);
|
||||
}
|
||||
}
|
||||
|
||||
function getSyncStatusColor(status) {
|
||||
if (status === "synced") return "success";
|
||||
if (status === "failed") return "error";
|
||||
return "default";
|
||||
}
|
||||
|
||||
function selectChangeEvent({ records }) {
|
||||
selectedRows.value = records;
|
||||
selectedRowKeys.value = records.map((item) => item.customerSysid);
|
||||
}
|
||||
|
||||
function getList() {
|
||||
loading.value = true;
|
||||
queryPartnerList(queryParam.value)
|
||||
.then((res) => {
|
||||
if (!isSuccessResponse(res)) {
|
||||
tableData.value = [];
|
||||
total.value = 0;
|
||||
return;
|
||||
}
|
||||
const pageResult = getPageResult(res);
|
||||
tableData.value = (pageResult.records || []).map((item) => copyRecord(item));
|
||||
total.value = pageResult.total || 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,
|
||||
customerNameC: undefined,
|
||||
relativeType: undefined,
|
||||
oppositeCharacter: undefined,
|
||||
groupFlag: undefined,
|
||||
syncStatus: undefined,
|
||||
});
|
||||
getList();
|
||||
}
|
||||
|
||||
function onShowSizeChange(current, pageSize) {
|
||||
queryParam.value.pageNo = current;
|
||||
queryParam.value.pageSize = pageSize;
|
||||
getList();
|
||||
}
|
||||
|
||||
function handleCreate() {
|
||||
currentModalMode.value = "create";
|
||||
partnerModalRef.value?.showModal({}, "create");
|
||||
}
|
||||
|
||||
function handleView(row) {
|
||||
currentModalMode.value = "view";
|
||||
getPartnerInfo(row.customerSysid).then((res) => {
|
||||
partnerModalRef.value?.showModal(copyRecord(getDataResult(res) || row), "view");
|
||||
});
|
||||
}
|
||||
|
||||
function handleEdit(row) {
|
||||
currentModalMode.value = "edit";
|
||||
getPartnerInfo(row.customerSysid).then((res) => {
|
||||
partnerModalRef.value?.showModal(copyRecord(getDataResult(res) || row), "edit");
|
||||
});
|
||||
}
|
||||
|
||||
function handleMockSync(row) {
|
||||
mockSyncPartner(row.customerSysid).then(() => {
|
||||
message.success(`已模拟同步:${row.customerNameC}`);
|
||||
getList();
|
||||
});
|
||||
}
|
||||
|
||||
function handleDelete(row) {
|
||||
Modal.confirm({
|
||||
title: `确认删除【${row.customerNameC}】吗?`,
|
||||
content: "删除后不可恢复,请确认是否继续。",
|
||||
okText: "删除",
|
||||
cancelText: "取消",
|
||||
okType: "danger",
|
||||
onOk: () => {
|
||||
deletePartnerByIds([row.customerSysid]).then(() => {
|
||||
selectedRows.value = [];
|
||||
selectedRowKeys.value = [];
|
||||
message.success("删除成功");
|
||||
getList();
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleBatchDelete() {
|
||||
Modal.confirm({
|
||||
title: `确认删除选中的 ${selectedRowKeys.value.length} 条记录吗?`,
|
||||
content: "删除后不可恢复,请确认是否继续。",
|
||||
okText: "删除",
|
||||
cancelText: "取消",
|
||||
okType: "danger",
|
||||
onOk: () => {
|
||||
deletePartnerByIds(selectedRowKeys.value).then(() => {
|
||||
selectedRows.value = [];
|
||||
selectedRowKeys.value = [];
|
||||
message.success("批量删除成功");
|
||||
getList();
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleModalSave(payload) {
|
||||
if (currentModalMode.value === "create") {
|
||||
createPartner(payload).then(() => {
|
||||
message.success("新增成功");
|
||||
getList();
|
||||
});
|
||||
} else {
|
||||
updatePartner(payload).then(() => {
|
||||
message.success("保存成功");
|
||||
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: 18px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.table-actions a {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.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