diff --git a/src/api/business/contract-manage.js b/src/api/business/contract-manage.js index 872a5ea..b7ce897 100644 --- a/src/api/business/contract-manage.js +++ b/src/api/business/contract-manage.js @@ -36,6 +36,8 @@ export const approve = id => url: `${baseUrl}/approve`, method: 'post', params: { id }, + // 审批通过后同步推送合规(附件转传可达 20s+),覆盖全局 10s 超时 + timeout: 120000, }); export const reject = id => diff --git a/src/api/business/hcs.js b/src/api/business/hcs.js new file mode 100644 index 0000000..47012a2 --- /dev/null +++ b/src/api/business/hcs.js @@ -0,0 +1,96 @@ +import request from '@/axios'; + +const baseUrl = '/blade-transport/hcs'; + +// 合规推送涉及 HCS 远程多次调用(风险校验+附件转传+起草),可达 20s+,覆盖全局 10s 超时 +export const repush = (bizType, id) => + request({ + url: `${baseUrl}/sync/${bizType}/${id}`, + method: 'post', + timeout: 60000, + }); + +// 批量推送为逐条同步推送,耗时随勾选行数线性增长,进一步放宽超时 +export const repushBatch = (bizType, ids) => + request({ + url: `${baseUrl}/sync/${bizType}/batch`, + method: 'post', + data: ids, + timeout: 300000, + }); + +export const getTemplates = () => + request({ + url: `${baseUrl}/master-data/template`, + method: 'get', + }); + +export const refreshMasterData = () => + request({ + url: `${baseUrl}/master-data/refresh`, + method: 'post', + timeout: 60000, + }); + +export const getRisk = customerCode => + request({ + url: `${baseUrl}/risk`, + method: 'get', + params: { customerCode }, + timeout: 30000, + }); + +// 客商风险批量查询(客商列表页展示用):入参编号数组,后端逐个查 C101-1,单个失败跳过 +export const getRiskBatch = customerCodes => + request({ + url: `${baseUrl}/risk/batch`, + method: 'get', + params: { customerCodes: (customerCodes || []).join(',') }, + timeout: 60000, + }); + +// 客商风险按名称查询(客商档案表单展示用):名称反查编号后口径同编号查询 +export const getRiskByName = customerName => + request({ + url: `${baseUrl}/risk/by-name`, + method: 'get', + params: { customerName }, + timeout: 30000, + }); + +export const uploadAttachment = formData => + request({ + url: `${baseUrl}/attachment/upload`, + method: 'post', + data: formData, + timeout: 60000, + }); + +export const downloadAttachment = (fdId, name) => + request({ + url: `${baseUrl}/attachment/download`, + method: 'get', + params: { fdId, name }, + responseType: 'blob', + timeout: 120000, + }); + +export const applyContractSpec = data => + request({ + url: `${baseUrl}/contract/spec-apply`, + method: 'post', + data, + timeout: 60000, + }); + +export const confirmContractSpec = (id, result) => + request({ + url: `${baseUrl}/contract/spec/confirm`, + method: 'post', + params: { id, result }, + }); + +export const unwrapData = response => { + const data = response?.data?.data ?? response?.data; + return data && typeof data === 'object' && 'data' in data ? data.data : data; +}; diff --git a/src/api/business/project-apply.js b/src/api/business/project-apply.js index 83ac915..bf12e6a 100644 --- a/src/api/business/project-apply.js +++ b/src/api/business/project-apply.js @@ -43,6 +43,8 @@ export const approve = id => url: `${baseUrl}/approve`, method: 'post', params: { id }, + // 审批通过后同步推送合规(远程多次调用),覆盖全局 10s 超时 + timeout: 60000, }); export const reject = id => diff --git a/src/api/common.js b/src/api/common.js index 854e4d2..7e03551 100644 --- a/src/api/common.js +++ b/src/api/common.js @@ -1,6 +1,9 @@ import request from '@/axios'; import { ElLoading, ElMessage } from 'element-plus'; +/** 审核按钮统一超时:审批通过后可能同步推送金蝶等耗时操作,覆盖全局 10s */ +export const APPROVE_TIMEOUT = 30000; + /** * 文件流返回 * @param url 接口地址 diff --git a/src/api/payment/invoiceApplication.js b/src/api/payment/invoiceApplication.js index 717703d..af6fb87 100644 --- a/src/api/payment/invoiceApplication.js +++ b/src/api/payment/invoiceApplication.js @@ -1,4 +1,5 @@ import request from '@/axios'; +import { APPROVE_TIMEOUT } from '@/api/common'; const baseUrl = '/blade-transport/invoice-application'; @@ -18,7 +19,8 @@ export const getReceiverInformation = settlementIds => export const save = data => request({ url: `${baseUrl}/save`, method: 'post', data }); export const remove = id => request({ url: `${baseUrl}/remove`, method: 'post', params: { id } }); export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post', data }); -export const approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data }); +export const approve = data => + request({ url: `${baseUrl}/approve`, method: 'post', data, timeout: APPROVE_TIMEOUT }); export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data }); export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data }); export const syncKingdee = id => diff --git a/src/api/payment/invoiceReceipt.js b/src/api/payment/invoiceReceipt.js index 071eff1..8db2dfb 100644 --- a/src/api/payment/invoiceReceipt.js +++ b/src/api/payment/invoiceReceipt.js @@ -1,4 +1,5 @@ import request from '@/axios'; +import { APPROVE_TIMEOUT } from '@/api/common'; const baseUrl = '/blade-transport/invoice-receipt'; @@ -31,7 +32,8 @@ export const getReferenceInformation = settlementIds => export const save = data => request({ url: `${baseUrl}/save`, method: 'post', data }); export const remove = id => request({ url: `${baseUrl}/remove`, method: 'post', params: { id } }); export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post', data }); -export const approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data }); +export const approve = data => + request({ url: `${baseUrl}/approve`, method: 'post', data, timeout: APPROVE_TIMEOUT }); export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data }); export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data }); export const pushKingdee = ids => diff --git a/src/api/payment/paymentApplication.js b/src/api/payment/paymentApplication.js index 59c9aa1..e464552 100644 --- a/src/api/payment/paymentApplication.js +++ b/src/api/payment/paymentApplication.js @@ -1,4 +1,5 @@ import request from '@/axios'; +import { APPROVE_TIMEOUT } from '@/api/common'; const baseUrl = '/blade-transport/payment-application'; export const getList = (current, size, params) => @@ -20,7 +21,8 @@ export const getProjectAdvanceWriteOffOptions = params => export const save = data => request({ url: `${baseUrl}/save`, method: 'post', data }); export const remove = id => request({ url: `${baseUrl}/remove`, method: 'post', params: { id } }); export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post', data }); -export const approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data }); +export const approve = data => + request({ url: `${baseUrl}/approve`, method: 'post', data, timeout: APPROVE_TIMEOUT }); export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data }); export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data }); // 同步金蝶涉及多次金蝶远程接口调用(A103-2 预检 + A103-1/A103-3 推送),单笔可达 20s+,覆盖全局 10s 超时 diff --git a/src/api/settlement/formalSettlement.js b/src/api/settlement/formalSettlement.js index ea4cb14..2d980d4 100644 --- a/src/api/settlement/formalSettlement.js +++ b/src/api/settlement/formalSettlement.js @@ -1,4 +1,5 @@ import request from '@/axios'; +import { APPROVE_TIMEOUT } from '@/api/common'; const baseUrl = '/blade-transport/formal-settlement'; @@ -27,7 +28,8 @@ export const getCandidateDetails = (current, size, params) => export const save = data => request({ url: `${baseUrl}/save`, method: 'post', data }); export const remove = id => request({ url: `${baseUrl}/remove`, method: 'post', params: { id } }); export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post', data }); -export const approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data }); +export const approve = data => + request({ url: `${baseUrl}/approve`, method: 'post', data, timeout: APPROVE_TIMEOUT }); export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data }); export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data }); // 同步金蝶涉及多次金蝶远程接口调用(A102-4 预检 + A102-2 保存),可达 20s+,覆盖全局 10s 超时 diff --git a/src/api/settlement/preSettlement.js b/src/api/settlement/preSettlement.js index e35f2a6..9aa2755 100644 --- a/src/api/settlement/preSettlement.js +++ b/src/api/settlement/preSettlement.js @@ -1,4 +1,5 @@ import request from '@/axios'; +import { APPROVE_TIMEOUT } from '@/api/common'; const baseUrl = '/blade-transport/pre-settlement'; @@ -42,7 +43,8 @@ export const removeDetail = (id, detailId) => export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post', data }); -export const approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data }); +export const approve = data => + request({ url: `${baseUrl}/approve`, method: 'post', data, timeout: APPROVE_TIMEOUT }); export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data }); diff --git a/src/api/system/business-process.js b/src/api/system/business-process.js index 5d0f502..0ea48d5 100644 --- a/src/api/system/business-process.js +++ b/src/api/system/business-process.js @@ -1,5 +1,8 @@ import request from '@/axios'; +/** 提交审核流(MK)统一超时:客商/项目/合同/预结算/正式结算/付款/开票/收票/汇票付款 */ +export const MK_SUBMIT_TIMEOUT = 30000; + /** * 调用 MK processSubmit 提交审核流 * @param {Object} data @@ -15,6 +18,7 @@ export const processSubmit = data => { url: '/blade-system/businessProcess/processSubmit', method: 'post', data, + timeout: MK_SUBMIT_TIMEOUT, }); }; @@ -28,6 +32,7 @@ export const processDelete = data => { url: '/blade-system/businessProcess/processDelete', method: 'post', data, + timeout: MK_SUBMIT_TIMEOUT, }); }; diff --git a/src/option/business/common.js b/src/option/business/common.js index c142601..729199e 100644 --- a/src/option/business/common.js +++ b/src/option/business/common.js @@ -1,4 +1,4 @@ -export const transportTypeOptions = [ +export const transportTypeOptions = [ { label: '公路运输', value: '公路运输' }, { label: '铁路运输', value: '铁路运输' }, { label: '水路运输', value: '水路运输' }, @@ -144,6 +144,17 @@ export const contractArchiveStatusOptions = [ { label: '已归档', value: '已归档' }, ]; +export const hcsSyncStatusOptions = [ + { label: '待推送', value: 'unsynced' }, + { label: '已推送', value: 'synced' }, + { label: '推送失败', value: 'failed' }, +]; + +export const yesNoOptions = [ + { label: '是', value: true }, + { label: '否', value: false }, +]; + export const nodeOptions = [ { label: '接单', value: '接单' }, { label: '到场', value: '到场' }, diff --git a/src/option/business/contract-manage.js b/src/option/business/contract-manage.js index e59adf3..9efc345 100644 --- a/src/option/business/contract-manage.js +++ b/src/option/business/contract-manage.js @@ -7,9 +7,11 @@ import { createCrudOption, effectiveTypeOptions, nonNegativeRule, + hcsSyncStatusOptions, selectRule, signTypeOptions, textRule, + yesNoOptions, } from './common'; const contractSettlementModeOptions = [ @@ -606,6 +608,50 @@ export const option = createCrudOption([ addDisplay: false, editDisplay: false, }, + { + label: '合规状态', + prop: 'hcsSyncStatus', + type: 'select', + search: true, + searchOrder: 6, + searchPlaceholder: '请选择', + slot: true, + dicData: hcsSyncStatusOptions, + minWidth: 110, + addDisplay: false, + editDisplay: false, + }, + { + label: '金蝶合同ID', + prop: 'kingdeeId', + minWidth: 130, + addDisplay: false, + editDisplay: false, + }, + { + label: '是否同步金蝶', + prop: 'kingdeeSynced', + type: 'select', + search: true, + searchPlaceholder: '请选择', + searchOrder: 7, + dicData: yesNoOptions, + hide: true, + addDisplay: false, + editDisplay: false, + }, + { + label: '是否同步合规', + prop: 'hcsSynced', + type: 'select', + search: true, + searchPlaceholder: '请选择', + searchOrder: 8, + dicData: yesNoOptions, + hide: true, + addDisplay: false, + editDisplay: false, + }, { label: '当前节点', prop: 'currentNode', diff --git a/src/option/business/project-apply.js b/src/option/business/project-apply.js index 502032e..08c0eef 100644 --- a/src/option/business/project-apply.js +++ b/src/option/business/project-apply.js @@ -3,6 +3,7 @@ import { businessModeOptions, businessTypeOptions, createCrudOption, + hcsSyncStatusOptions, nonNegativeRule, projectApprovalStatusOptions, projectSourceOptions, @@ -10,6 +11,7 @@ import { selectRule, settlementModeOptions, textRule, + yesNoOptions, } from './common'; const projectEffectiveTypeOptions = [ @@ -406,6 +408,57 @@ export const option = createCrudOption([ addDisplay: false, editDisplay: false, }, + { + label: '合规状态', + prop: 'hcsSyncStatus', + type: 'select', + search: true, + searchPlaceholder: '请选择', + searchOrder: 1, + slot: true, + dicData: hcsSyncStatusOptions, + minWidth: 110, + addDisplay: false, + editDisplay: false, + }, + { + label: '金蝶项目ID', + prop: 'kingdeeId', + minWidth: 130, + addDisplay: false, + editDisplay: false, + }, + { + label: '金蝶项目编号', + prop: 'kingdeeNumber', + minWidth: 130, + addDisplay: false, + editDisplay: false, + }, + { + label: '是否同步金蝶', + prop: 'kingdeeSynced', + type: 'select', + search: true, + searchPlaceholder: '请选择', + searchOrder: 2, + dicData: yesNoOptions, + hide: true, + addDisplay: false, + editDisplay: false, + }, + { + label: '是否同步合规', + prop: 'hcsSynced', + type: 'select', + search: true, + searchPlaceholder: '请选择', + searchOrder: 3, + dicData: yesNoOptions, + hide: true, + addDisplay: false, + editDisplay: false, + }, { label: '生效类型', prop: 'effectiveType', diff --git a/src/views/business/components/contract-attachment-section.vue b/src/views/business/components/contract-attachment-section.vue index bd3f02f..d595931 100644 --- a/src/views/business/components/contract-attachment-section.vue +++ b/src/views/business/components/contract-attachment-section.vue @@ -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('上传成功'); }, diff --git a/src/views/business/components/hcs-contract-panel.vue b/src/views/business/components/hcs-contract-panel.vue new file mode 100644 index 0000000..2a3b1f3 --- /dev/null +++ b/src/views/business/components/hcs-contract-panel.vue @@ -0,0 +1,271 @@ + + + + + diff --git a/src/views/business/contract-manage-change.vue b/src/views/business/contract-manage-change.vue index 63f5f88..e3fef3c 100644 --- a/src/views/business/contract-manage-change.vue +++ b/src/views/business/contract-manage-change.vue @@ -251,6 +251,11 @@ :disabled="isSettlementRuleChange" /> + +
+
风险客商提示:
+
{{ riskTipText }}
+
String(name || '').trim()) + .filter(Boolean) + .join('、'); + return `【${parties || '相对方'}】:被列入外部黑名单;涉贪污受贿裁判相关提及方(3年以上及其他);被执行人信息;买卖合同纠纷;劳动纠纷;重大纠纷。提交变更前须上传「特批附件」类型附件`; + }, isSettlementRuleChange() { return this.form.changeType === '结算规则变更'; }, @@ -1045,6 +1061,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; } diff --git a/src/views/business/contract-manage.vue b/src/views/business/contract-manage.vue index 158b9cb..15b9161 100644 --- a/src/views/business/contract-manage.vue +++ b/src/views/business/contract-manage.vue @@ -32,6 +32,14 @@ > 批量导出 + + 批量推送合规 + + + @@ -180,9 +203,18 @@ {{ detailValue('remark') }} + + {{ detailRiskTipText }} + + + + +
+
风险客商提示:
+
{{ partyRiskText }}
+
{ 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; diff --git a/src/views/business/hcs-master-data.vue b/src/views/business/hcs-master-data.vue new file mode 100644 index 0000000..0cf5872 --- /dev/null +++ b/src/views/business/hcs-master-data.vue @@ -0,0 +1,100 @@ + + + + + diff --git a/src/views/business/project-apply.vue b/src/views/business/project-apply.vue index 75dcdfe..965f9a0 100644 --- a/src/views/business/project-apply.vue +++ b/src/views/business/project-apply.vue @@ -52,12 +52,37 @@ > 批量删除 + + 批量推送合规 + + + @@ -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, { diff --git a/src/views/payment/invoice-application.vue b/src/views/payment/invoice-application.vue index 969c41a..a447f77 100644 --- a/src/views/payment/invoice-application.vue +++ b/src/views/payment/invoice-application.vue @@ -270,6 +270,7 @@ export default { rows: [], selection: [], loading: false, + approveLoading: false, batchSyncing: false, page: { current: 1, size: 10, total: 0 }, mkEnabled: false, @@ -408,15 +409,29 @@ export default { }); }, async handleApprove(row) { - await this.$confirm('确认审批通过该开票申请?', '提示', { type: 'warning' }); - const res = await api.approve({ id: row.id }); - // data=false 表示审批已通过但自动同步金蝶失败,msg 带具体原因 - if (res.data.data === false) { - this.$message.warning(res.data.msg); - } else { - this.$message.success(res.data.msg || '审批通过'); + // 防止重复点击:请求进行中忽略后续点击,避免重复审批和重复提示 + if (this.approveLoading) return; + this.approveLoading = true; + let loading = null; + try { + await this.$confirm('确认审批通过该开票申请?', '提示', { type: 'warning' }); + loading = this.$loading({ + lock: true, + text: '审批处理中,请稍候…', + background: 'rgba(255, 255, 255, 0.6)', + }); + const res = await api.approve({ id: row.id }); + // data=false 表示审批已通过但自动同步金蝶失败,msg 带具体原因 + if (res.data.data === false) { + this.$message.warning(res.data.msg); + } else { + this.$message.success(res.data.msg || '审批通过'); + } + this.loadTable(); + } finally { + loading?.close(); + this.approveLoading = false; } - this.loadTable(); }, async handleReturn(row) { const { value } = await this.$prompt('请输入驳回原因', '审批驳回'); diff --git a/src/views/payment/invoice-receipt.vue b/src/views/payment/invoice-receipt.vue index 8710449..aed1115 100644 --- a/src/views/payment/invoice-receipt.vue +++ b/src/views/payment/invoice-receipt.vue @@ -255,6 +255,7 @@ export default { rows: [], selection: [], loading: false, + approveLoading: false, pushLoading: false, page: { current: 1, size: 10, total: 0 }, mkEnabled: false, @@ -365,15 +366,29 @@ export default { }); }, async handleApprove(row) { - await this.$confirm('确认审批通过该收票登记?', '提示', { type: 'warning' }); - const res = await api.approve({ id: row.id }); - // data=false 表示审批已通过但自动同步金蝶失败,msg 带具体原因 - if (res.data.data === false) { - this.$message.warning(res.data.msg); - } else { - this.$message.success(res.data.msg || '审批通过'); + // 防止重复点击:请求进行中忽略后续点击,避免重复审批和重复提示 + if (this.approveLoading) return; + this.approveLoading = true; + let loading = null; + try { + await this.$confirm('确认审批通过该收票登记?', '提示', { type: 'warning' }); + loading = this.$loading({ + lock: true, + text: '审批处理中,请稍候…', + background: 'rgba(255, 255, 255, 0.6)', + }); + const res = await api.approve({ id: row.id }); + // data=false 表示审批已通过但自动同步金蝶失败,msg 带具体原因 + if (res.data.data === false) { + this.$message.warning(res.data.msg); + } else { + this.$message.success(res.data.msg || '审批通过'); + } + this.loadTable(); + } finally { + loading?.close(); + this.approveLoading = false; } - this.loadTable(); }, async handleReturn(row) { const { value } = await this.$prompt('请输入驳回原因', '审批驳回'); diff --git a/src/views/payment/payment-application-form.vue b/src/views/payment/payment-application-form.vue index 598d1e8..3e91449 100644 --- a/src/views/payment/payment-application-form.vue +++ b/src/views/payment/payment-application-form.vue @@ -219,6 +219,11 @@ show-word-limit /> + +
+
风险客商提示:
+
{{ payeeRiskText }}
+
item.accountName || item.accountHolderName || item.bankAccount) .map(item => ({ @@ -3312,6 +3351,15 @@ export default { this.clearMessages(); const saved = await this.saveDraft({ showSuccess: false, closePage: false }); if (!saved) return; + // 高风险/黑名单收款方必须上传特批附件(与后端提交强校验同口径) + if ( + this.missingAttachmentMaterials.some( + item => item.key === ATTACHMENT_MATERIALS.specialApproval.key + ) + ) { + this.$message.warning('收款方为高风险/黑名单客商,必须上传「特批附件」类型附件后再提交审批'); + return; + } const previousApprovalStatus = this.form.approvalStatus || ''; await api.submit({ id: this.form.id }); await submitMkApprovalFlow({ @@ -3344,6 +3392,26 @@ export default { .payment-form-page__form { padding-bottom: 72px; } + +// 风险客商提示条(基本信息备注后,同合同新增页):橙黄背景 + 红色文字 +.payment-form-page__risk-tip { + margin: 0 0 18px; + padding: 10px 16px; + background: #faecd8; + border-radius: 4px; + + .payment-form-page__risk-tip-title { + color: #f56c6c; + font-weight: 600; + line-height: 22px; + } + + .payment-form-page__risk-tip-content { + color: #f56c6c; + line-height: 22px; + } +} + .payment-form-page__settlement-nos { display: flex; align-items: center; diff --git a/src/views/payment/payment-application.vue b/src/views/payment/payment-application.vue index baf454c..45b8fe6 100644 --- a/src/views/payment/payment-application.vue +++ b/src/views/payment/payment-application.vue @@ -331,6 +331,11 @@ export default { // 防止重复点击:请求进行中忽略后续点击 if (this.approveLoading) return; this.approveLoading = true; + const loading = this.$loading({ + lock: true, + text: '审批处理中,请稍候…', + background: 'rgba(255, 255, 255, 0.6)', + }); try { const res = await api.approve({ id: row.id }); // data=false 表示审批已通过但自动同步金蝶失败,msg 带具体原因 @@ -341,6 +346,7 @@ export default { } this.refreshChange(); } finally { + loading.close(); this.approveLoading = false; } }, diff --git a/src/views/settlement/formal-settlement.vue b/src/views/settlement/formal-settlement.vue index 742cdb7..3c7926c 100644 --- a/src/views/settlement/formal-settlement.vue +++ b/src/views/settlement/formal-settlement.vue @@ -140,6 +140,7 @@ export default { rows: [], selection: [], loading: false, + approveLoading: false, syncLoading: false, page: { currentPage: 1, pageSize: 10, pageSizes: [10, 20, 50, 100], total: 0 }, editor: { visible: false, id: null, readonly: false }, @@ -371,19 +372,33 @@ export default { }); }, async handleApprove(row) { - await this.$confirm( - `审核通过后${this.billLabel}生效,金额只能通过结算调整单处理,确认通过?`, - '提示', - { type: 'warning' } - ); - const res = await api.approve({ id: row.id }); - // data=false 表示审批已通过但自动同步金蝶失败,msg 带具体原因 - if (res.data.data === false) { - this.$message.warning(res.data.msg); - } else { - this.$message.success(res.data.msg || '审批通过'); + // 防止重复点击:请求进行中忽略后续点击,避免重复审批和重复提示 + if (this.approveLoading) return; + this.approveLoading = true; + let loading = null; + try { + await this.$confirm( + `审核通过后${this.billLabel}生效,金额只能通过结算调整单处理,确认通过?`, + '提示', + { type: 'warning' } + ); + loading = this.$loading({ + lock: true, + text: '审批处理中,请稍候…', + background: 'rgba(255, 255, 255, 0.6)', + }); + const res = await api.approve({ id: row.id }); + // data=false 表示审批已通过但自动同步金蝶失败,msg 带具体原因 + if (res.data.data === false) { + this.$message.warning(res.data.msg); + } else { + this.$message.success(res.data.msg || '审批通过'); + } + this.onLoad(this.page); + } finally { + loading?.close(); + this.approveLoading = false; } - this.onLoad(this.page); }, async handleReturn(row) { const { value } = await this.$prompt('请输入驳回原因', '审批驳回', { diff --git a/src/views/settlement/pre-settlement.vue b/src/views/settlement/pre-settlement.vue index dda0871..8648db4 100644 --- a/src/views/settlement/pre-settlement.vue +++ b/src/views/settlement/pre-settlement.vue @@ -334,6 +334,7 @@ export default { ArrowDown, ArrowUp, loading: false, + approveLoading: false, query: emptyQuery(), searchFields: preSettlementSearchFields, searchExpanded: false, @@ -691,10 +692,24 @@ export default { }); }, async handleApprove(row) { - await this.$confirm('确认审批通过该预付结算单?', '提示', { type: 'warning' }); - await approve({ id: row.id }); - this.$message.success('审批通过'); - this.loadTable(); + // 防止重复点击:请求进行中忽略后续点击,避免重复审批和重复提示 + if (this.approveLoading) return; + this.approveLoading = true; + let loading = null; + try { + await this.$confirm('确认审批通过该预付结算单?', '提示', { type: 'warning' }); + loading = this.$loading({ + lock: true, + text: '审批处理中,请稍候…', + background: 'rgba(255, 255, 255, 0.6)', + }); + await approve({ id: row.id }); + this.$message.success('审批通过'); + this.loadTable(); + } finally { + loading?.close(); + this.approveLoading = false; + } }, async handleReturn(row) { const { value } = await this.$prompt('请输入驳回原因', '审批驳回', { diff --git a/src/views/vehicle/customer-archive.vue b/src/views/vehicle/customer-archive.vue index 68a4d1d..c08062d 100644 --- a/src/views/vehicle/customer-archive.vue +++ b/src/views/vehicle/customer-archive.vue @@ -47,6 +47,29 @@ {{ row.customerCode }} +