Merge pull request '项目+合同对接合同系统' (#8) from feature/hcs-api-p0-web into master

Reviewed-on: #8
This commit was merged in pull request #8.
This commit is contained in:
2026-10-27 14:47:14 +08:00
26 changed files with 1304 additions and 55 deletions
@@ -205,7 +205,9 @@ export const defaultContractAttachmentTypeOptions = [
{ label: '双章归档文件', value: '双章归档文件' },
{ label: '其他文件', value: CONTRACT_ATTACHMENT_TYPE_OTHER },
];
// 特批附件归口附件(佐证材料等)区上传:高风险/黑名单客商合同必传,后端按 fileType=特批附件 校验并挂特批面板(与后端 HcsSyncConstant.ATTACHMENT_TYPE_SPEC_EXPLANATION 对齐)
export const defaultContractSupportingAttachmentTypeOptions = [
{ label: '特批附件', value: '特批附件' },
{ label: '其他文件', value: CONTRACT_ATTACHMENT_TYPE_OTHER },
];
/** @deprecated 兼容旧引用,实际选项由业务字典动态加载 */
@@ -299,6 +301,8 @@ export const resolveContractAttachmentType = (
item => String(item.label) === aliasedFileType || String(item.label) === rawFileType
);
if (matchedByLabel) return matchedByLabel.value;
// 行已带类型但字典未命中(如历史保存值、字典缺失)时保留原值,避免重开页面时被降级为其他文件
if (rawFileType) return aliasedFileType;
const fileName = normalizeAttachmentTypeText(attachmentName(row));
if (!fileName) return otherType;
const matched = list
@@ -641,7 +645,7 @@ export default {
this.contractAttachmentTypeOptions
);
} else {
this.uploadDialogType = this.defaultAttachmentType;
this.uploadDialogType = this.uploadDialogType || this.defaultAttachmentType;
}
this.$message.success('上传成功');
},
@@ -0,0 +1,271 @@
<template>
<section class="contract-manage-form__section contract-manage-form__section--panel hcs-contract-panel">
<div class="contract-manage-form__attachment-head">
<div class="dialog-section-title">合规信息</div>
<div>
<el-button
v-if="canApplySpec"
type="warning"
plain
:loading="specApplying"
@click="openSpecDialog"
>
发起特批
</el-button>
<el-button
v-if="isSpecProcessing"
type="primary"
plain
@click="confirmSpecResult"
>
确认特批结果
</el-button>
</div>
</div>
<el-descriptions :column="3" class="contract-manage-detail__descriptions">
<el-descriptions-item label="合规同步状态">
<el-tooltip
v-if="hcsSyncMsg"
placement="top"
:content="hcsSyncMsg"
>
<el-tag :type="syncTagType" class="status-text">{{ syncLabel }}</el-tag>
</el-tooltip>
<el-tag v-else :type="syncTagType" class="status-text">{{ syncLabel }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="合规合同ID">{{
contract.hcsFdId || '-'
}}</el-descriptions-item>
<el-descriptions-item label="客商风险">
<template v-if="contract.hcsRiskLevel || contract.hcsBlackStatus">
<el-tag
v-if="isBlacklist"
type="danger"
class="status-text"
>黑名单</el-tag
>
<span v-if="contract.hcsRiskLevel" class="hcs-contract-panel__risk">{{
`风险等级:${contract.hcsRiskLevel}`
}}</span>
<span v-if="!contract.hcsRiskLevel && !isBlacklist">-</span>
</template>
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="特批状态">
<el-tag v-if="specLabel" :type="specTagType" class="status-text">{{ specLabel }}</el-tag>
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="特批说明" :span="2">{{
contract.hcsSpecMsg || '-'
}}</el-descriptions-item>
</el-descriptions>
<el-dialog
v-model="specDialogVisible"
title="发起合同特批"
width="640px"
append-to-body
destroy-on-close
class="hcs-contract-panel__spec-dialog"
@closed="resetSpecDialog"
>
<el-alert
title="高风险客商合同需经集团特批后方可签约推送;请上传特批申请材料及内部审批流程附件。"
type="warning"
:closable="false"
class="hcs-contract-panel__spec-alert"
/>
<el-upload
drag
multiple
:show-file-list="false"
:http-request="uploadMaterial"
:on-error="() => uploadFailed()"
class="hcs-contract-panel__spec-upload"
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">将特批材料拖到此处,或<em>点击上传到合同系统</em></div>
</el-upload>
<el-table :data="materials" border class="hcs-contract-panel__spec-table">
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column prop="name" label="文件名" min-width="220" show-overflow-tooltip />
<el-table-column prop="fdId" label="合规附件ID" min-width="240" show-overflow-tooltip />
<el-table-column label="操作" width="90" align="center" fixed="right">
<template #default="{ $index }">
<el-link type="danger" @click="materials.splice($index, 1)">删除</el-link>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button type="primary" :loading="specApplying" @click="submitSpec">确认发起</el-button>
<el-button @click="specDialogVisible = false">取消</el-button>
</template>
</el-dialog>
</section>
</template>
<script>
import { UploadFilled } from '@element-plus/icons-vue';
import {
applyContractSpec,
confirmContractSpec,
uploadAttachment,
} from '@/api/business/hcs';
const SYNC_STATUS_OPTIONS = [
{ label: '待推送', value: 'unsynced', type: 'info' },
{ label: '已推送', value: 'synced', type: 'success' },
{ label: '推送失败', value: 'failed', type: 'danger' },
];
const SPEC_STATUS_OPTIONS = [
{ label: '待特批', value: 'need', type: 'warning' },
{ label: '特批中', value: 'processing', type: 'warning' },
{ label: '特批通过', value: 'approved', type: 'success' },
{ label: '特批驳回', value: 'rejected', type: 'danger' },
];
export default {
name: 'HcsContractPanel',
components: { UploadFilled },
props: {
// 合同详情行(须含 id/hcsSyncStatus/hcsFdId/hcsRiskLevel/hcsBlackStatus/hcsSpecStatus 等)
contract: { type: Object, required: true },
},
emits: ['refresh'],
data() {
return {
specDialogVisible: false,
specApplying: false,
materials: [],
};
},
computed: {
syncLabel() {
return SYNC_STATUS_OPTIONS.find(item => item.value === this.contract.hcsSyncStatus)?.label || '-';
},
syncTagType() {
return SYNC_STATUS_OPTIONS.find(item => item.value === this.contract.hcsSyncStatus)?.type || 'info';
},
hcsSyncMsg() {
return this.contract.hcsSyncMsg || '';
},
isBlacklist() {
return String(this.contract.hcsBlackStatus || '').toLowerCase() === 'yes';
},
specLabel() {
return SPEC_STATUS_OPTIONS.find(item => item.value === this.contract.hcsSpecStatus)?.label || '';
},
specTagType() {
return SPEC_STATUS_OPTIONS.find(item => item.value === this.contract.hcsSpecStatus)?.type || 'info';
},
isSpecProcessing() {
return this.contract.hcsSpecStatus === 'processing';
},
canApplySpec() {
return ['need', 'rejected'].includes(this.contract.hcsSpecStatus);
},
},
methods: {
openSpecDialog() {
this.specDialogVisible = true;
},
resetSpecDialog() {
this.materials = [];
},
// 自定义上传:直传合同合规系统(C103-2),成功记录合规附件ID
uploadMaterial(options) {
const file = options.file;
if (file.size / 1024 / 1024 > 50) {
this.$message.warning('单文件大小不能超过 50MB');
return;
}
const formData = new FormData();
formData.append('fdFile', file);
uploadAttachment(formData)
.then(response => {
const fdId = response?.data?.data || response?.data;
if (!fdId || typeof fdId !== 'string') {
this.$message.error(response?.data?.msg || '上传合同系统失败');
return;
}
this.materials.push({ fdId, name: file.name });
this.$message.success(`已上传合同系统:${file.name}`);
})
.catch(error => {
this.$message.error(error?.response?.data?.msg || '上传合同系统失败');
});
},
uploadFailed() {
this.$message.error('上传合同系统失败');
},
submitSpec() {
if (!this.materials.length) {
this.$message.warning('请先上传特批申请材料');
return;
}
this.specApplying = true;
applyContractSpec({
contractId: this.contract.id,
attachments: this.materials.map(item => ({ fdId: item.fdId, name: item.name })),
})
.then(response => {
if (response?.data?.success === false) {
this.$message.error(response?.data?.msg || '特批发起失败');
return;
}
this.$message.success(response?.data?.data || '特批发起成功,等待集团审批');
this.specDialogVisible = false;
this.$emit('refresh');
})
.catch(error => {
this.$message.error(error?.response?.data?.msg || '特批发起失败');
})
.finally(() => {
this.specApplying = false;
});
},
confirmSpecResult() {
this.$prompt('请输入集团特批结果(approved=通过 / rejected=驳回)', '确认特批结果', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern: /^(approved|rejected)$/,
inputErrorMessage: '请输入 approved 或 rejected',
}).then(({ value }) => {
confirmContractSpec(this.contract.id, value).then(() => {
this.$message.success('特批结果已确认');
this.$emit('refresh');
});
});
},
},
};
</script>
<style lang="scss" scoped>
.hcs-contract-panel__risk {
margin-left: 8px;
color: #606266;
}
.hcs-contract-panel__spec-alert {
margin-bottom: 12px;
}
.hcs-contract-panel__spec-upload {
margin-bottom: 12px;
:deep(.el-upload) {
width: 100%;
}
:deep(.el-upload-dragger) {
width: 100%;
}
}
.hcs-contract-panel__spec-table {
margin-bottom: 4px;
}
</style>
@@ -257,6 +257,11 @@
:disabled="isSettlementRuleChange"
/>
</el-form-item>
<!-- 风险客商提示:合同已推送合规并留痕高风险/黑名单时展示,变更提交同样要求特批附件 -->
<div v-if="riskTipText" class="contract-manage-form__risk-tip">
<div class="contract-manage-form__risk-tip-title">风险客商提示:</div>
<div class="contract-manage-form__risk-tip-content">{{ riskTipText }}</div>
</div>
</section>
<contract-attachment-section
@@ -710,6 +715,17 @@ export default {
pageTitle() {
return this.$route.query.name || '合同变更';
},
// 高风险/黑名单客商提示:按合同推送合规留痕 hcs_risk_level/hcs_black_status 判定(与合同详情口径一致)
riskTipText() {
const riskLevel = String(this.form.hcsRiskLevel || '').trim();
const blacklist = String(this.form.hcsBlackStatus || '').toLowerCase() === 'yes';
if (riskLevel !== '2' && !blacklist) return '';
const parties = [this.form.partyA, this.form.partyB]
.map(name => String(name || '').trim())
.filter(Boolean)
.join('、');
return `【${parties || '相对方'}】:被列入外部黑名单;涉贪污受贿裁判相关提及方(3年以上及其他);被执行人信息;买卖合同纠纷;劳动纠纷;重大纠纷。提交变更前须上传「特批附件」类型附件`;
},
isSettlementRuleChange() {
return this.form.changeType === '结算规则变更';
},
@@ -1079,6 +1095,10 @@ export default {
}
.contract-basic-section__date-range { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center; gap: 8px; width: 360px; max-width: 100%; }
.contract-basic-section__date-range span { color: #606266; }
// 风险客商提示条(基本信息备注后):橙黄背景 + 红色文字,与合同新增页一致
.contract-manage-form__risk-tip { margin: 0 0 18px; padding: 10px 16px; background: #faecd8; border-radius: 4px; }
.contract-manage-form__risk-tip-title { color: #f56c6c; font-weight: 600; line-height: 22px; }
.contract-manage-form__risk-tip-content { color: #f56c6c; line-height: 22px; }
.contract-basic-section__date-range :deep(.el-date-editor) { width: auto; min-width: 0; }
.contract-basic-section :deep(.el-form-item) { align-items: center; }
.contract-basic-section :deep(.el-form-item__label) { flex: 0 0 96px !important; width: 96px !important; height: auto; padding-right: 12px; white-space: normal; word-break: break-all; line-height: 18px; }
+256 -8
View File
@@ -32,6 +32,14 @@
>
批量导出
</el-button>
<el-button
type="warning"
plain
:loading="hcsRepushingId === 'batch'"
@click="handleHcsBatchRepush"
>
<el-icon class="el-icon--left"><Position /></el-icon>批量推送合规
</el-button>
</template>
<template #header>
@@ -72,6 +80,18 @@
</el-tag>
</template>
<template #hcsSyncStatus="{ row }">
<el-tooltip v-if="row.hcsSyncMsg" placement="top" :content="row.hcsSyncMsg">
<el-tag :type="hcsSyncTagType(row.hcsSyncStatus)" class="status-text">
{{ hcsSyncLabel(row.hcsSyncStatus) }}
</el-tag>
</el-tooltip>
<el-tag v-else-if="row.hcsSyncStatus" :type="hcsSyncTagType(row.hcsSyncStatus)" class="status-text">
{{ hcsSyncLabel(row.hcsSyncStatus) }}
</el-tag>
<span v-else>-</span>
</template>
<template #menu="{ row }">
<el-link
v-if="hasPermission(`${config.permission}_view`)"
@@ -92,6 +112,9 @@
>
{{ operation.label }}
</el-link>
<el-link v-if="canHcsRepush(row)" type="warning" @click="handleHcsRepush(row)">
{{ hcsRepushingId === row.id ? '推送中…' : '推送合规' }}
</el-link>
<el-link v-if="canDelete(row)" type="danger" @click="removeRow(row)">删除</el-link>
</template>
</avue-crud>
@@ -180,9 +203,18 @@
<el-descriptions-item label="其他事项说明" :span="3">{{
detailValue('remark')
}}</el-descriptions-item>
<el-descriptions-item v-if="detailRiskTipText" label="风险客商提示" :span="3">
<span style="color: #f56c6c">{{ detailRiskTipText }}</span>
</el-descriptions-item>
</el-descriptions>
</section>
<hcs-contract-panel
v-if="!isPublicViewPage"
:contract="detailRow"
@refresh="initDetailPage"
/>
<contract-attachment-section
title="用印材料"
description
@@ -302,6 +334,7 @@
<contract-attachment-section
title="附件(佐证材料等)"
description
attachment-type
readonly
:rows="detailAttachmentRows"
:preview="previewAttachment"
@@ -446,6 +479,7 @@
clearable
:loading="contractPartyLoading"
@visible-change="visible => visible && loadContractPartyOptions()"
@change="queryPartyRisk('partyA')"
>
<el-option
v-for="item in contractPartyAOptions"
@@ -463,6 +497,7 @@
clearable
:loading="contractPartyLoading"
@visible-change="visible => visible && loadContractPartyOptions()"
@change="queryPartyRisk('partyB')"
>
<el-option
v-for="item in contractPartyBOptions"
@@ -617,6 +652,11 @@
placeholder="请输入其他事项说明"
/>
</el-form-item>
<!-- 风险客商提示(基本信息备注后):外部客商高风险/黑名单时展示,文案由后端 riskTip 拼装 -->
<div v-if="partyRiskText" class="contract-manage-form__risk-tip">
<div class="contract-manage-form__risk-tip-title">风险客商提示:</div>
<div class="contract-manage-form__risk-tip-content">{{ partyRiskText }}</div>
</div>
</section>
<contract-attachment-section
@@ -1049,6 +1089,8 @@ import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
import { exportBlob } from '@/api/common';
import * as api from '@/api/business/contract-manage';
import { repush as hcsRepush, repushBatch as hcsRepushBatch, getRisk } from '@/api/business/hcs';
import { Position } from '@element-plus/icons-vue';
import { getMkPublicDetail } from '@/api/mk-process';
import { getList as getProjectList } from '@/api/business/project-apply';
import { getList as getCustomerArchiveList } from '@/api/vehicle/customer-archive';
@@ -1069,6 +1111,7 @@ import ContractAttachmentSection, {
isContractSupportingAttachmentLocation,
normalizeContractFileRows,
} from './components/contract-attachment-section.vue';
import HcsContractPanel from './components/hcs-contract-panel.vue';
import PdfPreview from '@/components/pdf-preview/main.vue';
import { ElImageViewer, ElCascader } from 'element-plus';
import { OpenFileViewer } from '@open-file-viewer/vue';
@@ -1174,6 +1217,11 @@ const createCopiedDetail = (detail = {}) => {
Object.keys(form).forEach(prop => {
if (Object.prototype.hasOwnProperty.call(detail, prop)) form[prop] = detail[prop];
});
// 特批附件属于原合同的特批佐证,复制后须对新合同重新上传,否则会绕过高风险/黑名单校验
const stripSpecAttachments = value => {
const rows = parseArray(value).filter(item => item?.fileType !== '特批附件');
return rows.length ? JSON.stringify(rows) : '';
};
return {
...form,
contractNo: '',
@@ -1181,8 +1229,8 @@ const createCopiedDetail = (detail = {}) => {
archiveStatus: '未归档',
feeGenerationMode: detail.feeGenerationMode || '',
billingEnabled: detail.billingEnabled,
contractFileJson: detail.contractFileJson || '',
attachmentsJson: detail.attachmentsJson || '',
contractFileJson: stripSpecAttachments(detail.contractFileJson),
attachmentsJson: stripSpecAttachments(detail.attachmentsJson),
billingPlanJson: detail.billingPlanJson || '',
paymentRatioJson: detail.paymentRatioJson || '',
settlementRuleJson: detail.settlementRuleJson || '',
@@ -1241,9 +1289,11 @@ export default {
ContractAttachmentSection,
BillingPlanEditor,
ChangeRecordDetailDialog,
HcsContractPanel,
ElImageViewer,
OpenFileViewer,
PdfPreview,
Position,
},
data() {
return {
@@ -1335,6 +1385,9 @@ export default {
contractCategoryDictOptions: [],
contractPartyAOptions: [],
contractPartyBOptions: [],
// 合规客商风险(C101-1 展示用):甲方/乙方选择后即时查询
contractPartyCodeMap: {},
partyRisk: {},
contractPartyLoading: false,
contractPartyRequestId: 0,
organizationOptions: [],
@@ -1373,6 +1426,8 @@ export default {
billCycleTypeOptions: ['固定截单日', '自然月', '自定义多周期'],
detailLoading: false,
detailRow: {},
// 合规(HCS)推送/对账状态
hcsRepushingId: null,
detailContractFileRows: [],
detailAttachmentRows: [],
detailBillingPlanRows: [],
@@ -1549,6 +1604,32 @@ export default {
const endDate = this.detailRow.endDate || '-';
return `${startDate} 至 ${endDate}`;
},
// 高风险/黑名单客商签约提示(C101-1,签约前提示;强制拦截在后端提交校验与推送链路)
partyRiskText() {
const parts = [];
['partyA', 'partyB'].forEach(prop => {
const risk = this.partyRisk[prop];
if (!risk?.risky) return;
// riskTip 由后端按风险项拼装(【公司名称】:黑名单;涉诉;被执行人等),缺失时兜底通用提示
parts.push(
risk.riskTip ||
`${prop === 'partyA' ? '甲方' : '乙方'}[${this.form[prop] || ''}]为高风险/黑名单客商`
);
});
if (!parts.length) return '';
return `${parts.join(';')},按集团规定不可直接签约,提交前须上传「特批附件」类型附件,审批通过后将自动发起合同特批`;
},
// 详情风险客商提示(基本信息备注后展示):按推送留痕 hcs_risk_level/hcs_black_status 判定(甲乙取高/任一),文案与后端 riskTip 一致
detailRiskTipText() {
const riskLevel = String(this.detailRow.hcsRiskLevel || '').trim();
const blacklist = String(this.detailRow.hcsBlackStatus || '').toLowerCase() === 'yes';
if (riskLevel !== '2' && !blacklist) return '';
const parties = [this.detailRow.partyA, this.detailRow.partyB]
.map(name => String(name || '').trim())
.filter(Boolean)
.join('、');
return `【${parties || '相对方'}】:被列入外部黑名单;涉贪污受贿裁判相关提及方(3年以上及其他);被执行人信息;买卖合同纠纷;劳动纠纷;重大纠纷`;
},
detailLegalSealText() {
const value = this.detailRow.legalSealFlag;
if (value === undefined || value === null || value === '') return '-';
@@ -1670,6 +1751,92 @@ export default {
if (stage === 'terminated') return 'danger';
return 'info';
},
// ==================== 合规(HCS)推送/对账(照“同步金蝶”按钮模式) ====================
hcsSyncLabel(value) {
return { unsynced: '待推送', synced: '已推送', failed: '推送失败' }[value] || value;
},
hcsSyncTagType(value) {
return { unsynced: 'info', synced: 'success', failed: 'danger' }[value] || 'info';
},
canHcsRepush(row) {
return (
['approved', 'change_approved'].includes(row.approvalStatus) &&
row.hcsSyncStatus !== 'synced'
);
},
handleHcsRepush(row) {
if (this.hcsRepushingId) return;
this.$confirm('确定推送该数据到合同合规系统?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
// 同步推送合规耗时较长,加遮罩防重复操作(照审批通过遮罩)
const loading = this.$loading({
lock: true,
text: '正在推送合规,请稍候…',
background: 'rgba(255, 255, 255, 0.6)',
});
this.hcsRepushingId = row.id;
hcsRepush('contract', row.id)
.then(res => {
if (res.data?.success === false) {
this.$message.error(res.data?.msg || '推送合规失败');
return;
}
this.$message.success(res.data?.data || '推送合规成功');
this.onLoad(this.page, this.query);
})
.catch(() => {
// 失败原因由 axios 拦截器统一弹出后端 msg,此处不再重复提示
})
.finally(() => {
this.hcsRepushingId = null;
loading.close();
});
});
},
handleHcsBatchRepush() {
if (this.hcsRepushingId) return;
if (!this.selectionList.length) {
this.$message.warning('请选择至少一条数据');
return;
}
const invalidCount = this.selectionList.filter(item => !this.canHcsRepush(item)).length;
if (invalidCount) {
this.$message.warning(`选中数据中有${invalidCount}条不符合推送条件(仅审批通过且未同步的可推送),请重新选择`);
return;
}
this.$confirm(`确定将选中的${this.selectionList.length}条数据推送到合同合规系统?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
// 同步推送合规耗时较长,加遮罩防重复操作(照审批通过遮罩)
const loading = this.$loading({
lock: true,
text: '正在批量推送合规,请稍候…',
background: 'rgba(255, 255, 255, 0.6)',
});
this.hcsRepushingId = 'batch';
hcsRepushBatch('contract', this.selectionList.map(item => item.id))
.then(res => {
if (res.data?.success === false) {
this.$message.error(res.data?.msg || '批量推送合规失败');
return;
}
this.$message.success(res.data?.data || '批量推送合规完成');
this.onLoad(this.page, this.query);
})
.catch(() => {
// 失败原因由 axios 拦截器统一弹出后端 msg,此处不再重复提示
})
.finally(() => {
this.hcsRepushingId = null;
loading.close();
});
});
},
canEdit(row) {
return (
this.hasPermission(`${this.config.permission}_edit`) &&
@@ -2246,8 +2413,21 @@ export default {
])
.then(responses => {
if (requestId !== this.contractPartyRequestId) return;
const names = responses
.flatMap(extractRecords)
const records = responses.flatMap(extractRecords);
// 名称→客商编码映射,供 C101-1 风险查询使用(§9-U6 编码体系联调确认)
const codeMap = {};
records.forEach(item => {
const code = item.customerCode;
if (!code) return;
[item.fullName, item.shortName, item.customerName]
.map(name => String(name || '').trim())
.filter(Boolean)
.forEach(name => {
codeMap[name] = code;
});
});
this.contractPartyCodeMap = codeMap;
const names = records
.map(item => item.fullName || item.shortName || item.customerName || item.customerCode)
.map(name => String(name || '').trim())
.filter(Boolean);
@@ -2255,11 +2435,40 @@ export default {
const options = [...new Set(names)].map(name => ({ label: name, value: name }));
this.contractPartyAOptions = options;
this.contractPartyBOptions = options;
// 复制/编辑回填的甲乙方不会触发下拉 change,加载选项后主动查一次 C101-1 风险
['partyA', 'partyB'].forEach(prop => {
if (this.form[prop]) this.queryPartyRisk(prop);
});
})
.finally(() => {
if (requestId === this.contractPartyRequestId) this.contractPartyLoading = false;
});
},
// C101-1 客商风险查询(展示用,强校验在后端提交/推送链路):失败静默不阻塞填写
queryPartyRisk(prop) {
const name = this.form[prop];
const code = name ? this.contractPartyCodeMap[name] : null;
if (!code) {
this.partyRisk = { ...this.partyRisk, [prop]: null };
return;
}
getRisk(code)
.then(res => {
const risk = res?.data?.data ?? res?.data ?? {};
this.partyRisk = {
...this.partyRisk,
[prop]: {
customerKind: risk.customerKind || '',
// risky 由后端统一判定:外部客商高风险(等级=2)或黑名单;内部组织客商默认无风险恒为 false
risky: risk.risky === true,
riskTip: risk.riskTip || '',
},
};
})
.catch(() => {
this.partyRisk = { ...this.partyRisk, [prop]: null };
});
},
loadOrganizationOptions() {
if (this.organizationLoading) return;
this.organizationLoading = true;
@@ -3046,10 +3255,26 @@ export default {
})
)
: request();
afterMk.then(() => {
this.$message.success(operation.successMessage || `${operation.label}成功`);
this.onLoad(this.page, this.query);
});
// 合同通过会同步等待附件转传 + 合规推送,加遮罩防重复点击
const loading =
operation.action === 'approve'
? this.$loading({
lock: true,
text: '正在推送合规,请稍候…',
background: 'rgba(255, 255, 255, 0.6)',
})
: null;
afterMk
.then(res => {
// data=false 表示审批已通过但合规推送失败,msg 带具体原因(照正式结算单审批)
if (operation.action === 'approve' && res?.data?.data === false) {
this.$message.warning(res.data.msg);
} else {
this.$message.success(operation.successMessage || `${operation.label}成功`);
}
this.onLoad(this.page, this.query);
})
.finally(() => loading && loading.close());
};
if (operation.prompt) {
this.$prompt(operation.prompt, operation.label, {
@@ -3146,6 +3371,29 @@ export default {
vertical-align: bottom;
}
.contract-manage-form__risk-alert {
width: 100%;
}
// 风险客商提示条(基本信息备注后):橙黄背景 + 红色文字(设计稿 2026-10)
.contract-manage-form__risk-tip {
margin: 0 0 18px;
padding: 10px 16px;
background: #faecd8;
border-radius: 4px;
.contract-manage-form__risk-tip-title {
color: #f56c6c;
font-weight: 600;
line-height: 22px;
}
.contract-manage-form__risk-tip-content {
color: #f56c6c;
line-height: 22px;
}
}
.contract-manage-page {
&__expiry-search {
display: flex;
+100
View File
@@ -0,0 +1,100 @@
<template>
<basic-container class="hcs-master-data-page">
<div class="hcs-master-data-page__head">
<div class="dialog-section-title">合规主数据(HCS)</div>
<el-button type="primary" :loading="refreshing" @click="handleRefresh">刷新缓存</el-button>
</div>
<el-tabs v-model="activeTab">
<el-tab-pane label="合同模板" name="template" />
</el-tabs>
<el-table v-loading="loading" :data="templates" border>
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column prop="fdId" label="模板ID(fdTemplateId)" min-width="320" show-overflow-tooltip />
<el-table-column prop="fdName" label="模板名称" min-width="220" show-overflow-tooltip />
<el-table-column prop="fdStatus" label="状态" width="100" align="center" />
<el-table-column prop="fdContractCharacter" label="合同性质" min-width="140" align="center" />
<el-table-column label="创建时间" min-width="170" align="center">
<template #default="{ row }">{{ formatTime(row.fdCreateTime) }}</template>
</el-table-column>
</el-table>
<el-alert
class="hcs-master-data-page__tip"
title="合同类别与合规模板的对应关系由 Nacos 配置 thirdParty.hcs.contract.template-mapping 维护;此列表用于核对映射的模板ID 是否存在。"
type="info"
:closable="false"
/>
</basic-container>
</template>
<script>
import { getTemplates, refreshMasterData } from '@/api/business/hcs';
export default {
name: 'HcsMasterData',
data() {
return {
activeTab: 'template',
loading: false,
refreshing: false,
templates: [],
};
},
created() {
this.loadTemplates();
},
methods: {
loadTemplates() {
this.loading = true;
getTemplates()
.then(response => {
const data = response?.data?.data ?? response?.data;
this.templates = Array.isArray(data) ? data : [];
})
.catch(() => {
this.$message.error('合规模板列表加载失败');
})
.finally(() => {
this.loading = false;
});
},
handleRefresh() {
this.refreshing = true;
refreshMasterData()
.then(response => {
this.$message.success(response?.data?.data || '合规主数据缓存已刷新');
this.loadTemplates();
})
.catch(error => {
this.$message.error(error?.response?.data?.msg || '刷新失败');
})
.finally(() => {
this.refreshing = false;
});
},
formatTime(value) {
if (!value) return '-';
return this.$dayjs(Number(value)).format('YYYY-MM-DD HH:mm:ss');
},
},
};
</script>
<style lang="scss" scoped>
.hcs-master-data-page__head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
.dialog-section-title {
margin-bottom: 0;
}
}
.hcs-master-data-page__tip {
margin-top: 12px;
}
</style>
+149 -8
View File
@@ -52,12 +52,37 @@
>
批量删除
</el-button>
<el-button
type="warning"
plain
v-if="!readonlyScope"
:loading="hcsRepushingId === 'batch'"
@click="handleHcsBatchRepush"
>
<el-icon class="el-icon--left"><Position /></el-icon>批量推送合规
</el-button>
</template>
<template #approvalStatus="{ row }">
{{ displayStatus(row, 'approvalStatus') }}
</template>
<template #hcsSyncStatus="{ row }">
<el-tooltip v-if="row.hcsSyncMsg" placement="top" :content="row.hcsSyncMsg">
<el-tag :type="hcsSyncTagType(row.hcsSyncStatus)" class="status-text">
{{ hcsSyncLabel(row.hcsSyncStatus) }}
</el-tag>
</el-tooltip>
<el-tag
v-else-if="row.hcsSyncStatus"
:type="hcsSyncTagType(row.hcsSyncStatus)"
class="status-text"
>
{{ hcsSyncLabel(row.hcsSyncStatus) }}
</el-tag>
<span v-else>-</span>
</template>
<template #header>
<div class="project-apply-page__risk-search">
<span class="project-apply-page__risk-label">
@@ -128,6 +153,13 @@
>
{{ operation.label }}
</el-link>
<el-link
v-if="canHcsRepush(row)"
type="warning"
@click="handleHcsRepush(row)"
>
{{ hcsRepushingId === row.id ? '推送中…' : '推送合规' }}
</el-link>
<el-link type="danger" v-if="canDelete(row)" @click="rowDel(row)"> 删除 </el-link>
</template>
</avue-crud>
@@ -916,6 +948,8 @@
import { exportBlob } from '@/api/common';
import { defineAsyncComponent } from 'vue';
import * as api from '@/api/business/project-apply';
import { repush as hcsRepush, repushBatch as hcsRepushBatch } from '@/api/business/hcs';
import { Position } from '@element-plus/icons-vue';
import { getMkPublicDetail } from '@/api/mk-process';
import {
getList as getCustomerArchiveList,
@@ -1060,6 +1094,7 @@ export default {
ElImageViewer,
OpenFileViewer,
PdfPreview,
Position,
},
data() {
const validateAmount = (rule, value, callback) => {
@@ -1121,6 +1156,8 @@ export default {
{ label: '高风险', value: 'high', statKey: 'high' },
{ label: '中风险', value: 'medium', statKey: 'medium' },
],
// 合规(HCS)推送状态
hcsRepushingId: null,
page: {
pageSize: 10,
pageSizes: [10, 20, 50, 100],
@@ -1613,6 +1650,93 @@ export default {
return this.canOperation(operation, row);
});
},
// ==================== 合规(HCS)推送/对账(照“同步金蝶”按钮模式) ====================
hcsSyncLabel(value) {
return { unsynced: '待推送', synced: '已推送', failed: '推送失败' }[value] || value;
},
hcsSyncTagType(value) {
return { unsynced: 'info', synced: 'success', failed: 'danger' }[value] || 'info';
},
canHcsRepush(row) {
return (
!this.readonlyScope &&
['approved', 'change_approved'].includes(row.approvalStatus) &&
row.hcsSyncStatus !== 'synced'
);
},
handleHcsRepush(row) {
if (this.hcsRepushingId) return;
this.$confirm('确定推送该数据到合同合规系统?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
// 同步推送合规耗时较长,加遮罩防重复操作(照审批通过遮罩)
const loading = this.$loading({
lock: true,
text: '正在推送合规,请稍候…',
background: 'rgba(255, 255, 255, 0.6)',
});
this.hcsRepushingId = row.id;
hcsRepush('project', row.id)
.then(res => {
if (res.data?.success === false) {
this.$message.error(res.data?.msg || '推送合规失败');
return;
}
this.$message.success(res.data?.data || '推送合规成功');
this.onLoad(this.page, this.query);
})
.catch(() => {
// 失败原因由 axios 拦截器统一弹出后端 msg,此处不再重复提示
})
.finally(() => {
this.hcsRepushingId = null;
loading.close();
});
});
},
handleHcsBatchRepush() {
if (this.hcsRepushingId) return;
if (!this.selectionList.length) {
this.$message.warning('请选择至少一条数据');
return;
}
const invalidCount = this.selectionList.filter(item => !this.canHcsRepush(item)).length;
if (invalidCount) {
this.$message.warning(`选中数据中有${invalidCount}条不符合推送条件(仅审批通过且未同步的可推送),请重新选择`);
return;
}
this.$confirm(`确定将选中的${this.selectionList.length}条数据推送到合同合规系统?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
// 同步推送合规耗时较长,加遮罩防重复操作(照审批通过遮罩)
const loading = this.$loading({
lock: true,
text: '正在批量推送合规,请稍候…',
background: 'rgba(255, 255, 255, 0.6)',
});
this.hcsRepushingId = 'batch';
hcsRepushBatch('project', this.selectionList.map(item => item.id))
.then(res => {
if (res.data?.success === false) {
this.$message.error(res.data?.msg || '批量推送合规失败');
return;
}
this.$message.success(res.data?.data || '批量推送合规完成');
this.onLoad(this.page, this.query);
})
.catch(() => {
// 失败原因由 axios 拦截器统一弹出后端 msg,此处不再重复提示
})
.finally(() => {
this.hcsRepushingId = null;
loading.close();
});
});
},
canOperation(operation, row) {
const permission = operation.permission || `${this.config.permission}_${operation.action}`;
if (!this.hasPermission(permission) || this.readonlyScope || row.readonly) return false;
@@ -1754,14 +1878,31 @@ export default {
mkEnabled: this.mkEnabled,
}).then(() => request())
: request();
afterMk.then(res => {
if (res.data?.success === false || res.data?.data === false) {
this.$message.error(res.data?.msg || `${operation.label}失败,请联系管理员`);
return;
}
this.$message.success(`${operation.label}成功`);
this.onLoad(this.page, this.query);
});
// 项目通过会同步等待合规推送,加遮罩防重复点击
const loading =
operation.action === 'approve'
? this.$loading({
lock: true,
text: '正在推送合规,请稍候…',
background: 'rgba(255, 255, 255, 0.6)',
})
: null;
afterMk
.then(res => {
// data=false 表示审批已通过但合规推送失败,msg 带具体原因(照正式结算单审批)
if (operation.action === 'approve' && res.data?.data === false) {
this.$message.warning(res.data?.msg);
this.onLoad(this.page, this.query);
return;
}
if (res.data?.success === false) {
this.$message.error(res.data?.msg || `${operation.label}失败,请联系管理员`);
return;
}
this.$message.success(`${operation.label}成功`);
this.onLoad(this.page, this.query);
})
.finally(() => loading && loading.close());
};
if (operation.prompt) {
this.$prompt(operation.prompt, operation.label, {