前端完善

This commit is contained in:
2026-05-29 15:26:25 +08:00
parent dcbd86f8f8
commit 2fba4fbba7
12 changed files with 1829 additions and 736 deletions
+148
View File
@@ -0,0 +1,148 @@
import request from "@/utils/request";
function getResult(res) {
return res?.result;
}
function isSuccessResponse(res) {
return String(res?.code || "") === "0000";
}
export function queryLaunchList(data) {
return request({
url: "/contract/launch/list",
method: "post",
data,
});
}
export function getLaunchDetail(id) {
return request({
url: `/contract/launch/${id}`,
method: "get",
});
}
export function createLaunchDraft(data) {
return request({
url: "/contract/launch",
method: "post",
data,
});
}
export function updateLaunchDraft(data) {
return request({
url: "/contract/launch",
method: "put",
data,
});
}
export function submitLaunchRecord(id) {
return request({
url: `/contract/launch/${id}/submit`,
method: "post",
});
}
export function mockApproveLaunch(id, data = {}) {
return request({
url: `/contract/launch/${id}/mock-approve`,
method: "post",
data,
});
}
export function mockRejectLaunch(id, data = {}) {
return request({
url: `/contract/launch/${id}/mock-reject`,
method: "post",
data,
});
}
export function saveLaunchSealInfo(id, data) {
return request({
url: `/contract/launch/${id}/seal/save`,
method: "post",
data,
});
}
export function finishLaunchSeal(id, data) {
return request({
url: `/contract/launch/${id}/seal/finish`,
method: "post",
data,
});
}
export function queryApproveViewUrl(data) {
return request({
url: "/contract/push/settlement/approve/view-url",
method: "post",
data,
});
}
export function mockCollectLaunch(id) {
return request({
url: `/contract/launch/${id}/mock-collect`,
method: "post",
});
}
export function archiveLaunch(id, data = {}) {
return request({
url: `/contract/launch/${id}/archive`,
method: "post",
data,
});
}
export function deleteLaunchRecord(id) {
return request({
url: `/contract/launch/${id}`,
method: "delete",
});
}
export function queryPublishedTemplates() {
return request({
url: "/manifest/manage/published",
method: "get",
}).then((res) => (isSuccessResponse(res) ? getResult(res) || [] : []));
}
export function queryPartnerOptions(data = {}) {
return request({
url: "/partner/manage/list",
method: "post",
data: {
pageNo: 1,
pageSize: 200,
...data,
},
}).then((res) => (isSuccessResponse(res) ? getResult(res)?.data || [] : []));
}
export function uploadLaunchFile(formData) {
return request({
url: "/trans/minio/file/uploadOutie",
method: "post",
data: formData,
headers: {
"Content-Type": "multipart/form-data",
},
});
}
export function downloadLaunchFile(fileName) {
return request({
url: "/trans/minio/file/download",
method: "get",
params: { fileName },
responseType: "arraybuffer",
});
}
-309
View File
@@ -1,309 +0,0 @@
/**
* 合同发起模块 - 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)
}
@@ -118,7 +118,8 @@
:options="contractCompanyOptions" :options="contractCompanyOptions"
show-search show-search
:placeholder="`请选择${party.displayName}`" :placeholder="`请选择${party.displayName}`"
@change="handleCompanySelect(party.roleKey, $event)" :filter-option="filterPartnerOption"
@change="(value, option) => handleCompanySelect(party.roleKey, value, option)"
/> />
<a-button <a-button
class="icon-btn" class="icon-btn"
@@ -182,6 +183,7 @@
v-model:value="detailState.contractType" v-model:value="detailState.contractType"
:options="contractTypeOptions" :options="contractTypeOptions"
placeholder="请选择" placeholder="请选择"
@change="handleContractTypeChange"
/> />
</a-form-item> </a-form-item>
</div> </div>
@@ -276,6 +278,20 @@
placeholder="请选择付款方式" placeholder="请选择付款方式"
/> />
</a-form-item> </a-form-item>
<a-form-item class="tight-item" label="收支方向" required>
<a-select
v-model:value="detailState.paymentDirection"
:options="paymentDirectionOptions"
placeholder="请选择收支方向"
/>
</a-form-item>
<a-form-item class="tight-item" label="签署方式" required>
<a-select
v-model:value="detailState.isElectron"
:options="isElectronOptions"
placeholder="请选择签署方式"
/>
</a-form-item>
</div> </div>
</div> </div>
</div> </div>
@@ -326,6 +342,20 @@
</div> </div>
</div> </div>
<div class="upload-block">
<div class="upload-label">签订依据附件</div>
<a-upload-dragger
:file-list="gistAttachmentList"
:before-upload="handleGistAttachmentUpload"
@remove="handleRemoveGistAttachment"
:disabled="isViewMode"
:show-upload-list="{ showRemoveIcon: !isViewMode }"
multiple
>
<div class="other-upload-text">签订依据文件对应法务 contractGistFile</div>
</a-upload-dragger>
</div>
<div class="upload-block"> <div class="upload-block">
<div class="upload-label">其他附件</div> <div class="upload-label">其他附件</div>
<a-upload-dragger <a-upload-dragger
@@ -356,6 +386,14 @@
/> />
</a-form-item> </a-form-item>
<a-form-item class="tight-item" label="签订依据说明">
<a-textarea
v-model:value="detailState.contractGistRemark"
:rows="2"
placeholder="选填,推送法务字段 contractGistRemark"
/>
</a-form-item>
<div class="basic-title contract-attr-title"> <div class="basic-title contract-attr-title">
<span class="dot"></span> <span class="dot"></span>
合同属性 合同属性
@@ -398,6 +436,20 @@
readonly readonly
/> />
</a-form-item> </a-form-item>
<a-form-item class="tight-item" label="文本来源" required>
<a-select
v-model:value="detailState.textSource"
:options="textSourceOptions"
placeholder="请选择文本来源"
/>
</a-form-item>
<a-form-item class="tight-item" label="合同来源" required>
<a-select
v-model:value="detailState.contractSource"
:options="contractSourceOptions"
placeholder="请选择合同来源"
/>
</a-form-item>
<a-form-item class="tight-item" label="协助部门"> <a-form-item class="tight-item" label="协助部门">
<a-input <a-input
v-model:value="detailState.assistDept" v-model:value="detailState.assistDept"
@@ -409,6 +461,7 @@
v-model:value="detailState.contractTypeCode" v-model:value="detailState.contractTypeCode"
:options="contractTypeOptions" :options="contractTypeOptions"
placeholder="合同类型" placeholder="合同类型"
@change="handleContractTypeCodeChange"
/> />
</a-form-item> </a-form-item>
<a-form-item <a-form-item
@@ -421,6 +474,45 @@
placeholder="合同执行人手机号" placeholder="合同执行人手机号"
/> />
</a-form-item> </a-form-item>
<a-form-item class="tight-item" label="二级单位合同编码">
<a-input
v-model:value="detailState.selfCode"
placeholder="选填,对应法务 selfCode"
/>
</a-form-item>
<a-form-item class="tight-item" label="主合同编码">
<a-input
v-model:value="detailState.mainContractCode"
placeholder="补录/变更合同时填写"
/>
</a-form-item>
<a-form-item class="tight-item" label="固定期限日期类型">
<a-select
v-model:value="detailState.dateType"
:options="dateTypeOptions"
placeholder="请选择"
/>
</a-form-item>
<a-form-item class="tight-item" label="签约主体编码">
<a-input
v-model:value="detailState.signingSubjectCode"
placeholder="默认使用当前组织ID,多个用逗号分隔"
/>
</a-form-item>
<a-form-item class="tight-item" label="开票时间">
<a-date-picker
v-model:value="detailState.invoiceDate"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</a-form-item>
<a-form-item class="tight-item" label="收款时间">
<a-date-picker
v-model:value="detailState.receiveDate"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</a-form-item>
</div> </div>
<div class="boolean-grid"> <div class="boolean-grid">
@@ -512,6 +604,7 @@
</div> </div>
<div class="plan-toolbar"> <div class="plan-toolbar">
<a-button <a-button
v-if="!isViewMode"
type="dashed" type="dashed"
size="small" size="small"
@click="addPerformancePlan" @click="addPerformancePlan"
@@ -571,6 +664,16 @@
/> />
</template> </template>
</a-table-column> </a-table-column>
<a-table-column title="所属主体编码" key="planSubjectCode" width="160">
<template #default="{ record }">
<a-input v-model:value="record.planSubjectCode" />
</template>
</a-table-column>
<a-table-column title="所属主体" key="planSubjectName" width="180">
<template #default="{ record }">
<a-input v-model:value="record.planSubjectName" />
</template>
</a-table-column>
<a-table-column title="备注" key="planDetail"> <a-table-column title="备注" key="planDetail">
<template #default="{ record }"> <template #default="{ record }">
<a-input v-model:value="record.planDetail" /> <a-input v-model:value="record.planDetail" />
@@ -703,6 +806,20 @@ import { computed, ref } from "vue";
import { message } from "ant-design-vue"; import { message } from "ant-design-vue";
import { PlusOutlined, SearchOutlined } from "@ant-design/icons-vue"; import { PlusOutlined, SearchOutlined } from "@ant-design/icons-vue";
import ContractTemplateFillDemoModal from "./ContractTemplateFillDemoModal.vue"; import ContractTemplateFillDemoModal from "./ContractTemplateFillDemoModal.vue";
import { queryPartnerOptions, uploadLaunchFile } from "@/api/launch.js";
import {
contractSourceOptions,
contractTypeOptions,
currencyOptions,
dateTypeOptions,
extraAmountFieldOptions,
isElectronOptions,
paymentDirectionOptions,
sealTypeOptions,
textSourceOptions,
valuationModeOptions,
getOptionLabel,
} from "../contractLaunchOptions.js";
// ======================================================================== // ========================================================================
// 以下为表单模型 + 字典 + 工具函数,直接内联在此文件中。 // 以下为表单模型 + 字典 + 工具函数,直接内联在此文件中。
@@ -756,7 +873,7 @@ const countryOptions = [
{ label: "日本", value: "GJ392" }, { label: "日本", value: "GJ392" },
]; ];
const contractCompanyOptions = [ const contractCompanyOptions = ref([
{ label: "扬州宏富特种材料有限公司", value: "扬州宏富特种材料有限公司" }, { label: "扬州宏富特种材料有限公司", value: "扬州宏富特种材料有限公司" },
{ label: "宁波港船务货运代理有限公司", value: "宁波港船务货运代理有限公司" }, { label: "宁波港船务货运代理有限公司", value: "宁波港船务货运代理有限公司" },
{ label: "宁波浙港供应链科技有限公司", value: "宁波浙港供应链科技有限公司" }, { label: "宁波浙港供应链科技有限公司", value: "宁波浙港供应链科技有限公司" },
@@ -765,43 +882,10 @@ const contractCompanyOptions = [
{ label: "浙江临港数智科技有限公司", value: "浙江临港数智科技有限公司" }, { label: "浙江临港数智科技有限公司", value: "浙江临港数智科技有限公司" },
{ label: "杭州智云实施服务有限公司", value: "杭州智云实施服务有限公司" }, { label: "杭州智云实施服务有限公司", value: "杭州智云实施服务有限公司" },
{ label: "浙港物流平台有限公司", value: "浙港物流平台有限公司" }, { label: "浙港物流平台有限公司", value: "浙港物流平台有限公司" },
]; ]);
// ========== 合同相关字典 ========== // ========== 合同相关字典 ==========
const contractTypeOptions = [
{ label: "采购合同", value: "采购合同" },
{ label: "销售合同", value: "销售合同" },
{ label: "服务合同", value: "服务合同" },
{ label: "框架合同", value: "框架合同" },
];
const valuationModeOptions = [
{ label: "无金额", value: "0" },
{ label: "固定总价", value: "1" },
{ label: "预估总价", value: "2" },
{ label: "无法预估总价", value: "3" },
{ label: "固定单价", value: "4" },
];
const currencyOptions = [
{ label: "人民币", value: "RMB" },
{ label: "美元", value: "USD" },
{ label: "港币", value: "HKD" },
{ label: "欧元", value: "EUR" },
{ label: "印尼盾", value: "IDR" },
];
const sealTypeOptions = [
{ label: "公章", value: "GZ" },
{ label: "合同专用章", value: "HTZYZ" },
{ label: "公章+法定代表人私章", value: "GZFDDBRSZ" },
{ label: "公章+法定代表人签字", value: "GZFDDBRQZ" },
{ label: "公章+被授权人签字", value: "GZBSQRQZ" },
{ label: "合同专用章+被授权人签字", value: "HTZYZBSQRQZ" },
{ label: "合同专用章+法定代表人签字", value: "HTZYZFDDBRQZ" },
{ label: "其他", value: "QT" },
];
const mandateTypeOptions = [ const mandateTypeOptions = [
{ label: "海铁联运委托", value: "海铁联运委托" }, { label: "海铁联运委托", value: "海铁联运委托" },
@@ -826,29 +910,118 @@ const paymentMethodOptions = [
{ label: "银行转账(一次性结清)", value: "银行转账(一次性结清)" }, { label: "银行转账(一次性结清)", value: "银行转账(一次性结清)" },
]; ];
const extraAmountFieldOptions = [
{ key: "deposit", label: "定金" },
{ key: "pledge", label: "押金" },
{ key: "advance", label: "预付款" },
{ key: "guarantee", label: "保证金" },
];
const SELF_COMPANY_NAME = "浙港物流平台有限公司"; const SELF_COMPANY_NAME = "浙港物流平台有限公司";
const MAX_COUNTERPARTY_COUNT = 2; const MAX_COUNTERPARTY_COUNT = 2;
function getOptionLabel(options, value) { // ========== 工具函数 ==========
return options.find((item) => item.value === value)?.label || value || "";
function formatDateTimeValue(value, endOfDay = false) {
if (!value) return null;
if (String(value).includes(" ")) return value;
return `${value} ${endOfDay ? "23:59:59" : "00:00:00"}`;
} }
// ========== 工具函数 ========== function normalizeFileItem(item = {}) {
return {
uid: item.uid || `file_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`,
name: item.name || item.fileName || "",
size: item.size ?? item.fileSize ?? 0,
status: item.status || "done",
filePath: item.filePath || "",
fileType: item.fileType || "",
fileCategory: item.fileCategory || "",
};
}
function getFileNameFromDetail(source, category) {
if (category === "body") {
return source.contractBodyFileName || source.contractTextFileName || "";
}
return "";
}
function splitFilesByCategory(source = {}) {
const sourceFiles = source.fileList || [];
const bodyFile = sourceFiles.find((item) => item.fileCategory === "body") || {};
return {
contractBodyFileName:
getFileNameFromDetail(source, "body") || bodyFile.fileName || "",
contractBodyFilePath:
source.contractBodyFilePath ||
source.contractTextFilePath ||
bodyFile.filePath ||
"",
contractBodyFileType:
source.contractBodyFileType ||
source.contractTextFileType ||
bodyFile.fileType ||
"",
contractBodyFileSize:
source.contractBodyFileSize ??
source.contractTextFileSize ??
bodyFile.fileSize ??
null,
otherAttachmentList: source.otherAttachmentList?.length
? source.otherAttachmentList.map(normalizeFileItem)
: sourceFiles
.filter((item) => item.fileCategory === "other")
.map(normalizeFileItem),
contractGistFileList: source.contractGistFileList?.length
? source.contractGistFileList.map(normalizeFileItem)
: sourceFiles
.filter((item) => item.fileCategory === "gist")
.map(normalizeFileItem),
};
}
function buildExtraAmountPayload(extraAmounts = {}) {
const enabledItems = extraAmountFieldOptions.filter(
(item) => extraAmounts[`${item.key}Enabled`],
);
const result = {
ewAmountType: enabledItems.map((item) => item.code).join(","),
djAmount: 0,
yjAmount: 0,
yfkAmount: 0,
bzjAmount: 0,
};
enabledItems.forEach((item) => {
result[item.amountField] = Number(extraAmounts[item.key] || 0);
});
return result;
}
function restoreExtraAmounts(source = {}, current = {}) {
const result = { ...current, ...(source.extraAmounts || {}) };
const selectedCodes = String(source.ewAmountType || "")
.split(",")
.map((item) => item.trim())
.filter(Boolean);
extraAmountFieldOptions.forEach((item) => {
const amount = source[item.amountField];
if (selectedCodes.includes(item.code) || amount || amount === 0) {
result[`${item.key}Enabled`] = selectedCodes.includes(item.code) || amount > 0;
if (amount !== undefined && amount !== null) {
result[item.key] = amount;
}
}
});
return result;
}
/** 深拷贝合同方状态 */ /** 深拷贝合同方状态 */
function copyPartyState(input) { function copyPartyState(input) {
const source = input || {}; const source = input || {};
const listState = buildPartyStateFromList(source.partyList);
const sourcePartyA = source.partyA || source.parties?.A || {}; const sourcePartyA = source.partyA || source.parties?.A || {};
const sourcePartyB = source.partyB || source.parties?.B || {}; const sourcePartyB = source.partyB || source.parties?.B || {};
const sourcePartyC = source.partyC || source.parties?.C || {}; const sourcePartyC = source.partyC || source.parties?.C || {};
return { const state = {
isThreePartyContract: Boolean(source.isThreePartyContract), isThreePartyContract: Boolean(source.isThreePartyContract),
partyA: { partyA: {
roleKey: "A", roleKey: "A",
@@ -872,6 +1045,72 @@ function copyPartyState(input) {
...sourcePartyC, ...sourcePartyC,
}, },
}; };
if (listState) {
state.partyA = { ...state.partyA, ...listState.partyA };
state.partyB = { ...state.partyB, ...listState.partyB };
state.partyC = { ...state.partyC, ...listState.partyC };
state.isThreePartyContract = listState.isThreePartyContract;
}
return state;
}
function mapLaunchPartyToFormParty(item = {}) {
return {
roleKey: item.roleCode || "B",
roleName: item.roleName || "",
displayName:
item.roleCode === "A" ? "我方" : item.roleCode === "C" ? "相对方2" : "相对方1",
partnerId: item.partnerId || null,
customerSysid: item.partnerId || null,
oppositeId: item.oppositeId || "",
companyName: item.companyName || "",
companyType: item.companyType || "",
oppositeCharacter: item.oppositeCharacter || "",
oppCharacter: item.oppositeCharacter || "",
companyNature: getCompanyNatureByOppositeCharacter(item.oppositeCharacter),
contactName: item.contactName || "",
contactPhone: item.contactPhone || "",
contactEmail: item.contactEmail || "",
contactAddress: item.contactAddress || "",
registeredAddress: item.registeredAddress || "",
legalRepresentative: item.legalRepresentative || "",
creditCode: item.creditCode || "",
bankName: item.bankName || "",
bankAccount: item.bankAccount || "",
naturalPersonIdType: item.naturalPersonIdType || "",
naturalPersonIdNumber: item.naturalPersonIdNumber || "",
isGroupInternal: item.isGroupInternal === "1" || item.isGroupInternal === true,
isHongKongMacaoTaiwan:
item.isHongKongMacaoTaiwan === "1" || item.isHongKongMacaoTaiwan === true,
};
}
function buildPartyStateFromList(list = []) {
if (!Array.isArray(list) || !list.length) return null;
const result = {
isThreePartyContract: false,
partyA: {},
partyB: {},
partyC: {},
};
list.forEach((item) => {
if (!item?.roleCode) return;
const targetKey =
item.roleCode === "A" ? "partyA" : item.roleCode === "C" ? "partyC" : "partyB";
result[targetKey] = mapLaunchPartyToFormParty(item);
});
result.isThreePartyContract = Boolean(result.partyB?.companyName && result.partyC?.companyName);
return result;
}
function getCompanyNatureByOppositeCharacter(value) {
const map = {
1: "境内组织",
2: "境内个人",
3: "境外组织",
4: "境外个人",
};
return map[String(value || "")] || "";
} }
function createEmptyCounterparty(roleKey) { function createEmptyCounterparty(roleKey) {
@@ -930,6 +1169,8 @@ function createPerformancePlan() {
planTime: "", planTime: "",
planDay: 0, planDay: 0,
planMoney: "", planMoney: "",
planSubjectCode: "",
planSubjectName: "",
planDetail: "", planDetail: "",
}; };
} }
@@ -947,6 +1188,10 @@ function createContractFormData() {
contractPeriodType: "0", contractPeriodType: "0",
periodExplain: "", periodExplain: "",
paymentMethod: "", paymentMethod: "",
paymentDirection: "0",
isElectron: "1",
textSource: "standardText",
contractSource: "ECP",
valuationMode: "", valuationMode: "",
currencyName: "", currencyName: "",
sealType: "", sealType: "",
@@ -956,10 +1201,22 @@ function createContractFormData() {
amountExplain: "", amountExplain: "",
assistDept: "", assistDept: "",
executorAccount: "", executorAccount: "",
selfCode: "",
mainContractCode: "",
dateType: "0",
signingSubjectCode: "",
contractGistRemark: "",
bodySummary: "",
invoiceDate: "",
receiveDate: "",
contractBodyFileName: "", contractBodyFileName: "",
contractBodyFilePath: "",
contractBodyFileType: "",
contractBodyFileSize: null,
contractBodyArrayBuffer: null, contractBodyArrayBuffer: null,
otherAttachmentList: [], otherAttachmentList: [],
contractGistFileList: [],
partyState: { partyState: {
isThreePartyContract: false, isThreePartyContract: false,
partyA: { companyName: SELF_COMPANY_NAME }, partyA: { companyName: SELF_COMPANY_NAME },
@@ -982,6 +1239,11 @@ function createContractFormData() {
guaranteeEnabled: false, guaranteeEnabled: false,
guarantee: 0, guarantee: 0,
}, },
ewAmountType: "",
djAmount: 0,
yjAmount: 0,
yfkAmount: 0,
bzjAmount: 0,
performancePlans: [], performancePlans: [],
signDate: "", signDate: "",
}; };
@@ -991,33 +1253,53 @@ function createContractFormData() {
function copyContractFormData(data = {}) { function copyContractFormData(data = {}) {
const form = createContractFormData(); const form = createContractFormData();
const source = data || {}; const source = data || {};
const fileState = splitFilesByCategory(source);
Object.keys(form).forEach((key) => { Object.keys(form).forEach((key) => {
if (key === "extraAmounts") { if (key === "extraAmounts") {
form.extraAmounts = { form.extraAmounts = restoreExtraAmounts(source, form.extraAmounts);
...form.extraAmounts,
...(source.extraAmounts || {}),
};
} else if (key === "performancePlans") { } else if (key === "performancePlans") {
form.performancePlans = (source.performancePlans || []).map((item) => ({ form.performancePlans = (source.performancePlans || []).map((item) => ({
...createPerformancePlan(), ...createPerformancePlan(),
...item, ...item,
})); }));
} else if (key === "partyState") { } else if (key === "partyState") {
form.partyState = normalizePartyState(source.partyState); form.partyState = normalizePartyState(source.partyState || source);
} else if (key === "otherAttachmentList") { } else if (key === "otherAttachmentList") {
form.otherAttachmentList = [ form.otherAttachmentList = fileState.otherAttachmentList;
...(source.otherAttachmentList || source.attachmentList || []), } else if (key === "contractGistFileList") {
]; form.contractGistFileList = fileState.contractGistFileList;
} else if (
[
"contractBodyFileName",
"contractBodyFilePath",
"contractBodyFileType",
"contractBodyFileSize",
].includes(key)
) {
form[key] = fileState[key] ?? form[key];
} else if (key === "contractBodyArrayBuffer") { } else if (key === "contractBodyArrayBuffer") {
form.contractBodyArrayBuffer = source.contractBodyArrayBuffer form.contractBodyArrayBuffer = source.contractBodyArrayBuffer
? source.contractBodyArrayBuffer instanceof ArrayBuffer ? source.contractBodyArrayBuffer instanceof ArrayBuffer
? source.contractBodyArrayBuffer.slice(0) ? source.contractBodyArrayBuffer.slice(0)
: null : null
: null; : null;
} else if (key === "signDate" || key === "invoiceDate" || key === "receiveDate") {
form[key] = source[key] ? String(source[key]).slice(0, 10) : form[key];
} else if (key === "assistDept") {
form.assistDept = source.assistDept ?? source.assistDepartmentName ?? form.assistDept;
} else if (key === "signingSubjectCode") {
form.signingSubjectCode =
source.signingSubjectCode ?? source.orgId ?? form.signingSubjectCode;
} else { } else {
form[key] = source[key] ?? form[key]; form[key] = source[key] ?? form[key];
} }
}); });
if (source.textSource) {
form.textSource = source.textSource;
}
if (source.contractTextFileName && !form.contractBodyFileName) {
form.contractBodyFileName = source.contractTextFileName;
}
return form; return form;
} }
@@ -1030,6 +1312,7 @@ const templateKey = ref("");
const templateName = ref(""); const templateName = ref("");
const detailState = ref(createContractFormData()); const detailState = ref(createContractFormData());
const otherAttachmentList = ref([]); const otherAttachmentList = ref([]);
const gistAttachmentList = ref([]);
const bodyConfigModalRef = ref(null); const bodyConfigModalRef = ref(null);
const counterpartyRoleKeys = ref([]); const counterpartyRoleKeys = ref([]);
@@ -1112,8 +1395,16 @@ function syncUploadLists() {
otherAttachmentList.value = (detailState.value.otherAttachmentList || []).map( otherAttachmentList.value = (detailState.value.otherAttachmentList || []).map(
(item, index) => ({ (item, index) => ({
uid: item.uid || `other_${index + 1}`, uid: item.uid || `other_${index + 1}`,
name: item.name, name: item.name || item.fileName,
size: item.size, size: item.size ?? item.fileSize,
status: item.status || "done",
}),
);
gistAttachmentList.value = (detailState.value.contractGistFileList || []).map(
(item, index) => ({
uid: item.uid || `gist_${index + 1}`,
name: item.name || item.fileName,
size: item.size ?? item.fileSize,
status: item.status || "done", status: item.status || "done",
}), }),
); );
@@ -1152,9 +1443,8 @@ function resetCurrentState() {
} }
function showSearchTip(title) { function showSearchTip(title) {
message.info( loadPartnerOptions();
`${title}查询接口当前未接入,这里先保留与原型一致的搜索入口样式`, message.info(`${title}数据已从相对方管理加载,可直接在下拉框搜索选择`);
);
} }
function showInvoiceTitles(party, roleName) { function showInvoiceTitles(party, roleName) {
@@ -1166,15 +1456,105 @@ function showInvoiceTitles(party, roleName) {
); );
} }
function handleCompanySelect(roleKey, companyName) { function filterPartnerOption(input, option) {
const keyword = String(input || "").trim().toLowerCase();
if (!keyword) return true;
return [option?.label, option?.value, option?.raw?.oppositeCode, option?.raw?.oppositeId]
.filter(Boolean)
.some((item) => String(item).toLowerCase().includes(keyword));
}
function getRelativeTypeLabel(value) {
const map = {
client: "客户",
supplier: "供应商",
Partner: "合作伙伴",
clientAndSup: "客户及供应商",
other: "其他",
};
return map[value] || value || "";
}
function getOrganizationNatureLabel(value) {
const map = {
ZY: "政府",
QY: "企业",
QT: "其他组织",
};
return map[value] || value || "";
}
function getEnterpriseNatureLevel2Label(value) {
const map = {
GNQY: "国有企业",
MYQY: "民营企业",
WZQY: "外资企业",
HZQY: "合资企业",
GYQZHKGQY: "国有全资和控股企业",
QT: "其他",
};
return map[value] || value || "";
}
function mapPartnerToParty(raw = {}, roleKey, companyName) {
const firstContact = raw.contacts?.[0] || {};
return {
partnerId: raw.customerSysid || null,
customerSysid: raw.customerSysid || null,
oppositeId: raw.oppositeId || "",
companyName: raw.customerNameC || companyName || "",
companyType: getRelativeTypeLabel(raw.relativeType),
oppositeCharacter: raw.oppositeCharacter || "",
oppCharacter: raw.oppositeCharacter || "",
companyNature: getCompanyNatureByOppositeCharacter(raw.oppositeCharacter),
organizationNature: getOrganizationNatureLabel(raw.natureOfEnterpriseOrg),
enterpriseNatureLevel2: getEnterpriseNatureLevel2Label(raw.natureOfEnterpriseOrgLev),
creditCode: raw.customerTaxno || "",
tinNo: raw.tinCode || "",
countryCode: raw.country || "",
legalRepresentative: raw.legalPersonName || "",
registeredAddress: raw.customerRegaddr || "",
registeredCapital: raw.regCapital || raw.registeredCapital || "",
contactName: firstContact.contactName || "",
contactPhone: firstContact.contactPhone || raw.officephone || "",
contactEmail: firstContact.email || raw.emailAddress || "",
contactAddress: firstContact.postalAddress || raw.customerAddress || "",
bankName: raw.customerBank || "",
bankAccount: raw.customerBankaccount || "",
naturalPersonIdType: raw.certificateType || "",
naturalPersonIdNumber: raw.certificateNum || "",
isGroupInternal: raw.groupFlag === "Y" || raw.isJtn === "1",
isHongKongMacaoTaiwan: raw.gatFlag === "Y" || raw.isSgat === "1",
invoiceTitleOptions: raw.customerNameC ? [raw.customerNameC] : [],
roleKey,
roleName: roleKey === "A" ? "我方" : "相对方",
displayName: roleKey === "A" ? "我方" : roleKey === "C" ? "相对方2" : "相对方1",
};
}
function handleCompanySelect(roleKey, companyName, option = {}) {
const party = const party =
roleKey === "A" roleKey === "A"
? detailState.value.partyState.partyA ? detailState.value.partyState.partyA
: roleKey === "B" : roleKey === "B"
? detailState.value.partyState.partyB ? detailState.value.partyState.partyB
: detailState.value.partyState.partyC; : detailState.value.partyState.partyC;
if (!party) return;
const rawPartner =
option?.raw ||
contractCompanyOptions.value.find((item) => item.value === companyName)?.raw;
if (rawPartner) {
Object.assign(party, mapPartnerToParty(rawPartner, roleKey, companyName));
refreshPartyDisplayMeta();
return;
}
const profile = companyProfileMap[companyName]; const profile = companyProfileMap[companyName];
if (!party || !profile) return; if (!profile) {
party.companyName = companyName;
return;
}
Object.assign(party, { Object.assign(party, {
companyName, companyName,
@@ -1186,9 +1566,20 @@ function handleCompanySelect(roleKey, companyName) {
legalRepresentative: legalRepresentative:
party.legalRepresentative || profile.legalRepresentative, party.legalRepresentative || profile.legalRepresentative,
creditCode: party.creditCode || profile.creditCode, creditCode: party.creditCode || profile.creditCode,
oppCharacter:
party.oppCharacter ||
(String(party.companyNature || "").includes("个人") ? "2" : "1"),
}); });
} }
function handleContractTypeChange(value) {
detailState.value.contractTypeCode = value;
}
function handleContractTypeCodeChange(value) {
detailState.value.contractType = value;
}
function handleAddCounterparty() { function handleAddCounterparty() {
if (counterpartyRoleKeys.value.length >= MAX_COUNTERPARTY_COUNT) { if (counterpartyRoleKeys.value.length >= MAX_COUNTERPARTY_COUNT) {
message.warning(`最多支持添加 ${MAX_COUNTERPARTY_COUNT} 个相对方`); message.warning(`最多支持添加 ${MAX_COUNTERPARTY_COUNT} 个相对方`);
@@ -1229,15 +1620,39 @@ function handleRemoveCounterparty(roleKey) {
} }
function handleOtherAttachmentUpload(file) { function handleOtherAttachmentUpload(file) {
return uploadAttachmentFile(file, "other");
}
function handleGistAttachmentUpload(file) {
return uploadAttachmentFile(file, "gist");
}
function uploadAttachmentFile(file, category) {
const formData = new FormData();
formData.append("file", file);
uploadLaunchFile(formData).then((res) => {
if (String(res?.code || "") !== "0000") {
message.error(res?.message || res?.msg || "附件上传失败");
return;
}
const item = { const item = {
uid: `other_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`, uid: `other_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`,
name: file.name, name: file.name,
size: file.size, size: file.size,
status: "done", status: "done",
filePath: res?.result || "",
fileType: file.name.split(".").pop()?.toLowerCase() || "",
fileCategory: category,
}; };
if (category === "gist") {
detailState.value.contractGistFileList.push(item);
gistAttachmentList.value.push(item);
} else {
detailState.value.otherAttachmentList.push(item); detailState.value.otherAttachmentList.push(item);
otherAttachmentList.value.push(item); otherAttachmentList.value.push(item);
}
message.success(`已添加附件:${file.name}`); message.success(`已添加附件:${file.name}`);
});
return false; return false;
} }
@@ -1251,6 +1666,16 @@ function handleRemoveOtherAttachment(file) {
); );
} }
function handleRemoveGistAttachment(file) {
detailState.value.contractGistFileList =
detailState.value.contractGistFileList.filter(
(item) => item.uid !== file.uid && item.name !== file.name,
);
gistAttachmentList.value = gistAttachmentList.value.filter(
(item) => item.uid !== file.uid && item.name !== file.name,
);
}
function openBodyConfigModal() { function openBodyConfigModal() {
bodyConfigModalRef.value?.showModal( bodyConfigModalRef.value?.showModal(
{ {
@@ -1305,10 +1730,22 @@ function validateState() {
if (!detailState.value.valuationMode) return "请选择计价方式"; if (!detailState.value.valuationMode) return "请选择计价方式";
if (!detailState.value.currencyName) return "请选择币种"; if (!detailState.value.currencyName) return "请选择币种";
if (!detailState.value.sealType) return "请选择签订方式"; if (!detailState.value.sealType) return "请选择签订方式";
if (!detailState.value.textSource) return "请选择文本来源";
if (!detailState.value.mandateType) return "请选择委托类型"; if (!detailState.value.mandateType) return "请选择委托类型";
if (!detailState.value.cargoType) return "请选择货种"; if (!detailState.value.cargoType) return "请选择货种";
if (!detailState.value.paymentDirection) return "请选择收支方向";
if (!detailState.value.isElectron) return "请选择签署方式";
if (!detailState.value.contractTypeCode) return "请选择合同属性中的合同类型"; if (!detailState.value.contractTypeCode) return "请选择合同属性中的合同类型";
if (!detailState.value.executorAccount?.trim()) return "请填写合同执行人"; if (!detailState.value.executorAccount?.trim()) return "请填写合同执行人";
if (!detailState.value.contractBodyFileName?.trim())
return "请先配置合同正文";
for (const plan of detailState.value.performancePlans || []) {
if (!plan.planMatter?.trim()) return "请填写履行计划的履行事项";
if (!plan.planWhere?.trim()) return "请填写履行计划的履约条件";
if (!plan.planTime) return "请选择履行计划的节点日期";
if (plan.planDay === null || plan.planDay === undefined || plan.planDay === "")
return "请填写履行计划的提醒提前天数";
}
return ""; return "";
} }
@@ -1327,16 +1764,171 @@ function handleSave() {
? null ? null
: Number(detailState.value.contractAmount); : Number(detailState.value.contractAmount);
const record = buildLaunchRecord();
emit("save", { emit("save", {
detailState: copyContractFormData(detailState.value), mode: modalMode.value,
creationMode: creationMode.value, record,
templateKey: templateKey.value,
templateName: templateName.value,
}); });
visible.value = false; visible.value = false;
message.success("合同方信息已保存"); message.success("合同方信息已保存");
} }
function buildLaunchRecord() {
const partyList = [];
const partyA = detailState.value.partyState.partyA || {};
partyList.push({
roleCode: "A",
roleName: "甲方",
companyName: partyA.companyName,
companyType: partyA.companyType,
oppositeCharacter:
partyA.oppCharacter ||
(String(partyA.companyNature || "").includes("个人") ? "2" : "1"),
contactName: partyA.contactName,
contactPhone: partyA.contactPhone,
contactEmail: partyA.contactEmail,
contactAddress: partyA.contactAddress,
registeredAddress: partyA.registeredAddress,
legalRepresentative: partyA.legalRepresentative,
creditCode: partyA.creditCode,
bankName: partyA.bankName,
bankAccount: partyA.bankAccount,
naturalPersonIdType: partyA.naturalPersonIdType,
naturalPersonIdNumber: partyA.naturalPersonIdNumber,
});
counterpartyParties.value.forEach((party, index) => {
partyList.push({
roleCode: index === 0 ? "B" : "C",
roleName: index === 0 ? "乙方" : "丙方",
partnerId: party.partnerId || party.customerSysid || null,
oppositeId: party.oppositeId,
companyName: party.companyName,
companyType: party.companyType,
oppositeCharacter:
party.oppCharacter ||
(String(party.companyNature || "").includes("个人") ? "2" : "1"),
contactName: party.contactName,
contactPhone: party.contactPhone,
contactEmail: party.contactEmail,
contactAddress: party.contactAddress,
registeredAddress: party.registeredAddress,
legalRepresentative: party.legalRepresentative,
creditCode: party.creditCode,
bankName: party.bankName,
bankAccount: party.bankAccount,
naturalPersonIdType: party.naturalPersonIdType,
naturalPersonIdNumber: party.naturalPersonIdNumber,
isGroupInternal: party.isGroupInternal ? "1" : "0",
isHongKongMacaoTaiwan: party.isHongKongMacaoTaiwan ? "1" : "0",
});
});
const fileList = [];
if (detailState.value.contractBodyFileName) {
fileList.push({
fileCategory: "body",
fileName: detailState.value.contractBodyFileName,
filePath: detailState.value.contractBodyFilePath || "",
fileType: detailState.value.contractBodyFileType || "docx",
fileSize: detailState.value.contractBodyFileSize || null,
});
}
(detailState.value.contractGistFileList || []).forEach((item) => {
fileList.push({
fileCategory: "gist",
fileName: item.name || item.fileName,
filePath: item.filePath || "",
fileType: item.fileType || "",
fileSize: item.size ?? item.fileSize ?? 0,
});
});
(detailState.value.otherAttachmentList || []).forEach((item) => {
fileList.push({
fileCategory: "other",
fileName: item.name || item.fileName,
filePath: item.filePath || "",
fileType: item.fileType || "",
fileSize: item.size ?? item.fileSize ?? 0,
});
});
const extraAmountPayload = buildExtraAmountPayload(detailState.value.extraAmounts);
return {
id: detailState.value.id || null,
contractCode: detailState.value.contractCode,
contractName: detailState.value.contractName,
contractType: getOptionLabel(contractTypeOptions, detailState.value.contractType),
contractTypeCode: detailState.value.contractTypeCode || detailState.value.contractType,
contractSource: detailState.value.contractSource || "ECP",
creationMode: creationMode.value,
templateId: templateKey.value || null,
templateName: templateName.value || "",
contractAmount: detailState.value.contractAmount,
currencyName: detailState.value.currencyName,
contractPeriodType: detailState.value.contractPeriodType,
periodExplain: detailState.value.periodExplain,
effectiveStart: detailState.value.effectiveStart
? `${detailState.value.effectiveStart} 00:00:00`
: null,
effectiveEnd: detailState.value.effectiveEnd
? `${detailState.value.effectiveEnd} 23:59:59`
: null,
paymentMethod: detailState.value.paymentMethod,
primaryContent: detailState.value.primaryContent,
amountExplain: detailState.value.amountExplain,
valuationMode: detailState.value.valuationMode,
sealType: detailState.value.sealType,
textSource:
detailState.value.textSource ||
(creationMode.value === "manual" ? "draftBySelf" : "standardText"),
mandateType: detailState.value.mandateType,
cargoType: detailState.value.cargoType,
assistDepartmentName: detailState.value.assistDept,
executorAccount: detailState.value.executorAccount,
creatorPhone: detailState.value.executorAccount,
selfCode: detailState.value.selfCode || "",
mainContractCode: detailState.value.mainContractCode || "",
dateType: detailState.value.dateType || "0",
isAddit: 2,
isElectron: detailState.value.isElectron || "1",
paymentDirection: detailState.value.paymentDirection || "0",
isMajorContract: detailState.value.isMajorContract ? 1 : 0,
isOpenBidding: detailState.value.isPublicTender ? 1 : 0,
isYearBudget: detailState.value.isIncludedInCurrentYearBudget ? 1 : 0,
isShare: detailState.value.isShared ? "1" : "0",
contractNature: detailState.value.isForeignRelatedSubject ? "1" : "0",
isSgat: detailState.value.isInvolveHongKongMacaoTaiwan ? "1" : "0",
contractGistRemark: detailState.value.contractGistRemark || "",
bodySummary: detailState.value.bodySummary || detailState.value.primaryContent || "",
contractTextFileName: detailState.value.contractBodyFileName || "",
contractTextFilePath: detailState.value.contractBodyFilePath || "",
contractTextFileType: detailState.value.contractBodyFileType || "docx",
contractTextFileSize: detailState.value.contractBodyFileSize || null,
signDate: formatDateTimeValue(detailState.value.signDate),
invoiceDate: formatDateTimeValue(detailState.value.invoiceDate),
receiveDate: formatDateTimeValue(detailState.value.receiveDate),
remindDays: 3,
signingSubjectCode: detailState.value.signingSubjectCode || "",
extraAmounts: { ...detailState.value.extraAmounts },
...extraAmountPayload,
partyList,
fileList,
performancePlans: (detailState.value.performancePlans || []).map((item) => ({
planMatter: item.planMatter,
planWhere: item.planWhere,
planTime: formatDateTimeValue(item.planTime),
planDay: item.planDay,
planMoney: item.planMoney,
planSubjectCode: item.planSubjectCode,
planSubjectName: item.planSubjectName,
planDetail: item.planDetail,
planStatus: "0",
})),
};
}
function showModal(context = {}, mode = "create") { function showModal(context = {}, mode = "create") {
modalMode.value = mode || "create"; modalMode.value = mode || "create";
const source = context.detailState || const source = context.detailState ||
@@ -1350,9 +1942,21 @@ function showModal(context = {}, mode = "create") {
detailState.value = copyContractFormData(source); detailState.value = copyContractFormData(source);
syncCounterpartyState(); syncCounterpartyState();
syncUploadLists(); syncUploadLists();
loadPartnerOptions();
visible.value = true; visible.value = true;
} }
function loadPartnerOptions() {
queryPartnerOptions().then((list) => {
const options = (list || []).map((item) => ({
label: `${item.customerNameC || "未命名相对方"}${item.oppositeId ? `${item.oppositeId}` : ""}`,
value: item.customerNameC,
raw: item,
}));
contractCompanyOptions.value = options.length ? options : contractCompanyOptions.value;
});
}
defineExpose({ defineExpose({
showModal, showModal,
}); });
@@ -71,11 +71,12 @@
v-for="field in launchFieldList" v-for="field in launchFieldList"
:key="field.key" :key="field.key"
:label="field.label" :label="field.label"
required :required="!field.readonly"
> >
<a-input <a-input
:value="getLaunchFieldValue(field.key)" :value="getLaunchFieldValue(field.key)"
:placeholder="`请输入${field.label}`" :placeholder="`请输入${field.label}`"
:readonly="field.readonly"
@update:value="setLaunchFieldValue(field.key, $event)" @update:value="setLaunchFieldValue(field.key, $event)"
/> />
</a-form-item> </a-form-item>
@@ -196,6 +197,20 @@ import {
downloadTemplateFile, downloadTemplateFile,
queryPublishedManifestList, queryPublishedManifestList,
} from "@/api/manifest.js"; } from "@/api/manifest.js";
import { uploadLaunchFile } from "@/api/launch.js";
import {
contractSourceOptions,
contractTypeOptions,
currencyOptions,
dateTypeOptions,
extraAmountFieldOptions,
getOptionLabel,
isElectronOptions,
paymentDirectionOptions,
sealTypeOptions,
textSourceOptions,
valuationModeOptions,
} from "../contractLaunchOptions.js";
// ======================================================================== // ========================================================================
// 以下为表单模型 + 字典 + 工具函数,直接内联在此文件中。 // 以下为表单模型 + 字典 + 工具函数,直接内联在此文件中。
@@ -208,68 +223,68 @@ const contractCreateModeOptions = [
{ label: '手动创建', value: 'manual' }, { label: '手动创建', value: 'manual' },
] ]
// ========== 合同类型选项 ==========
const contractTypeOptions = [
{ label: '采购合同', value: '采购合同' },
{ label: '销售合同', value: '销售合同' },
{ label: '服务合同', value: '服务合同' },
{ label: '框架合同', value: '框架合同' },
]
// ========== 付款方式选项 ==========
const paymentMethodOptions = [
{ label: '银行转账(30%预付款,70%验收后支付)', value: '银行转账(30%预付款,70%验收后支付)' },
{ label: '银行转账(按月结算)', value: '银行转账(按月结算)' },
{ label: '银行转账(一次性结清)', value: '银行转账(一次性结清)' },
]
// ========== 计价方式选项 ==========
const valuationModeOptions = [
{ label: '无金额', value: '0' },
{ label: '固定总价', value: '1' },
{ label: '预估总价', value: '2' },
{ label: '无法预估总价', value: '3' },
{ label: '固定单价', value: '4' },
]
// ========== 币种选项 ==========
const currencyOptions = [
{ label: '人民币', value: 'RMB' },
{ label: '美元', value: 'USD' },
{ label: '港币', value: 'HKD' },
{ label: '欧元', value: 'EUR' },
{ label: '印尼盾', value: 'IDR' },
]
// ========== 签订方式选项 ==========
const sealTypeOptions = [
{ label: '公章', value: 'GZ' },
{ label: '合同专用章', value: 'HTZYZ' },
{ label: '公章+法定代表人私章', value: 'GZFDDBRSZ' },
{ label: '公章+法定代表人签字', value: 'GZFDDBRQZ' },
{ label: '公章+被授权人签字', value: 'GZBSQRQZ' },
{ label: '合同专用章+被授权人签字', value: 'HTZYZBSQRQZ' },
{ label: '合同专用章+法定代表人签字', value: 'HTZYZFDDBRQZ' },
{ label: '其他', value: 'QT' },
]
// ========== 可绑定业务字段列表(用于映射下拉和展示) ========== // ========== 可绑定业务字段列表(用于映射下拉和展示) ==========
const templateFieldList = [ const templateFieldList = [
{ value: 'contractName', label: '合同名称' }, { value: 'contractName', label: '合同名称' },
{ value: 'contractCode', label: '合同编码' }, { value: 'contractCode', label: '合同编码' },
{ value: 'contractType', label: '合同类型' }, { value: 'contractType', label: '合同类型' },
{ value: 'contractTypeCode', label: '合同类型编码' },
{ value: 'contractAmountDisplay', label: '合同金额' }, { value: 'contractAmountDisplay', label: '合同金额' },
{ value: 'contractAmount', label: '合同金额数字' },
{ value: 'effectiveStart', label: '有效期起' }, { value: 'effectiveStart', label: '有效期起' },
{ value: 'effectiveEnd', label: '有效期止' }, { value: 'effectiveEnd', label: '有效期止' },
{ value: 'effectivePeriod', label: '有效期区间' }, { value: 'effectivePeriod', label: '有效期区间' },
{ value: 'contractPeriodTypeLabel', label: '合同期限类型' },
{ value: 'periodExplain', label: '期限说明' },
{ value: 'paymentMethod', label: '付款方式' }, { value: 'paymentMethod', label: '付款方式' },
{ value: 'paymentDirectionLabel', label: '收支方向' },
{ value: 'valuationModeLabel', label: '计价方式' },
{ value: 'currencyNameLabel', label: '币种' },
{ value: 'sealTypeLabel', label: '签订方式' },
{ value: 'textSourceLabel', label: '文本来源' },
{ value: 'isElectronLabel', label: '签署方式' },
{ value: 'contractSourceLabel', label: '合同来源' },
{ value: 'dateTypeLabel', label: '固定期限日期类型' },
{ value: 'mandateType', label: '委托类型' },
{ value: 'cargoType', label: '货种' },
{ value: 'primaryContent', label: '合同标的说明' }, { value: 'primaryContent', label: '合同标的说明' },
{ value: 'amountExplain', label: '业务备注' }, { value: 'amountExplain', label: '金额说明' },
{ value: 'contractGistRemark', label: '签订依据说明' },
{ value: 'bodySummary', label: '正文摘要' },
{ value: 'assistDept', label: '协助部门' },
{ value: 'executorAccount', label: '合同执行人' },
{ value: 'selfCode', label: '二级单位合同编码' },
{ value: 'mainContractCode', label: '主合同编码' },
{ value: 'signingSubjectCode', label: '签约主体编码' },
{ value: 'invoiceDate', label: '开票时间' },
{ value: 'receiveDate', label: '收款时间' },
{ value: 'partyAName', label: '甲方名称' }, { value: 'partyAName', label: '甲方名称' },
{ value: 'partyBName', label: '乙方名称' }, { value: 'partyBName', label: '乙方名称' },
{ value: 'partyCName', label: '丙方名称' }, { value: 'partyCName', label: '丙方名称' },
{ value: 'partyAContactName', label: '甲方联系人' },
{ value: 'partyBContactName', label: '乙方联系人' },
{ value: 'partyCContactName', label: '丙方联系人' },
{ value: 'partyAContactPhone', label: '甲方电话' },
{ value: 'partyBContactPhone', label: '乙方电话' },
{ value: 'partyCContactPhone', label: '丙方电话' },
{ value: 'partyAAddress', label: '甲方地址' },
{ value: 'partyBAddress', label: '乙方地址' },
{ value: 'partyCAddress', label: '丙方地址' },
{ value: 'partyACreditCode', label: '甲方统一社会信用代码' },
{ value: 'partyBCreditCode', label: '乙方统一社会信用代码' },
{ value: 'partyCCreditCode', label: '丙方统一社会信用代码' },
{ value: 'partyABankName', label: '甲方开户行' },
{ value: 'partyBBankName', label: '乙方开户行' },
{ value: 'partyCBankName', label: '丙方开户行' },
{ value: 'partyABankAccount', label: '甲方银行账号' },
{ value: 'partyBBankAccount', label: '乙方银行账号' },
{ value: 'partyCBankAccount', label: '丙方银行账号' },
{ value: 'counterpartySummary', label: '合同方摘要' }, { value: 'counterpartySummary', label: '合同方摘要' },
{ value: 'attachmentSummary', label: '附件情况' }, { value: 'attachmentSummary', label: '附件情况' },
{ value: 'contractGistFileSummary', label: '签订依据附件' },
{ value: 'contractBodyFileName', label: '合同正文文件名' },
{ value: 'extraAmountSummary', label: '额外金额' },
{ value: 'performancePlanSummary', label: '履行计划' },
{ value: 'templateLabel', label: '模板名称' }, { value: 'templateLabel', label: '模板名称' },
{ value: 'signDate', label: '签署日期' }, { value: 'signDate', label: '签署日期' },
] ]
@@ -283,19 +298,154 @@ function formatContractAmount(value) {
}).format(Number(value))} 元` }).format(Number(value))} 元`
} }
function getContractPeriodTypeLabel(value) {
if (value === '1') return '无固定期限'
return '固定期限'
}
function normalizeFileItem(item = {}) {
return {
uid: item.uid || `file_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`,
name: item.name || item.fileName || '',
size: item.size ?? item.fileSize ?? 0,
status: item.status || 'done',
filePath: item.filePath || '',
fileType: item.fileType || '',
fileCategory: item.fileCategory || '',
}
}
function splitFilesByCategory(source = {}) {
const sourceFiles = source.fileList || []
const bodyFile = sourceFiles.find((item) => item.fileCategory === 'body') || {}
return {
contractBodyFileName:
source.contractBodyFileName ||
source.contractTextFileName ||
bodyFile.fileName ||
'',
contractBodyFilePath:
source.contractBodyFilePath ||
source.contractTextFilePath ||
bodyFile.filePath ||
'',
contractBodyFileType:
source.contractBodyFileType ||
source.contractTextFileType ||
bodyFile.fileType ||
'',
contractBodyFileSize:
source.contractBodyFileSize ??
source.contractTextFileSize ??
bodyFile.fileSize ??
null,
otherAttachmentList: source.otherAttachmentList?.length
? source.otherAttachmentList.map(normalizeFileItem)
: sourceFiles
.filter((item) => item.fileCategory === 'other')
.map(normalizeFileItem),
contractGistFileList: source.contractGistFileList?.length
? source.contractGistFileList.map(normalizeFileItem)
: sourceFiles
.filter((item) => item.fileCategory === 'gist')
.map(normalizeFileItem),
}
}
function formatFileSummary(fileList = []) {
return fileList.length
? fileList.map((item) => item.name || item.fileName).filter(Boolean).join('')
: '暂无附件'
}
function buildExtraAmountSummary(extraAmounts = {}) {
const parts = extraAmountFieldOptions
.filter((item) => extraAmounts[`${item.key}Enabled`])
.map((item) => `${item.label}${formatContractAmount(extraAmounts[item.key]) || '0.00 元'}`)
return parts.length ? parts.join('') : '无'
}
function restoreExtraAmounts(source = {}, current = {}) {
const result = { ...current, ...(source.extraAmounts || {}) }
const selectedCodes = String(source.ewAmountType || '')
.split(',')
.map((item) => item.trim())
.filter(Boolean)
extraAmountFieldOptions.forEach((item) => {
const amount = source[item.amountField]
if (selectedCodes.includes(item.code) || amount || amount === 0) {
result[`${item.key}Enabled`] = selectedCodes.includes(item.code) || amount > 0
if (amount !== undefined && amount !== null) {
result[item.key] = amount
}
}
})
return result
}
function buildPerformancePlanSummary(list = []) {
if (!list.length) return '暂无履行计划'
return list
.map((item, index) => {
const money = item.planMoney || item.planMoney === 0 ? `,金额${formatContractAmount(item.planMoney)}` : ''
return `${index + 1}. ${item.planMatter || '未填写事项'}${item.planWhere || '未填写条件'}${item.planTime || '未填写日期'}${money}`
})
.join('\n')
}
// ========== 合同方相关函数 ========== // ========== 合同方相关函数 ==========
/** 深拷贝合同方状态 */ /** 深拷贝合同方状态 */
function copyPartyState(input) { function copyPartyState(input) {
const source = input || {} const source = input || {}
const listState = buildPartyStateFromList(source.partyList)
return { return {
isThreePartyContract: Boolean(source.isThreePartyContract), isThreePartyContract: Boolean(source.isThreePartyContract),
partyA: { roleKey: 'A', roleName: '甲方', companyName: '', ...(source.partyA || source.parties?.A || {}) }, partyA: { roleKey: 'A', roleName: '甲方', companyName: '', ...(source.partyA || source.parties?.A || {}), ...(listState?.partyA || {}) },
partyB: { roleKey: 'B', roleName: '乙方', companyName: '', ...(source.partyB || source.parties?.B || {}) }, partyB: { roleKey: 'B', roleName: '乙方', companyName: '', ...(source.partyB || source.parties?.B || {}), ...(listState?.partyB || {}) },
partyC: { roleKey: 'C', roleName: '丙方', companyName: '', ...(source.partyC || source.parties?.C || {}) }, partyC: { roleKey: 'C', roleName: '丙方', companyName: '', ...(source.partyC || source.parties?.C || {}), ...(listState?.partyC || {}) },
...(listState ? { isThreePartyContract: listState.isThreePartyContract } : {}),
} }
} }
function mapLaunchPartyToFormParty(item = {}) {
return {
roleKey: item.roleCode || 'B',
roleName: item.roleCode === 'A' ? '甲方' : item.roleCode === 'C' ? '丙方' : '乙方',
partnerId: item.partnerId || null,
oppositeId: item.oppositeId || '',
companyName: item.companyName || '',
companyType: item.companyType || '',
oppCharacter: item.oppositeCharacter || '',
contactName: item.contactName || '',
contactPhone: item.contactPhone || '',
contactEmail: item.contactEmail || '',
contactAddress: item.contactAddress || '',
registeredAddress: item.registeredAddress || '',
legalRepresentative: item.legalRepresentative || '',
creditCode: item.creditCode || '',
bankName: item.bankName || '',
bankAccount: item.bankAccount || '',
naturalPersonIdType: item.naturalPersonIdType || '',
naturalPersonIdNumber: item.naturalPersonIdNumber || '',
isGroupInternal: item.isGroupInternal === '1' || item.isGroupInternal === true,
isHongKongMacaoTaiwan: item.isHongKongMacaoTaiwan === '1' || item.isHongKongMacaoTaiwan === true,
}
}
function buildPartyStateFromList(list = []) {
if (!Array.isArray(list) || !list.length) return null
const state = { partyA: {}, partyB: {}, partyC: {}, isThreePartyContract: false }
list.forEach((item) => {
const key = item.roleCode === 'A' ? 'partyA' : item.roleCode === 'C' ? 'partyC' : 'partyB'
state[key] = mapLaunchPartyToFormParty(item)
})
state.isThreePartyContract = Boolean(state.partyB.companyName && state.partyC.companyName)
return state
}
/** 获取有效的合同方列表 */ /** 获取有效的合同方列表 */
function getPartyList(partyState) { function getPartyList(partyState) {
const state = copyPartyState(partyState) const state = copyPartyState(partyState)
@@ -366,6 +516,8 @@ function createPerformancePlan() {
planTime: '', planTime: '',
planDay: 0, planDay: 0,
planMoney: '', planMoney: '',
planSubjectCode: '',
planSubjectName: '',
planDetail: '', planDetail: '',
} }
} }
@@ -386,6 +538,11 @@ function createContractFormData() {
contractPeriodType: '0', // 合同期限类型: 0-固定期限 1-无固定期限 contractPeriodType: '0', // 合同期限类型: 0-固定期限 1-无固定期限
periodExplain: '', // 期限说明(无固定期限时填写) periodExplain: '', // 期限说明(无固定期限时填写)
paymentMethod: '', // 付款方式 paymentMethod: '', // 付款方式
paymentDirection: '0', // 收支方向
isElectron: '1', // 签署方式: 0-纸质 1-电子
textSource: 'standardText', // 文本来源
contractSource: 'ECP', // 合同来源
dateType: '0', // 固定期间日期类型:0-年 1-月 2-日
valuationMode: '', // 计价方式: 0-无金额 1-固定总价 2-预估总价 3-无法预估总价 4-固定单价 valuationMode: '', // 计价方式: 0-无金额 1-固定总价 2-预估总价 3-无法预估总价 4-固定单价
currencyName: '', // 币种: RMB/USD/HKD/EUR/IDR currencyName: '', // 币种: RMB/USD/HKD/EUR/IDR
sealType: '', // 签订方式: GZ/HTZYZ/... sealType: '', // 签订方式: GZ/HTZYZ/...
@@ -393,13 +550,24 @@ function createContractFormData() {
cargoType: '', // 货种 cargoType: '', // 货种
primaryContent: '', // 合同主要内容 primaryContent: '', // 合同主要内容
amountExplain: '', // 金额说明 amountExplain: '', // 金额说明
contractGistRemark: '', // 签订依据说明
bodySummary: '', // 正文摘要
assistDept: '', // 协助部门 assistDept: '', // 协助部门
executorAccount: '', // 合同执行人手机号 executorAccount: '', // 合同执行人手机号
selfCode: '', // 二级单位合同编码
mainContractCode: '', // 主合同编码
signingSubjectCode: '', // 签约主体编码
invoiceDate: '', // 开票时间
receiveDate: '', // 收款时间
// 文件 // 文件
contractBodyFileName: '', // 合同正文文件名 contractBodyFileName: '', // 合同正文文件名
contractBodyFilePath: '',
contractBodyFileType: '',
contractBodyFileSize: null,
contractBodyArrayBuffer: null, // 合同正文文件二进制 contractBodyArrayBuffer: null, // 合同正文文件二进制
otherAttachmentList: [], // 其他附件列表 otherAttachmentList: [], // 其他附件列表
contractGistFileList: [], // 签订依据附件列表
// 合同方 // 合同方
partyState: { partyState: {
@@ -424,6 +592,11 @@ function createContractFormData() {
advanceEnabled: false, advance: 0, advanceEnabled: false, advance: 0,
guaranteeEnabled: false, guarantee: 0, guaranteeEnabled: false, guarantee: 0,
}, },
ewAmountType: '',
djAmount: 0,
yjAmount: 0,
yfkAmount: 0,
bzjAmount: 0,
// 履行计划 // 履行计划
performancePlans: [], performancePlans: [],
@@ -437,17 +610,23 @@ function createContractFormData() {
function copyContractFormData(data = {}) { function copyContractFormData(data = {}) {
const form = createContractFormData() const form = createContractFormData()
const source = data || {} const source = data || {}
const fileState = splitFilesByCategory(source)
form.contractName = source.contractName ?? form.contractName form.contractName = source.contractName ?? form.contractName
form.contractCode = source.contractCode ?? form.contractCode form.contractCode = source.contractCode ?? form.contractCode
form.contractType = source.contractType ?? form.contractType form.contractType = source.contractType ?? form.contractType
form.contractTypeCode = source.contractTypeCode ?? source.contractType ?? form.contractTypeCode form.contractTypeCode = source.contractTypeCode ?? source.contractType ?? form.contractTypeCode
form.contractAmount = source.contractAmount ?? form.contractAmount form.contractAmount = source.contractAmount ?? form.contractAmount
form.effectiveStart = source.effectiveStart ?? form.effectiveStart form.effectiveStart = source.effectiveStart ? String(source.effectiveStart).slice(0, 10) : form.effectiveStart
form.effectiveEnd = source.effectiveEnd ?? form.effectiveEnd form.effectiveEnd = source.effectiveEnd ? String(source.effectiveEnd).slice(0, 10) : form.effectiveEnd
form.contractPeriodType = source.contractPeriodType ?? form.contractPeriodType form.contractPeriodType = source.contractPeriodType ?? form.contractPeriodType
form.periodExplain = source.periodExplain ?? form.periodExplain form.periodExplain = source.periodExplain ?? form.periodExplain
form.paymentMethod = source.paymentMethod ?? form.paymentMethod form.paymentMethod = source.paymentMethod ?? form.paymentMethod
form.paymentDirection = source.paymentDirection ?? form.paymentDirection
form.isElectron = source.isElectron ?? form.isElectron
form.textSource = source.textSource ?? form.textSource
form.contractSource = source.contractSource ?? form.contractSource
form.dateType = source.dateType ?? form.dateType
form.valuationMode = source.valuationMode ?? form.valuationMode form.valuationMode = source.valuationMode ?? form.valuationMode
form.currencyName = source.currencyName ?? form.currencyName form.currencyName = source.currencyName ?? form.currencyName
form.sealType = source.sealType ?? form.sealType form.sealType = source.sealType ?? form.sealType
@@ -455,31 +634,48 @@ function copyContractFormData(data = {}) {
form.cargoType = source.cargoType ?? form.cargoType form.cargoType = source.cargoType ?? form.cargoType
form.primaryContent = source.primaryContent ?? form.primaryContent form.primaryContent = source.primaryContent ?? form.primaryContent
form.amountExplain = source.amountExplain ?? form.amountExplain form.amountExplain = source.amountExplain ?? form.amountExplain
form.assistDept = source.assistDept ?? form.assistDept form.contractGistRemark = source.contractGistRemark ?? form.contractGistRemark
form.bodySummary = source.bodySummary ?? form.bodySummary
form.assistDept = source.assistDept ?? source.assistDepartmentName ?? form.assistDept
form.executorAccount = source.executorAccount ?? form.executorAccount form.executorAccount = source.executorAccount ?? form.executorAccount
form.contractBodyFileName = source.contractBodyFileName ?? form.contractBodyFileName form.selfCode = source.selfCode ?? form.selfCode
form.mainContractCode = source.mainContractCode ?? form.mainContractCode
form.signingSubjectCode = source.signingSubjectCode ?? source.orgId ?? form.signingSubjectCode
form.invoiceDate = source.invoiceDate ? String(source.invoiceDate).slice(0, 10) : form.invoiceDate
form.receiveDate = source.receiveDate ? String(source.receiveDate).slice(0, 10) : form.receiveDate
form.contractBodyFileName = fileState.contractBodyFileName
form.contractBodyFilePath = fileState.contractBodyFilePath
form.contractBodyFileType = fileState.contractBodyFileType
form.contractBodyFileSize = fileState.contractBodyFileSize
form.contractBodyArrayBuffer = source.contractBodyArrayBuffer form.contractBodyArrayBuffer = source.contractBodyArrayBuffer
? (source.contractBodyArrayBuffer instanceof ArrayBuffer ? source.contractBodyArrayBuffer.slice(0) : null) ? (source.contractBodyArrayBuffer instanceof ArrayBuffer ? source.contractBodyArrayBuffer.slice(0) : null)
: null : null
form.otherAttachmentList = [...(source.otherAttachmentList || source.attachmentList || [])] form.otherAttachmentList = fileState.otherAttachmentList
form.partyState = copyPartyState(source.partyState) form.contractGistFileList = fileState.contractGistFileList
form.partyState = copyPartyState(source.partyState || source)
form.isInvolveHongKongMacaoTaiwan = source.isInvolveHongKongMacaoTaiwan ?? form.isInvolveHongKongMacaoTaiwan form.isInvolveHongKongMacaoTaiwan = source.isInvolveHongKongMacaoTaiwan ?? form.isInvolveHongKongMacaoTaiwan
form.isPublicTender = source.isPublicTender ?? form.isPublicTender form.isPublicTender = source.isPublicTender ?? form.isPublicTender
form.isForeignRelatedSubject = source.isForeignRelatedSubject ?? form.isForeignRelatedSubject form.isForeignRelatedSubject = source.isForeignRelatedSubject ?? form.isForeignRelatedSubject
form.isIncludedInCurrentYearBudget = source.isIncludedInCurrentYearBudget ?? form.isIncludedInCurrentYearBudget form.isIncludedInCurrentYearBudget = source.isIncludedInCurrentYearBudget ?? form.isIncludedInCurrentYearBudget
form.isShared = source.isShared ?? form.isShared form.isShared = source.isShared ?? form.isShared
form.isMajorContract = source.isMajorContract ?? form.isMajorContract form.isMajorContract = source.isMajorContract ?? form.isMajorContract
form.signDate = source.signDate ?? form.signDate form.signDate = source.signDate ? String(source.signDate).slice(0, 10) : form.signDate
form.extraAmounts = { form.extraAmounts = {
depositEnabled: false, deposit: 0, depositEnabled: false, deposit: 0,
pledgeEnabled: false, pledge: 0, pledgeEnabled: false, pledge: 0,
advanceEnabled: false, advance: 0, advanceEnabled: false, advance: 0,
guaranteeEnabled: false, guarantee: 0, guaranteeEnabled: false, guarantee: 0,
...(source.extraAmounts || {}),
} }
form.extraAmounts = restoreExtraAmounts(source, form.extraAmounts)
form.ewAmountType = source.ewAmountType ?? form.ewAmountType
form.djAmount = source.djAmount ?? form.djAmount
form.yjAmount = source.yjAmount ?? form.yjAmount
form.yfkAmount = source.yfkAmount ?? form.yfkAmount
form.bzjAmount = source.bzjAmount ?? form.bzjAmount
form.performancePlans = (source.performancePlans || []).map((item) => ({ form.performancePlans = (source.performancePlans || []).map((item) => ({
...createPerformancePlan(), ...createPerformancePlan(),
...item, ...item,
planTime: item.planTime ? String(item.planTime).slice(0, 10) : '',
})) }))
return form return form
@@ -490,28 +686,77 @@ function copyContractFormData(data = {}) {
/** 将合同表单数据转换为模板变量字典(用于 docxtemplater 渲染) */ /** 将合同表单数据转换为模板变量字典(用于 docxtemplater 渲染) */
function buildTemplatePreviewData(formData, templateLabel = '', creationMode = 'template') { function buildTemplatePreviewData(formData, templateLabel = '', creationMode = 'template') {
const form = copyContractFormData(formData) const form = copyContractFormData(formData)
const attachmentSummary = form.otherAttachmentList.length const attachmentSummary = formatFileSummary(form.otherAttachmentList)
? form.otherAttachmentList.map((item) => item.name).join('') const contractGistFileSummary = formatFileSummary(form.contractGistFileList)
: '暂无附件' const partyA = form.partyState.partyA || {}
const partyB = form.partyState.partyB || {}
const partyC = form.partyState.partyC || {}
return { return {
contractName: form.contractName || '未填写', contractName: form.contractName || '未填写',
contractCode: form.contractCode || '未填写', contractCode: form.contractCode || '未填写',
contractType: form.contractType || '未填写', contractType: getOptionLabel(contractTypeOptions, form.contractType) || form.contractType || '未填写',
contractTypeCode: form.contractTypeCode || '',
contractAmountDisplay: formatContractAmount(form.contractAmount), contractAmountDisplay: formatContractAmount(form.contractAmount),
contractAmount: form.contractAmount ?? '',
effectiveStart: form.effectiveStart || '--', effectiveStart: form.effectiveStart || '--',
effectiveEnd: form.effectiveEnd || '--', effectiveEnd: form.effectiveEnd || '--',
effectivePeriod: `${form.effectiveStart || '--'}${form.effectiveEnd || '--'}`, effectivePeriod: form.contractPeriodType === '1'
? form.periodExplain || '无固定期限'
: `${form.effectiveStart || '--'}${form.effectiveEnd || '--'}`,
contractPeriodTypeLabel: getContractPeriodTypeLabel(form.contractPeriodType),
periodExplain: form.periodExplain || '',
paymentMethod: form.paymentMethod || '未填写', paymentMethod: form.paymentMethod || '未填写',
paymentDirectionLabel: getOptionLabel(paymentDirectionOptions, form.paymentDirection),
valuationModeLabel: getOptionLabel(valuationModeOptions, form.valuationMode),
currencyNameLabel: getOptionLabel(currencyOptions, form.currencyName),
sealTypeLabel: getOptionLabel(sealTypeOptions, form.sealType),
textSourceLabel: getOptionLabel(textSourceOptions, form.textSource),
isElectronLabel: getOptionLabel(isElectronOptions, form.isElectron),
contractSourceLabel: getOptionLabel(contractSourceOptions, form.contractSource),
dateTypeLabel: getOptionLabel(dateTypeOptions, form.dateType),
mandateType: form.mandateType || '',
cargoType: form.cargoType || '',
primaryContent: form.primaryContent || form.cargoType || '', primaryContent: form.primaryContent || form.cargoType || '',
amountExplain: form.amountExplain || '', amountExplain: form.amountExplain || '',
contractGistRemark: form.contractGistRemark || '',
bodySummary: form.bodySummary || form.primaryContent || '',
assistDept: form.assistDept || '',
executorAccount: form.executorAccount || '',
selfCode: form.selfCode || '',
mainContractCode: form.mainContractCode || '',
signingSubjectCode: form.signingSubjectCode || '',
invoiceDate: form.invoiceDate || '',
receiveDate: form.receiveDate || '',
counterpartySummary: getPartySummary(form.partyState), counterpartySummary: getPartySummary(form.partyState),
attachmentSummary, attachmentSummary,
contractGistFileSummary,
contractBodyFileName: form.contractBodyFileName || '',
extraAmountSummary: buildExtraAmountSummary(form.extraAmounts),
performancePlanSummary: buildPerformancePlanSummary(form.performancePlans),
templateLabel: templateLabel || (creationMode === 'manual' ? '手动创建' : ''), templateLabel: templateLabel || (creationMode === 'manual' ? '手动创建' : ''),
signDate: form.signDate || dayjs().format('YYYY-MM-DD'), signDate: form.signDate || dayjs().format('YYYY-MM-DD'),
partyAName: form.partyState.partyA.companyName || '', partyAName: partyA.companyName || '',
partyBName: form.partyState.partyB.companyName || '', partyBName: partyB.companyName || '',
partyCName: form.partyState.isThreePartyContract ? form.partyState.partyC.companyName || '' : '', partyCName: form.partyState.isThreePartyContract ? partyC.companyName || '' : '',
partyAContactName: partyA.contactName || '',
partyBContactName: partyB.contactName || '',
partyCContactName: partyC.contactName || '',
partyAContactPhone: partyA.contactPhone || '',
partyBContactPhone: partyB.contactPhone || '',
partyCContactPhone: partyC.contactPhone || '',
partyAAddress: partyA.registeredAddress || partyA.contactAddress || '',
partyBAddress: partyB.registeredAddress || partyB.contactAddress || '',
partyCAddress: partyC.registeredAddress || partyC.contactAddress || '',
partyACreditCode: partyA.creditCode || '',
partyBCreditCode: partyB.creditCode || '',
partyCCreditCode: partyC.creditCode || '',
partyABankName: partyA.bankName || '',
partyBBankName: partyB.bankName || '',
partyCBankName: partyC.bankName || '',
partyABankAccount: partyA.bankAccount || '',
partyBBankAccount: partyB.bankAccount || '',
partyCBankAccount: partyC.bankAccount || '',
} }
} }
@@ -536,6 +781,11 @@ function buildContractSubmitPayload(formData, extra = {}) {
contractPeriodType: form.contractPeriodType, contractPeriodType: form.contractPeriodType,
periodExplain: form.periodExplain, periodExplain: form.periodExplain,
paymentMethod: form.paymentMethod, paymentMethod: form.paymentMethod,
paymentDirection: form.paymentDirection,
isElectron: form.isElectron,
textSource: form.textSource,
contractSource: form.contractSource,
dateType: form.dateType,
valuationMode: form.valuationMode, valuationMode: form.valuationMode,
currencyName: form.currencyName, currencyName: form.currencyName,
sealType: form.sealType, sealType: form.sealType,
@@ -543,8 +793,15 @@ function buildContractSubmitPayload(formData, extra = {}) {
cargoType: form.cargoType, cargoType: form.cargoType,
primaryContent: form.primaryContent, primaryContent: form.primaryContent,
amountExplain: form.amountExplain, amountExplain: form.amountExplain,
contractGistRemark: form.contractGistRemark,
bodySummary: form.bodySummary,
assistDept: form.assistDept, assistDept: form.assistDept,
executorAccount: form.executorAccount, executorAccount: form.executorAccount,
selfCode: form.selfCode,
mainContractCode: form.mainContractCode,
signingSubjectCode: form.signingSubjectCode,
invoiceDate: form.invoiceDate,
receiveDate: form.receiveDate,
}, },
partyMaintenance: { partyMaintenance: {
isThreePartyContract: form.partyState.isThreePartyContract ? 1 : 0, isThreePartyContract: form.partyState.isThreePartyContract ? 1 : 0,
@@ -557,16 +814,29 @@ function buildContractSubmitPayload(formData, extra = {}) {
startTime: form.effectiveStart, startTime: form.effectiveStart,
endTime: form.effectiveEnd, endTime: form.effectiveEnd,
periodExplain: form.periodExplain, periodExplain: form.periodExplain,
dateType: form.dateType,
valuationMode: form.valuationMode, valuationMode: form.valuationMode,
currencyName: form.currencyName, currencyName: form.currencyName,
sealType: form.sealType, sealType: form.sealType,
isText: form.textSource,
isElectron: form.isElectron,
paymentDirection: form.paymentDirection,
primaryContent: form.primaryContent, primaryContent: form.primaryContent,
amountExplain: form.amountExplain, amountExplain: form.amountExplain,
contractGistRemark: form.contractGistRemark,
isMajorContract: form.isMajorContract ? 1 : 0, isMajorContract: form.isMajorContract ? 1 : 0,
contractOpposites: buildContractOpposites(form.partyState), contractOpposites: buildContractOpposites(form.partyState),
otherAttach: form.otherAttachmentList.map((item) => ({ otherAttach: form.otherAttachmentList.map((item) => ({
fileName: item.name, fileName: item.name || item.fileName,
fileSize: item.size, filePath: item.filePath,
fileType: item.fileType,
fileSize: item.size ?? item.fileSize,
})),
contractGistFile: form.contractGistFileList.map((item) => ({
fileName: item.name || item.fileName,
filePath: item.filePath,
fileType: item.fileType,
fileSize: item.size ?? item.fileSize,
})), })),
contractBodyFileName: form.contractBodyFileName, contractBodyFileName: form.contractBodyFileName,
performancePlans: form.performancePlans, performancePlans: form.performancePlans,
@@ -639,14 +909,21 @@ const launchFieldList = computed(() => {
return fieldKeys.map((key) => ({ return fieldKeys.map((key) => ({
key, key,
label: templateFieldLabelMap.value[key] || key, label: templateFieldLabelMap.value[key] || key,
readonly: Object.prototype.hasOwnProperty.call(templateVariables.value, key),
})); }));
}); });
function getLaunchFieldValue(fieldKey) { function getLaunchFieldValue(fieldKey) {
if (Object.prototype.hasOwnProperty.call(templateVariables.value, fieldKey)) {
return templateVariables.value[fieldKey] ?? "";
}
return contractDetailState.value[fieldKey] ?? ""; return contractDetailState.value[fieldKey] ?? "";
} }
function setLaunchFieldValue(fieldKey, value) { function setLaunchFieldValue(fieldKey, value) {
if (Object.prototype.hasOwnProperty.call(templateVariables.value, fieldKey)) {
return;
}
contractDetailState.value[fieldKey] = value; contractDetailState.value[fieldKey] = value;
} }
@@ -713,7 +990,11 @@ watch(
if (mode === "template" && prevMode === "manual") { if (mode === "template" && prevMode === "manual") {
contractDetailState.value.contractBodyFileName = ""; contractDetailState.value.contractBodyFileName = "";
contractDetailState.value.contractBodyFilePath = "";
contractDetailState.value.contractBodyFileType = "";
contractDetailState.value.contractBodyFileSize = null;
contractDetailState.value.contractBodyArrayBuffer = null; contractDetailState.value.contractBodyArrayBuffer = null;
contractDetailState.value.textSource = "standardText";
syncManualBodyFileList(); syncManualBodyFileList();
} }
@@ -846,7 +1127,10 @@ async function buildFilledDocumentBlob() {
if (mappings.length) { if (mappings.length) {
mappings.forEach((item) => { mappings.forEach((item) => {
if (!item.variableKey) return; if (!item.variableKey) return;
renderData[item.variableKey] = sourceValues[item.bindField] ?? ""; renderData[item.variableKey] =
sourceValues[item.bindField] ??
contractDetailState.value[item.bindField] ??
"";
}); });
} else { } else {
Object.assign(renderData, sourceValues); Object.assign(renderData, sourceValues);
@@ -930,7 +1214,20 @@ async function handleManualContractBodyUpload(file) {
return false; return false;
} }
const formData = new FormData();
formData.append("file", file);
const uploadRes = await uploadLaunchFile(formData);
if (String(uploadRes?.code || "") !== "0000" || !uploadRes?.result) {
message.error(uploadRes?.message || uploadRes?.msg || "合同正文上传失败");
return false;
}
contractDetailState.value.contractBodyFileName = file.name; contractDetailState.value.contractBodyFileName = file.name;
contractDetailState.value.contractBodyFilePath = uploadRes.result;
contractDetailState.value.contractBodyFileType =
file.name.split(".").pop()?.toLowerCase() || "docx";
contractDetailState.value.contractBodyFileSize = file.size;
contractDetailState.value.textSource = "draftBySelf";
contractDetailState.value.contractBodyArrayBuffer = await readFileAsArrayBuffer( contractDetailState.value.contractBodyArrayBuffer = await readFileAsArrayBuffer(
file, file,
); );
@@ -944,6 +1241,9 @@ async function handleManualContractBodyUpload(file) {
function handleRemoveManualContractBody() { function handleRemoveManualContractBody() {
contractDetailState.value.contractBodyFileName = ""; contractDetailState.value.contractBodyFileName = "";
contractDetailState.value.contractBodyFilePath = "";
contractDetailState.value.contractBodyFileType = "";
contractDetailState.value.contractBodyFileSize = null;
contractDetailState.value.contractBodyArrayBuffer = null; contractDetailState.value.contractBodyArrayBuffer = null;
syncManualBodyFileList(); syncManualBodyFileList();
previewBlob.value = null; previewBlob.value = null;
@@ -952,11 +1252,38 @@ function handleRemoveManualContractBody() {
return true; return true;
} }
async function uploadBlobAsLaunchFile(blob, fileName) {
const file = new File([blob], fileName, {
type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
});
const formData = new FormData();
formData.append("file", file);
const uploadRes = await uploadLaunchFile(formData);
if (String(uploadRes?.code || "") !== "0000" || !uploadRes?.result) {
throw new Error(uploadRes?.message || uploadRes?.msg || "合同正文上传失败");
}
contractDetailState.value.contractBodyFileName = fileName;
contractDetailState.value.contractBodyFilePath = uploadRes.result;
contractDetailState.value.contractBodyFileType = "docx";
contractDetailState.value.contractBodyFileSize = file.size;
}
async function ensureTemplateBodyFileUploaded() {
if (creationMode.value !== "template") return;
const blob = previewBlob.value || (await buildFilledDocumentBlob());
previewBlob.value = blob;
const fileName = `${contractDetailState.value.contractName || "合同正文"}.docx`;
await uploadBlobAsLaunchFile(blob, fileName);
}
function validateMainForm() { function validateMainForm() {
if (creationMode.value === "template" && !templateKey.value) if (creationMode.value === "template" && !templateKey.value)
return "请选择合同模板"; return "请选择合同模板";
for (const field of launchFieldList.value) { for (const field of launchFieldList.value) {
if (field.readonly) {
continue;
}
const value = getLaunchFieldValue(field.key); const value = getLaunchFieldValue(field.key);
if (value === null || value === undefined || value === "") { if (value === null || value === undefined || value === "") {
return `请输入${field.label}`; return `请输入${field.label}`;
@@ -971,13 +1298,15 @@ function validateMainForm() {
return ""; return "";
} }
function handleSave() { async function handleSave() {
const errorMessage = validateMainForm(); const errorMessage = validateMainForm();
if (errorMessage) { if (errorMessage) {
message.warning(errorMessage); message.warning(errorMessage);
return; return;
} }
try {
await ensureTemplateBodyFileUploaded();
emit("save", { emit("save", {
creationMode: creationMode.value, creationMode: creationMode.value,
templateKey: templateKey.value, templateKey: templateKey.value,
@@ -986,6 +1315,9 @@ function handleSave() {
}); });
visible.value = false; visible.value = false;
message.success("合同正文配置已保存"); message.success("合同正文配置已保存");
} catch (error) {
message.warning(error?.message || "合同正文保存失败");
}
} }
function resetForm() { function resetForm() {
@@ -1,11 +1,12 @@
<template> <template>
<BaseMixedSignModal ref="innerRef" /> <BaseMixedSignModal ref="innerRef" @business-updated="emit('business-updated', $event)" />
</template> </template>
<script setup> <script setup>
import { ref } from 'vue' import { ref } from 'vue'
import BaseMixedSignModal from '../../../seal/mixedSignModal.vue' import BaseMixedSignModal from '../../../seal/mixedSignModal.vue'
const emit = defineEmits(['business-updated'])
const innerRef = ref(null) const innerRef = ref(null)
function showModal(...args) { function showModal(...args) {
@@ -0,0 +1,127 @@
export const contractTypeOptions = [
{ label: "MM 买卖合同", value: "MM", simpleLabel: "买卖合同" },
{ label: "GY 供用电、水、气、热力合同", value: "GY", simpleLabel: "供用电、水、气、热力合同" },
{ label: "ZY 赠与合同", value: "ZY", simpleLabel: "赠与合同" },
{ label: "JK 借款合同", value: "JK", simpleLabel: "借款合同" },
{ label: "BZ 保证合同", value: "BZ", simpleLabel: "保证合同" },
{ label: "ZL 租赁合同", value: "ZL", simpleLabel: "租赁合同" },
{ label: "RZ 融资租赁合同", value: "RZ", simpleLabel: "融资租赁合同" },
{ label: "BL 保理合同", value: "BL", simpleLabel: "保理合同" },
{ label: "CL 承揽合同", value: "CL", simpleLabel: "承揽合同" },
{ label: "GC 建设工程合同", value: "GC", simpleLabel: "建设工程合同" },
{ label: "YS 运输合同", value: "YS", simpleLabel: "运输合同" },
{ label: "JS 技术合同", value: "JS", simpleLabel: "技术合同" },
{ label: "BG 保管合同", value: "BG", simpleLabel: "保管合同" },
{ label: "CC 仓储合同", value: "CC", simpleLabel: "仓储合同" },
{ label: "WT 委托合同", value: "WT", simpleLabel: "委托合同" },
{ label: "WY 物业服务合同", value: "WY", simpleLabel: "物业服务合同" },
{ label: "HJ 行纪合同", value: "HJ", simpleLabel: "行纪合同" },
{ label: "ZJ 中介合同", value: "ZJ", simpleLabel: "中介合同" },
{ label: "HH 合伙合同", value: "HH", simpleLabel: "合伙合同" },
{ label: "YG 用工合同", value: "YG", simpleLabel: "用工合同" },
{ label: "YW 业务合同", value: "YW", simpleLabel: "业务合同" },
{ label: "QT 其他合同", value: "QT", simpleLabel: "其他合同" },
{ label: "YWBFP 采购合同 / 保税油业务合同", value: "YWBFP", simpleLabel: "采购合同" },
{ label: "YWBFS 销售合同 / 保税油业务合同", value: "YWBFS", simpleLabel: "销售合同" },
{ label: "YWIOP 采购合同 / 国际原油贸易业务合同", value: "YWIOP", simpleLabel: "采购合同" },
{ label: "YWIOS 销售合同 / 国际原油贸易业务合同", value: "YWIOS", simpleLabel: "销售合同" },
{ label: "YWDOP 采购合同 / 国内油品贸易业务合同", value: "YWDOP", simpleLabel: "采购合同" },
{ label: "YWDOS 销售合同 / 国内油品贸易业务合同", value: "YWDOS", simpleLabel: "销售合同" },
{ label: "YWBCP 采购合同 / 大宗贸易业务合同", value: "YWBCP", simpleLabel: "采购合同" },
{ label: "YWBCS 销售合同 / 大宗贸易业务合同", value: "YWBCS", simpleLabel: "销售合同" },
{ label: "YWOTP 采购合同 / 其他业务合同", value: "YWOTP", simpleLabel: "采购合同" },
{ label: "YWOTS 销售合同 / 其他业务合同", value: "YWOTS", simpleLabel: "销售合同" },
];
export const valuationModeOptions = [
{ label: "无金额", value: "0" },
{ label: "固定总价", value: "1" },
{ label: "预估总价", value: "2" },
{ label: "无法预估总价", value: "3" },
{ label: "固定单价", value: "4" },
];
export const currencyOptions = [
{ label: "人民币", value: "RMB" },
{ label: "美元", value: "USD" },
{ label: "港币", value: "HKD" },
{ label: "欧元", value: "EUR" },
{ label: "印尼盾", value: "IDR" },
];
export const sealTypeOptions = [
{ label: "公章", value: "GZ" },
{ label: "合同专用章", value: "HTZYZ" },
{ label: "公章+法定代表人私章", value: "GZFDDBRSZ" },
{ label: "公章+法定代表人签字", value: "GZFDDBRQZ" },
{ label: "公章+被授权人签字", value: "GZBSQRQZ" },
{ label: "合同专用章+被授权人签字", value: "HTZYZBSQRQZ" },
{ label: "合同专用章+法定代表人签字", value: "HTZYZFDDBRQZ" },
{ label: "其他", value: "QT" },
];
export const textSourceOptions = [
{ label: "推荐文本", value: "standardText" },
{ label: "示范文本", value: "exampleText" },
{ label: "自行起草", value: "draftBySelf" },
{ label: "对方文本", value: "otherText" },
];
export const paymentDirectionOptions = [
{ label: "付款", value: "1" },
{ label: "收款", value: "2" },
{ label: "收支双向", value: "3" },
{ label: "无", value: "0" },
];
export const isElectronOptions = [
{ label: "电子合同", value: "1" },
{ label: "纸质合同", value: "0" },
];
export const dateTypeOptions = [
{ label: "年", value: "0" },
{ label: "月", value: "1" },
{ label: "日", value: "2" },
];
export const contractSourceOptions = [
{ label: "结算平台-ECP", value: "ECP" },
{ label: "大宗平台-DSSP", value: "DSSP" },
];
export const yesNoBooleanOptions = [
{ label: "是", value: true },
{ label: "否", value: false },
];
export const extraAmountFieldOptions = [
{ key: "deposit", label: "定金", code: "DJ", amountField: "djAmount" },
{ key: "pledge", label: "押金", code: "YJ", amountField: "yjAmount" },
{ key: "advance", label: "预付款", code: "YFK", amountField: "yfkAmount" },
{ key: "guarantee", label: "保证金", code: "BZJ", amountField: "bzjAmount" },
];
const labelMap = new Map([
...contractTypeOptions,
...valuationModeOptions,
...currencyOptions,
...sealTypeOptions,
...textSourceOptions,
...paymentDirectionOptions,
...isElectronOptions,
...dateTypeOptions,
...contractSourceOptions,
].map((item) => [item.value, item.label]));
export function getOptionLabel(options, value) {
return options.find((item) => item.value === value)?.label || value || "";
}
export function getCommonOptionLabel(value) {
return labelMap.get(value) || value || "";
}
export function getContractTypeSimpleLabel(value) {
return contractTypeOptions.find((item) => item.value === value)?.simpleLabel || value || "";
}
+253 -211
View File
@@ -10,23 +10,17 @@
<a-row :gutter="16" style="width: 100%"> <a-row :gutter="16" style="width: 100%">
<a-col :md="6" :sm="24"> <a-col :md="6" :sm="24">
<a-form-item label="合同名称"> <a-form-item label="合同名称">
<a-input <a-input v-model:value="queryParam.contractName" allow-clear placeholder="请输入合同名称" />
size="small"
v-model:value="queryParam.contractName"
allow-clear
placeholder="请输入合同名称"
/>
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :md="6" :sm="24"> <a-col :md="6" :sm="24">
<a-form-item label="合同编码"> <a-form-item label="合同编码">
<a-input <a-input v-model:value="queryParam.contractCode" allow-clear placeholder="请输入合同编码" />
size="small" </a-form-item>
v-model:value="queryParam.contractCode" </a-col>
allow-clear <a-col :md="6" :sm="24">
placeholder="请输入合同编码" <a-form-item label="合同类型">
/> <a-input v-model:value="queryParam.contractType" allow-clear placeholder="请输入合同类型编码" />
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :md="6" :sm="24"> <a-col :md="6" :sm="24">
@@ -61,32 +55,10 @@
<vxe-toolbar ref="toolbarRef" custom> <vxe-toolbar ref="toolbarRef" custom>
<template #buttons> <template #buttons>
<a-space wrap> <a-space wrap>
<a-button <a-button type="primary" :icon="h(SearchOutlined)" @click="handleQuery">查询</a-button>
type="primary" <a-button :icon="h(RedoOutlined)" @click="handleReset">重置</a-button>
:icon="h(SearchOutlined)" <a-button type="primary" ghost :icon="h(PlusOutlined)" @click="handleCreate">发起合同</a-button>
@click="handleQuery" <a-button danger :icon="h(DeleteOutlined)" :disabled="!selectedRowKeys.length" @click="handleBatchDelete">删除</a-button>
>
查询
</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> </a-space>
</template> </template>
</vxe-toolbar> </vxe-toolbar>
@@ -102,91 +74,47 @@
:checkbox-config="{ range: true }" :checkbox-config="{ range: true }"
:data="tableData" :data="tableData"
:loading="loading" :loading="loading"
:loading-config="{ :loading-config="{ icon: 'vxe-icon-indicator roll', text: '正在拼命加载中...' }"
icon: 'vxe-icon-indicator roll',
text: '正在拼命加载中...',
}"
@checkbox-range-change="selectChangeEvent" @checkbox-range-change="selectChangeEvent"
@checkbox-change="selectChangeEvent" @checkbox-change="selectChangeEvent"
@checkbox-all="selectChangeEvent" @checkbox-all="selectChangeEvent"
> >
<vxe-column type="checkbox" width="56" /> <vxe-column type="checkbox" width="56" />
<vxe-column type="seq" title="序号" width="68" /> <vxe-column type="seq" title="序号" width="68" />
<vxe-column <vxe-column field="contractCode" title="合同编码" width="160" show-overflow="title" />
field="contractCode" <vxe-column field="lawContractCode" title="法务合同编号" width="160" show-overflow="title" />
title="合同编码" <vxe-column field="contractName" title="合同名称" width="220" show-overflow="title" />
width="150" <vxe-column field="contractType" title="合同类型" width="160" show-overflow="title">
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 }"> <template #default="{ row }">
<a-tag :color="getStatusColor(row.status)"> {{ getContractTypeSimpleLabel(row.contractTypeCode || row.contractType) }}
{{ getStatusLabel(row.status) }}
</a-tag>
</template> </template>
</vxe-column> </vxe-column>
<vxe-column field="templateName" title="所选模板" width="180" show-overflow="title" />
<vxe-column <vxe-column field="counterpartySummary" title="合同相对方" width="260" show-overflow="title" />
field="initiator" <vxe-column field="status" title="状态" width="120">
title="发起人" <template #default="{ row }">
width="100" <a-tag :color="getStatusColor(row.status)">{{ getStatusLabel(row.status) }}</a-tag>
show-overflow="title" </template>
/> </vxe-column>
<vxe-column <vxe-column field="approveMessage" title="审批说明" width="220" show-overflow="title" />
field="initiatorDept" <vxe-column field="initiator" title="发起人" width="100" show-overflow="title" />
title="所属部门" <vxe-column field="initiatorDept" title="所属部门" width="140" show-overflow="title" />
width="140" <vxe-column field="createTime" title="发起时间" width="170" show-overflow="title" />
show-overflow="title" <vxe-column field="updateTime" title="最后更新时间" width="170" show-overflow="title" />
/> <vxe-column title="操作" width="520" fixed="right">
<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 }"> <template #default="{ row }">
<div class="table-actions"> <div class="table-actions">
<a @click="handleView(row)">查看</a> <a @click="handleView(row)">查看</a>
<a v-if="canEdit(row)" @click="handleEdit(row)">编辑</a> <a v-if="canEdit(row)" @click="handleEdit(row)">编辑</a>
<a v-if="canSubmit(row)" @click="handleSubmit(row)">提交法务</a>
<a v-if="canMockApprove(row)" @click="handleMockApprove(row)">测试通过</a>
<a v-if="canMockReject(row)" @click="handleMockReject(row)">测试驳回</a>
<a v-if="canOnlineSign(row)" @click="handleOnlineSign(row)"> <a v-if="canOnlineSign(row)" @click="handleOnlineSign(row)">
{{ row.status === "completed" ? "查看签署" : "在线签订" }} {{ row.status === "completed" ? "查看签署" : "在线签订" }}
</a> </a>
<a v-if="canMockFinishSign(row)" @click="handleMockFinishSign(row)">测试签署完成</a>
<a v-if="canCollect(row)" @click="handleMockCollect(row)">测试采集</a>
<a v-if="canArchive(row)" @click="handleArchive(row)">测试归档</a>
<a v-if="canDelete(row)" @click="handleDelete(row)">删除</a> <a v-if="canDelete(row)" @click="handleDelete(row)">删除</a>
</div> </div>
</template> </template>
@@ -208,8 +136,8 @@
/> />
</a-card> </a-card>
<ContractPartyPlaceholderModal ref="contractModalRef" /> <ContractPartyPlaceholderModal ref="contractModalRef" @save="handleModalSave" />
<MixedSignModal ref="mixedSignModalRef" /> <MixedSignModal ref="mixedSignModalRef" @business-updated="handleSignBusinessUpdated" />
</section> </section>
</template> </template>
@@ -224,17 +152,32 @@ import {
import { message, Modal } from "ant-design-vue"; import { message, Modal } from "ant-design-vue";
import ContractPartyPlaceholderModal from "./components/ContractPartyPlaceholderModal.vue"; import ContractPartyPlaceholderModal from "./components/ContractPartyPlaceholderModal.vue";
import MixedSignModal from "./components/mixedSignModal.vue"; import MixedSignModal from "./components/mixedSignModal.vue";
import { deleteLaunchRecord, queryLaunchList } from "./api.js"; import { getContractTypeSimpleLabel } from "./contractLaunchOptions.js";
import {
archiveLaunch,
createLaunchDraft,
deleteLaunchRecord,
getLaunchDetail,
mockApproveLaunch,
mockCollectLaunch,
mockRejectLaunch,
finishLaunchSeal,
queryLaunchList,
submitLaunchRecord,
updateLaunchDraft,
} from "@/api/launch.js";
// 页面内字典(后续可由后端接口替换)
const contractRecordStatusOptions = [ const contractRecordStatusOptions = [
{ label: "草稿", value: "draft" }, { label: "草稿", value: "draft" },
{ label: "审批中", value: "reviewing" }, { label: "审批中", value: "reviewing" },
{ label: "审批驳回", value: "rejected" }, { label: "审批驳回", value: "rejected" },
{ label: "待签署", value: "pending_sign" }, { label: "待签署", value: "pending_sign" },
{ label: "签署中", value: "signing" }, { label: "签署中", value: "signing" },
{ label: "待采集", value: "pending_collect" },
{ label: "待归档", value: "pending_archive" },
{ label: "已完成", value: "completed" }, { label: "已完成", value: "completed" },
] { label: "已终止", value: "terminated" },
];
const formRef = ref(null); const formRef = ref(null);
const queryContainer = ref(null); const queryContainer = ref(null);
@@ -250,7 +193,6 @@ const tableHeight = ref(420);
const tableData = ref([]); const tableData = ref([]);
const selectedRows = ref([]); const selectedRows = ref([]);
const selectedRowKeys = ref([]); const selectedRowKeys = ref([]);
const allRecords = ref([]);
const queryParam = ref({ const queryParam = ref({
pageNo: 1, pageNo: 1,
@@ -259,8 +201,6 @@ const queryParam = ref({
contractCode: undefined, contractCode: undefined,
contractType: undefined, contractType: undefined,
status: undefined, status: undefined,
queryScope: undefined,
templateKey: undefined,
dateRange: [], dateRange: [],
}); });
@@ -275,18 +215,16 @@ const tableCardBodyStyle = {
padding: "8px 12px 6px", padding: "8px 12px 6px",
}; };
const templateQueryOptions = [
{ label: "采购合同模板", value: "purchase" },
{ label: "销售合同模板", value: "sales" },
];
const statusLabelMap = { const statusLabelMap = {
draft: "草稿", draft: "草稿",
reviewing: "审批中", reviewing: "审批中",
rejected: "审批驳回", rejected: "审批驳回",
pending_sign: "待签署", pending_sign: "待签署",
signing: "签署中", signing: "签署中",
pending_collect: "待采集",
pending_archive: "待归档",
completed: "已完成", completed: "已完成",
terminated: "已终止",
}; };
const statusColorMap = { const statusColorMap = {
@@ -295,19 +233,31 @@ const statusColorMap = {
rejected: "error", rejected: "error",
pending_sign: "warning", pending_sign: "warning",
signing: "purple", signing: "purple",
pending_collect: "cyan",
pending_archive: "orange",
completed: "success", completed: "success",
terminated: "default",
}; };
const scopeLabelMap = { function isSuccessResponse(res) {
personal: "个人级", return String(res?.code || "") === "0000";
company: "公司级", }
admin: "管理员级",
}; function getPageResult(res) {
return res?.result || {};
}
function getDataResult(res) {
return res?.result || null;
}
function getResponseMessage(res, fallback = "操作失败") {
return res?.message || res?.msg || fallback;
}
function connectToolbar() { function connectToolbar() {
const table = tableRef.value; const table = tableRef.value;
const toolbar = toolbarRef.value; const toolbar = toolbarRef.value;
if (table && toolbar && table.connect) { if (table && toolbar && table.connect) {
table.connect(toolbar); table.connect(toolbar);
} }
@@ -321,10 +271,6 @@ function getStatusColor(status) {
return statusColorMap[status] || "default"; return statusColorMap[status] || "default";
} }
function getScopeLabel(scope) {
return scopeLabelMap[scope] || scope;
}
function canEdit(row) { function canEdit(row) {
return ["draft", "rejected"].includes(row.status); return ["draft", "rejected"].includes(row.status);
} }
@@ -333,76 +279,68 @@ function canDelete(row) {
return ["draft", "rejected"].includes(row.status); return ["draft", "rejected"].includes(row.status);
} }
function canSubmit(row) {
return ["draft", "rejected"].includes(row.status);
}
function canMockApprove(row) {
return row.status === "reviewing";
}
function canMockReject(row) {
return row.status === "reviewing";
}
function canOnlineSign(row) { function canOnlineSign(row) {
return ["pending_sign", "signing", "completed"].includes(row.status); return ["pending_sign", "signing", "completed"].includes(row.status);
} }
function canMockFinishSign(row) {
return ["pending_sign", "signing"].includes(row.status);
}
function canCollect(row) {
return row.status === "pending_collect";
}
function canArchive(row) {
return row.status === "pending_archive";
}
function selectChangeEvent({ records }) { function selectChangeEvent({ records }) {
selectedRows.value = records; selectedRows.value = records;
selectedRowKeys.value = records.map((item) => item.id); selectedRowKeys.value = records.map((item) => item.id);
} }
function filterRecords(records) { function buildQueryPayload() {
return records.filter((item) => { return {
const matchName = pageNo: queryParam.value.pageNo,
!queryParam.value.contractName || pageSize: queryParam.value.pageSize,
item.contractName.includes(queryParam.value.contractName); contractName: queryParam.value.contractName,
contractCode: queryParam.value.contractCode,
const matchCode = contractType: queryParam.value.contractType,
!queryParam.value.contractCode || status: queryParam.value.status,
item.contractCode.includes(queryParam.value.contractCode); dateStart: queryParam.value.dateRange?.[0],
dateEnd: queryParam.value.dateRange?.[1],
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() { function getList() {
loading.value = true; loading.value = true;
queryLaunchList(buildQueryPayload())
queryLaunchList().then((records) => { .then((res) => {
// TODO: 这里后续直接使用合同发起列表接口返回值 if (!isSuccessResponse(res)) {
allRecords.value = records; tableData.value = [];
const filtered = filterRecords(allRecords.value); total.value = 0;
const start = (queryParam.value.pageNo - 1) * queryParam.value.pageSize; message.error(getResponseMessage(res, "合同发起列表查询失败"));
const end = start + queryParam.value.pageSize; return;
}
total.value = filtered.length; const pageResult = getPageResult(res);
tableData.value = filtered.slice(start, end); tableData.value = pageResult.data || [];
total.value = pageResult.totalSize || 0;
})
.finally(() => {
loading.value = false; loading.value = false;
nextTick(() => { nextTick(() => {
connectToolbar(); connectToolbar();
syncTableHeight(); syncTableHeight();
@@ -424,8 +362,6 @@ function handleReset() {
contractCode: undefined, contractCode: undefined,
contractType: undefined, contractType: undefined,
status: undefined, status: undefined,
queryScope: undefined,
templateKey: undefined,
dateRange: [], dateRange: [],
}); });
getList(); getList();
@@ -442,41 +378,144 @@ function handleCreate() {
} }
function handleView(row) { function handleView(row) {
contractModalRef.value?.showModal(row, "view"); getLaunchDetail(row.id).then((res) => {
if (!isSuccessResponse(res) || !getDataResult(res)) {
message.error(getResponseMessage(res, "获取合同详情失败"));
return;
}
contractModalRef.value?.showModal(getDataResult(res), "view");
});
} }
function handleEdit(row) { function handleEdit(row) {
contractModalRef.value?.showModal(row, "edit"); getLaunchDetail(row.id).then((res) => {
if (!isSuccessResponse(res) || !getDataResult(res)) {
message.error(getResponseMessage(res, "获取合同详情失败"));
return;
}
contractModalRef.value?.showModal(getDataResult(res), "edit");
});
}
function handleSubmit(row) {
submitLaunchRecord(row.id).then((res) => {
if (!isSuccessResponse(res)) {
message.error(getResponseMessage(res, "提交法务失败"));
return;
}
message.success("已提交法务当前为测试环境后续可由法务系统真实审批");
getList();
});
}
function handleMockApprove(row) {
mockApproveLaunch(row.id, {}).then((res) => {
if (!isSuccessResponse(res)) {
message.error(getResponseMessage(res, "模拟审批通过失败"));
return;
}
message.success("测试审批通过完成合同已流转到待签署");
getList();
});
}
function handleMockReject(row) {
mockRejectLaunch(row.id, {}).then((res) => {
if (!isSuccessResponse(res)) {
message.error(getResponseMessage(res, "模拟审批驳回失败"));
return;
}
message.success("测试审批驳回完成合同已流转到审批驳回");
getList();
});
}
function handleMockFinishSign(row) {
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: "测试按钮模拟签署完成真实签章接入后可删除此按钮",
}).then((res) => {
if (!isSuccessResponse(res)) {
message.error(getResponseMessage(res, "测试签署完成失败"));
return;
}
message.success("测试签署完成合同已流转到待采集");
getList();
});
} }
function handleOnlineSign(row) { function handleOnlineSign(row) {
if (!row.sealContractId && !row.signContractId && !row.contractId) { getLaunchDetail(row.id).then((res) => {
message.info( if (!isSuccessResponse(res) || !getDataResult(res)) {
"当前未接入签署详情接口将进入在线签署弹窗并保留合同业务信息你可以继续上传文件后发起签署", message.error(getResponseMessage(res, "获取合同详情失败"));
); return;
} }
mixedSignModalRef.value?.showModal(row); const detail = getDataResult(res);
mixedSignModalRef.value?.showModal({
...detail,
contractId: detail.sealContractId,
});
});
} }
function deleteRowsByIds(ids) { function handleSignBusinessUpdated() {
// TODO: 这里后续替换为删除接口后再刷新列表
allRecords.value = allRecords.value.filter((item) => !ids.includes(item.id));
selectedRows.value = [];
selectedRowKeys.value = [];
getList(); getList();
} }
function handleModalSave(payload) {
const requestFn = payload.mode === "edit" ? updateLaunchDraft : createLaunchDraft;
requestFn(payload.record).then((res) => {
if (!isSuccessResponse(res)) {
message.error(getResponseMessage(res, "保存合同失败"));
return;
}
message.success(payload.mode === "edit" ? "合同修改成功" : "合同草稿保存成功");
getList();
});
}
function handleMockCollect(row) {
mockCollectLaunch(row.id).then((res) => {
if (!isSuccessResponse(res)) {
message.error(getResponseMessage(res, "模拟合同采集失败"));
return;
}
message.success("测试合同采集完成合同已流转到待归档");
getList();
});
}
function handleArchive(row) {
archiveLaunch(row.id, {}).then((res) => {
if (!isSuccessResponse(res)) {
message.error(getResponseMessage(res, "测试归档失败"));
return;
}
message.success("测试归档完成合同已流转到已完成");
getList();
});
}
function handleDelete(row) { function handleDelete(row) {
Modal.confirm({ Modal.confirm({
title: `确认删除合同【${row.contractName}】吗?`, title: `确认删除合同【${row.contractName}】吗?`,
content: "当前仅删除本地 mock 数据便于后续替换真实接口", content: "仅草稿或审批驳回状态支持删除",
okText: "删除", okText: "删除",
cancelText: "取消", cancelText: "取消",
okType: "danger", okType: "danger",
onOk: () => { onOk: () => {
deleteLaunchRecord([row.id]).then(() => { deleteLaunchRecord(row.id).then((res) => {
deleteRowsByIds([row.id]); if (!isSuccessResponse(res)) {
message.success("删除发起记录"); message.error(getResponseMessage(res, "删除失败"));
return;
}
message.success("删除成功");
getList();
}); });
}, },
}); });
@@ -488,18 +527,20 @@ function handleBatchDelete() {
message.warning("仅草稿或审批驳回状态支持删除请重新选择"); message.warning("仅草稿或审批驳回状态支持删除请重新选择");
return; return;
} }
Modal.confirm({ Modal.confirm({
title: `确认删除选中的 ${selectedRowKeys.value.length} 条记录吗?`, title: `确认删除选中的 ${selectedRowKeys.value.length} 条记录吗?`,
content: "当前仅演示本地数据删除后续接入真实接口即可替换", content: "仅草稿或审批驳回状态支持删除",
okText: "删除", okText: "删除",
cancelText: "取消", cancelText: "取消",
okType: "danger", okType: "danger",
onOk: () => { onOk: async () => {
deleteLaunchRecord(selectedRowKeys.value).then(() => { for (const id of selectedRowKeys.value) {
deleteRowsByIds(selectedRowKeys.value); await deleteLaunchRecord(id);
}
message.success("批量删除成功"); message.success("批量删除成功");
}); selectedRows.value = [];
selectedRowKeys.value = [];
getList();
}, },
}); });
} }
@@ -561,6 +602,7 @@ onBeforeUnmount(() => {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
flex-wrap: wrap;
} }
.table-pagination { .table-pagination {
@@ -283,16 +283,12 @@ import {
queryManifestOrgOptions, queryManifestOrgOptions,
uploadTemplateFile, uploadTemplateFile,
} from '@/api/manifest.js' } from '@/api/manifest.js'
import { contractTypeTreeOptions } from '../contractTypeOptions.js'
// ========== 模板管理字典(后续可由后端接口替换) ========== // ========== 模板管理字典(后续可由后端接口替换) ==========
// 合同类型选项 // 合同类型选项
const contractTypeOptions = [ const contractTypeOptions = contractTypeTreeOptions
{ label: '采购合同', value: '采购合同' },
{ label: '销售合同', value: '销售合同' },
{ label: '服务合同', value: '服务合同' },
{ label: '框架合同', value: '框架合同' },
]
// 范本类型选项 // 范本类型选项
const templateSourceOptions = [ const templateSourceOptions = [
@@ -412,7 +408,7 @@ function createDefaultFormState() {
templateName: '', templateName: '',
templateCode: '', templateCode: '',
source: '标准模板', source: '标准模板',
contractType: '采购合同', contractType: 'MM',
selfPartyRole: '甲方', selfPartyRole: '甲方',
version: 'V1.0', version: 'V1.0',
status: 'draft', status: 'draft',
@@ -0,0 +1,52 @@
export const contractTypeTreeOptions = [
{
label: "集团合同分类",
options: [
{ label: "MM 买卖合同", value: "MM", simpleLabel: "买卖合同" },
{ label: "GY 供用电、水、气、热力合同", value: "GY", simpleLabel: "供用电、水、气、热力合同" },
{ label: "ZY 赠与合同", value: "ZY", simpleLabel: "赠与合同" },
{ label: "JK 借款合同", value: "JK", simpleLabel: "借款合同" },
{ label: "BZ 保证合同", value: "BZ", simpleLabel: "保证合同" },
{ label: "ZL 租赁合同", value: "ZL", simpleLabel: "租赁合同" },
{ label: "RZ 融资租赁合同", value: "RZ", simpleLabel: "融资租赁合同" },
{ label: "BL 保理合同", value: "BL", simpleLabel: "保理合同" },
{ label: "CL 承揽合同", value: "CL", simpleLabel: "承揽合同" },
{ label: "GC 建设工程合同", value: "GC", simpleLabel: "建设工程合同" },
{ label: "YS 运输合同", value: "YS", simpleLabel: "运输合同" },
{ label: "JS 技术合同", value: "JS", simpleLabel: "技术合同" },
{ label: "BG 保管合同", value: "BG", simpleLabel: "保管合同" },
{ label: "CC 仓储合同", value: "CC", simpleLabel: "仓储合同" },
{ label: "WT 委托合同", value: "WT", simpleLabel: "委托合同" },
{ label: "WY 物业服务合同", value: "WY", simpleLabel: "物业服务合同" },
{ label: "HJ 行纪合同", value: "HJ", simpleLabel: "行纪合同" },
{ label: "ZJ 中介合同", value: "ZJ", simpleLabel: "中介合同" },
{ label: "HH 合伙合同", value: "HH", simpleLabel: "合伙合同" },
{ label: "YG 用工合同", value: "YG", simpleLabel: "用工合同" },
{ label: "YW 业务合同", value: "YW", simpleLabel: "业务合同" },
{ label: "QT 其他合同", value: "QT", simpleLabel: "其他合同" },
],
},
{
label: "国贸合同分类",
options: [
{ label: "YWBFP 采购合同 / 保税油业务合同", value: "YWBFP", simpleLabel: "采购合同 / 保税油业务合同" },
{ label: "YWBFS 销售合同 / 保税油业务合同", value: "YWBFS", simpleLabel: "销售合同 / 保税油业务合同" },
{ label: "YWIOP 采购合同 / 国际原油贸易业务合同", value: "YWIOP", simpleLabel: "采购合同 / 国际原油贸易业务合同" },
{ label: "YWIOS 销售合同 / 国际原油贸易业务合同", value: "YWIOS", simpleLabel: "销售合同 / 国际原油贸易业务合同" },
{ label: "YWDOP 采购合同 / 国内油品贸易业务合同", value: "YWDOP", simpleLabel: "采购合同 / 国内油品贸易业务合同" },
{ label: "YWDOS 销售合同 / 国内油品贸易业务合同", value: "YWDOS", simpleLabel: "销售合同 / 国内油品贸易业务合同" },
{ label: "YWBCP 采购合同 / 大宗贸易业务合同", value: "YWBCP", simpleLabel: "采购合同 / 大宗贸易业务合同" },
{ label: "YWBCS 销售合同 / 大宗贸易业务合同", value: "YWBCS", simpleLabel: "销售合同 / 大宗贸易业务合同" },
{ label: "YWOTP 采购合同 / 其他业务合同", value: "YWOTP", simpleLabel: "采购合同 / 其他业务合同" },
{ label: "YWOTS 销售合同 / 其他业务合同", value: "YWOTS", simpleLabel: "销售合同 / 其他业务合同" },
],
},
];
export const contractTypeOptions = contractTypeTreeOptions.flatMap((item) => item.options);
const contractTypeMap = new Map(contractTypeOptions.map((item) => [item.value, item]));
export function getContractTypeSimpleLabel(value) {
return contractTypeMap.get(value)?.simpleLabel || value || "";
}
+16 -35
View File
@@ -84,7 +84,11 @@
<vxe-column type="seq" title="序号" width="68" /> <vxe-column type="seq" title="序号" width="68" />
<vxe-column field="templateCode" title="模板编码" width="150" show-overflow="title" /> <vxe-column field="templateCode" title="模板编码" width="150" show-overflow="title" />
<vxe-column field="templateName" title="模板名称" width="180" 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="contractType" title="合同类型" width="180" show-overflow="title">
<template #default="{ row }">
{{ getContractTypeSimpleLabel(row.contractType) }}
</template>
</vxe-column>
<vxe-column field="source" title="模板来源" width="110" show-overflow="title" /> <vxe-column field="source" title="模板来源" width="110" show-overflow="title" />
<vxe-column field="version" title="版本" width="90" /> <vxe-column field="version" title="版本" width="90" />
<vxe-column field="status" title="状态" width="110"> <vxe-column field="status" title="状态" width="110">
@@ -145,6 +149,7 @@ import {
queryManifestList, queryManifestList,
saveManifest, saveManifest,
} from '@/api/manifest.js' } from '@/api/manifest.js'
import { contractTypeOptions, getContractTypeSimpleLabel } from './contractTypeOptions.js'
function isSuccessResponse(res) { function isSuccessResponse(res) {
return String(res?.code || '') === '0000' return String(res?.code || '') === '0000'
@@ -162,36 +167,6 @@ function getResponseMessage(res, fallback = '操作失败') {
return res?.message || res?.msg || 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 = [ const templateStatusOptions = [
{ label: '已发布', value: 'published' }, { label: '已发布', value: 'published' },
{ label: '草稿', value: 'draft' }, { label: '草稿', value: 'draft' },
@@ -257,7 +232,12 @@ function getStatusLabel(status) {
function getStatusColor(status) { function getStatusColor(status) {
return templateStatusColorMap[status] || 'default' return templateStatusColorMap[status] || 'default'
} }
function copyRecord(record) {
return {
...record,
contacts: (record.contacts || []).map((item) => ({ ...item })),
};
}
function getList() { function getList() {
loading.value = true loading.value = true
@@ -269,9 +249,10 @@ function getList() {
message.error(getResponseMessage(res, '模板列表查询失败')) message.error(getResponseMessage(res, '模板列表查询失败'))
return return
} }
const pageResult = getPageResult(res)
tableData.value = pageResult.records || [] const pageResult = getPageResult(res);
total.value = pageResult.total || 0 tableData.value = (pageResult.data || []).map((item) => copyRecord(item));
total.value = pageResult.totalSize || 0;
}) })
.finally(() => { .finally(() => {
loading.value = false loading.value = false
+118 -3
View File
@@ -108,13 +108,13 @@
<div <div
class="btn btn-primary" class="btn btn-primary"
:class="{ :class="{
'btn-disabled': !uploadFile || !contractName || creating, 'btn-disabled': (!uploadFile && !businessRecord?.id) || !contractName || creating,
}" }"
@click="handleCreateContract" @click="businessRecord?.id && !uploadFile ? handleCreateContractFromBusinessFile() : handleCreateContract()"
> >
<a-spin v-if="creating" size="small" /> <a-spin v-if="creating" size="small" />
<span v-if="creating"> 发起中...</span> <span v-if="creating"> 发起中...</span>
<span v-else>发起签署</span> <span v-else>{{ businessRecord?.id && !uploadFile ? "使用合同正文发起签署" : "发起签署" }}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -631,6 +631,11 @@ import {
getUserUniqueId, getUserUniqueId,
getSealBySignerId, getSealBySignerId,
} from "@/api/seal"; } from "@/api/seal";
import {
finishLaunchSeal,
downloadLaunchFile,
saveLaunchSealInfo,
} from "@/api/launch.js";
const SUCCESS_CODE = "0000"; const SUCCESS_CODE = "0000";
const CALLBACK_HOST = "http://47.110.50.12:7005"; const CALLBACK_HOST = "http://47.110.50.12:7005";
@@ -650,6 +655,7 @@ function getResponseMessage(res, fallback = "操作失败") {
const visible = ref(false); const visible = ref(false);
const businessRecord = ref(null); const businessRecord = ref(null);
const emit = defineEmits(["business-updated"]);
const businessStatusLabelMap = { const businessStatusLabelMap = {
draft: "草稿", draft: "草稿",
@@ -790,6 +796,78 @@ function clearContractState() {
} catch {} } catch {}
} }
function getBusinessBodyFile(record = {}) {
const bodyFile =
(record.fileList || []).find((item) => item.fileCategory === "body") || {};
return {
fileName:
record.contractTextFileName ||
record.contractBodyFileName ||
bodyFile.fileName ||
`${record.contractName || "合同正文"}.docx`,
filePath:
record.contractTextFilePath ||
record.contractBodyFilePath ||
bodyFile.filePath ||
"",
fileType:
record.contractTextFileType ||
record.contractBodyFileType ||
bodyFile.fileType ||
"docx",
};
}
async function buildBusinessBodyUploadFile(record = {}) {
const bodyFile = getBusinessBodyFile(record);
if (!bodyFile.filePath) {
return null;
}
const buffer = await downloadLaunchFile(bodyFile.filePath);
const type =
String(bodyFile.fileType || "").toLowerCase() === "pdf"
? "application/pdf"
: "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
return new File([buffer], bodyFile.fileName, { type });
}
function buildSignedFileInfo(downloadData = {}) {
const signedUrl = downloadData.downloadUrl || downloadData.url || "";
const signedName =
downloadData.fileName ||
`${businessRecord.value?.contractName || contractName.value || "已签署合同"}.pdf`;
return {
signedFileName: signedName,
signedFilePath: signedUrl || contractId.value || "",
signedFileType: signedName.split(".").pop()?.toLowerCase() || "pdf",
signedFileSize: downloadData.fileSize || downloadData.size || null,
};
}
async function writeBackLaunchSealFinished(status = 2000) {
if (!businessRecord.value?.id) {
return;
}
let signedFileInfo = {};
try {
const downloadRes = await downloadContract(contractId.value);
if (isSuccessResponse(downloadRes) && getResponseResult(downloadRes)) {
signedFileInfo = buildSignedFileInfo(getResponseResult(downloadRes));
}
} catch {}
const res = await finishLaunchSeal(businessRecord.value.id, {
sealContractId: contractId.value,
sealContractStatus: status,
...signedFileInfo,
});
if (isSuccessResponse(res) && getResponseResult(res)) {
businessRecord.value = { ...businessRecord.value, ...getResponseResult(res) };
emit("business-updated", getResponseResult(res));
}
}
function resetSealPlacementState() { function resetSealPlacementState() {
sealItems.value = []; sealItems.value = [];
sealPlacedId = 0; sealPlacedId = 0;
@@ -994,6 +1072,12 @@ async function handleCreateContract() {
imageHeight.value = Number(data.imageHeight) || 1122; imageHeight.value = Number(data.imageHeight) || 1122;
contractStatus.value = 10; contractStatus.value = 10;
updateBusinessStatusByContractStatus(contractStatus.value); updateBusinessStatusByContractStatus(contractStatus.value);
if (businessRecord.value?.id) {
await saveLaunchSealInfo(businessRecord.value.id, {
sealContractId: contractId.value,
sealContractStatus: contractStatus.value,
});
}
saveContractState(); saveContractState();
message.success("签署流程创建成功,请继续获取印章并拖拽到文档上"); message.success("签署流程创建成功,请继续获取印章并拖拽到文档上");
await nextTick(); await nextTick();
@@ -1005,6 +1089,30 @@ async function handleCreateContract() {
} }
} }
async function handleCreateContractFromBusinessFile() {
if (!businessRecord.value) {
return;
}
creating.value = true;
try {
const bodyUploadFile = await buildBusinessBodyUploadFile(businessRecord.value);
if (!bodyUploadFile) {
message.warning("当前合同没有已保存的正文文件,请先在合同发起弹窗中配置正文");
return;
}
uploadFile.value = bodyUploadFile;
if (!contractName.value) {
contractName.value =
businessRecord.value.contractName || bodyUploadFile.name.replace(/\.[^.]+$/, "");
}
await handleCreateContract();
} catch {
message.error("读取后端合同正文失败,请检查正文文件是否存在");
} finally {
creating.value = false;
}
}
const pdfScrollRef = ref(null); const pdfScrollRef = ref(null);
const displayWidth = ref(700); const displayWidth = ref(700);
const displayHeight = ref(990); const displayHeight = ref(990);
@@ -1424,6 +1532,10 @@ function handlePreSignCallback(data) {
} else { } else {
message.success(`预盖章确认成功,签署人:${data.signer || "未知"}`); message.success(`预盖章确认成功,签署人:${data.signer || "未知"}`);
} }
if (businessRecord.value?.id && data.contractStatus === 2000) {
writeBackLaunchSealFinished(data.contractStatus);
}
} }
function refreshQrCode() { function refreshQrCode() {
@@ -1522,6 +1634,9 @@ async function handleCompleteContract() {
contractStatus.value = 2000; contractStatus.value = 2000;
updateBusinessStatusByContractStatus(2000); updateBusinessStatusByContractStatus(2000);
if (businessRecord.value?.id) {
await writeBackLaunchSealFinished(2000);
}
clearContractState(); clearContractState();
message.success("合约已完成"); message.success("合约已完成");
} catch { } catch {
+4
View File
@@ -14,6 +14,10 @@ export default defineConfig({
host: true, host: true,
open: true, open: true,
proxy: { proxy: {
'/launch': {
target: 'http://127.0.0.1:8899',
changeOrigin: true,
},
'/manifest': { '/manifest': {
target: 'http://127.0.0.1:8899', target: 'http://127.0.0.1:8899',
changeOrigin: true, changeOrigin: true,