From ef1a43f947e7d25f1ba8dd513d897b790d4fa79c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com> Date: Fri, 9 Oct 2026 19:46:56 +0800 Subject: [PATCH 01/27] =?UTF-8?q?feat:=20=E7=AB=8B=E9=A1=B9=E4=B8=8E?= =?UTF-8?q?=E5=90=88=E5=90=8CHCS=E5=89=8D=E7=AB=AF=E6=8E=A5=E5=85=A5?= =?UTF-8?q?=E2=80=94=E2=80=94=E5=88=97=E8=A1=A8=E5=90=88=E8=A7=84=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E5=88=97/=E6=8E=A8=E9=80=81=E5=90=88=E8=A7=84/?= =?UTF-8?q?=E5=90=88=E8=A7=84=E5=AF=B9=E8=B4=A6=E5=BC=B9=E7=AA=97=E3=80=81?= =?UTF-8?q?=E5=90=88=E5=90=8C=E8=AF=A6=E6=83=85=E5=90=88=E8=A7=84=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E9=9D=A2=E6=9D=BF=E4=B8=8E=E7=89=B9=E6=89=B9=E5=8F=91?= =?UTF-8?q?=E8=B5=B7=E3=80=81=E7=94=B2=E4=B9=99=E6=96=B9=E5=AE=A2=E5=95=86?= =?UTF-8?q?=E9=A3=8E=E9=99=A9=E6=8F=90=E7=A4=BA=E3=80=81=E5=90=88=E8=A7=84?= =?UTF-8?q?=E4=B8=BB=E6=95=B0=E6=8D=AE=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/business/hcs.js | 93 ++++++ src/option/business/common.js | 8 +- src/option/business/contract-manage.js | 14 + src/option/business/project-apply.js | 14 + .../components/hcs-contract-panel.vue | 284 ++++++++++++++++++ .../business/components/hcs-recon-dialog.vue | 147 +++++++++ src/views/business/contract-manage.vue | 153 +++++++++- src/views/business/hcs-master-data.vue | 100 ++++++ src/views/business/project-apply.vue | 83 +++++ 9 files changed, 893 insertions(+), 3 deletions(-) create mode 100644 src/api/business/hcs.js create mode 100644 src/views/business/components/hcs-contract-panel.vue create mode 100644 src/views/business/components/hcs-recon-dialog.vue create mode 100644 src/views/business/hcs-master-data.vue diff --git a/src/api/business/hcs.js b/src/api/business/hcs.js new file mode 100644 index 0000000..4dac778 --- /dev/null +++ b/src/api/business/hcs.js @@ -0,0 +1,93 @@ +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 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, + }); + +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 reconProject = projectCode => + request({ + url: `${baseUrl}/recon/project`, + method: 'get', + params: { projectCode }, + timeout: 30000, + }); + +export const reconContract = contractNo => + request({ + url: `${baseUrl}/recon/contract`, + method: 'get', + params: { contractNo }, + timeout: 30000, + }); + +export const reconContractDetail = params => + request({ + url: `${baseUrl}/recon/contract/detail`, + method: 'get', + params, + timeout: 30000, + }); + +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/option/business/common.js b/src/option/business/common.js index 1f6feb2..490c9b4 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: '水路运输' }, @@ -142,6 +142,12 @@ export const contractArchiveStatusOptions = [ { label: '已归档', value: '已归档' }, ]; +export const hcsSyncStatusOptions = [ + { label: '待推送', value: 'unsynced' }, + { label: '已推送', value: 'synced' }, + { label: '推送失败', value: 'failed' }, +]; + export const nodeOptions = [ { label: '接单', value: '接单' }, { label: '到场', value: '到场' }, diff --git a/src/option/business/contract-manage.js b/src/option/business/contract-manage.js index 4a72be5..8789e72 100644 --- a/src/option/business/contract-manage.js +++ b/src/option/business/contract-manage.js @@ -6,6 +6,7 @@ import { contractStageOptions, createCrudOption, effectiveTypeOptions, + hcsSyncStatusOptions, selectRule, signTypeOptions, textRule, @@ -589,6 +590,19 @@ 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: '当前节点', prop: 'currentNode', diff --git a/src/option/business/project-apply.js b/src/option/business/project-apply.js index 721413a..c3a23e5 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, @@ -405,6 +406,19 @@ 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: '生效类型', prop: 'effectiveType', 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..e0e7f47 --- /dev/null +++ b/src/views/business/components/hcs-contract-panel.vue @@ -0,0 +1,284 @@ + + + + + diff --git a/src/views/business/components/hcs-recon-dialog.vue b/src/views/business/components/hcs-recon-dialog.vue new file mode 100644 index 0000000..6e80405 --- /dev/null +++ b/src/views/business/components/hcs-recon-dialog.vue @@ -0,0 +1,147 @@ + + + + + diff --git a/src/views/business/contract-manage.vue b/src/views/business/contract-manage.vue index c3b48e7..33e1f36 100644 --- a/src/views/business/contract-manage.vue +++ b/src/views/business/contract-manage.vue @@ -72,6 +72,13 @@ + + + + + + + + + { + const risk = this.partyRisk[prop]; + if (!risk) return; + const name = this.form[prop] || ''; + const label = prop === 'partyA' ? '甲方' : '乙方'; + if (risk.blacklist) parts.push(`${label}[${name}]为黑名单客商`); + else if (['高', '高风险', 'high'].includes(String(risk.riskLevel).trim())) + parts.push(`${label}[${name}]为高风险客商`); + }); + if (!parts.length) return ''; + return `${parts.join(';')},按集团规定不可直接签约,审批通过后需在合同详情发起特批`; + }, detailLegalSealText() { const value = this.detailRow.legalSealFlag; if (value === undefined || value === null || value === '') return '-'; @@ -1573,6 +1633,55 @@ 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' + ); + }, + canHcsRecon(row) { + return ['approved', 'change_approved'].includes(row.approvalStatus); + }, + handleHcsRepush(row) { + if (this.hcsRepushingId) return; + this.$confirm('确定推送该数据到合同合规系统?', '提示', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning', + }).then(() => { + 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(error => { + this.$message.error(error?.response?.data?.msg || '推送合规失败'); + }) + .finally(() => { + this.hcsRepushingId = null; + }); + }); + }, + openHcsRecon(row) { + if (!row.contractNo) { + this.$message.warning('该合同无合同编号,无法对账'); + return; + } + this.hcsReconLoader = () => reconContract(row.contractNo); + this.hcsReconVisible = true; + }, canEdit(row) { return ( this.hasPermission(`${this.config.permission}_edit`) && @@ -2113,8 +2222,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); @@ -2127,6 +2249,29 @@ export default { 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]: { + riskLevel: risk.fdRiskLevel || '', + blacklist: String(risk.fdBlackStatus || '').toLowerCase() === 'yes', + }, + }; + }) + .catch(() => { + this.partyRisk = { ...this.partyRisk, [prop]: null }; + }); + }, loadOrganizationOptions() { if (this.organizationLoading) return; this.organizationLoading = true; @@ -2961,6 +3106,10 @@ export default { vertical-align: bottom; } +.contract-manage-form__risk-alert { + width: 100%; +} + .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 0b4dc50..6a55654 100644 --- a/src/views/business/project-apply.vue +++ b/src/views/business/project-apply.vue @@ -58,6 +58,17 @@ {{ displayStatus(row, 'approvalStatus') }} + + + + import('@/views/vehicle/customer-archive.vue')), + HcsReconDialog: defineAsyncComponent(() => import('./components/hcs-recon-dialog.vue')), ChangeRecordDetailDialog, ElImageViewer, OpenFileViewer, @@ -1107,6 +1136,10 @@ export default { { label: '高风险', value: 'high', statKey: 'high' }, { label: '中风险', value: 'medium', statKey: 'medium' }, ], + // 合规(HCS)推送/对账状态 + hcsRepushingId: null, + hcsReconVisible: false, + hcsReconLoader: null, page: { pageSize: 10, pageSizes: [10, 20, 50, 100], @@ -1578,6 +1611,56 @@ 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' + ); + }, + canHcsRecon(row) { + return ['approved', 'change_approved'].includes(row.approvalStatus); + }, + handleHcsRepush(row) { + if (this.hcsRepushingId) return; + this.$confirm('确定推送该数据到合同合规系统?', '提示', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning', + }).then(() => { + 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(error => { + this.$message.error(error?.response?.data?.msg || '推送合规失败'); + }) + .finally(() => { + this.hcsRepushingId = null; + }); + }); + }, + openHcsRecon(row) { + if (!row.projectCode) { + this.$message.warning('该项目无项目编号,无法对账'); + return; + } + this.hcsReconLoader = () => reconProject(row.projectCode); + this.hcsReconVisible = true; + }, canOperation(operation, row) { const permission = operation.permission || `${this.config.permission}_${operation.action}`; if (!this.hasPermission(permission) || this.readonlyScope || row.readonly) return false; From 8640238cbef58a39b7f1393d733328f5f9c80ee0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com> Date: Sun, 11 Oct 2026 16:27:09 +0800 Subject: [PATCH 02/27] =?UTF-8?q?:fire:=20=E7=A7=BB=E9=99=A4=E5=90=88?= =?UTF-8?q?=E8=A7=84=E5=AF=B9=E8=B4=A6=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/business/hcs.js | 24 --- .../components/hcs-contract-panel.vue | 15 +- .../business/components/hcs-recon-dialog.vue | 147 ------------------ src/views/business/contract-manage.vue | 24 +-- src/views/business/project-apply.vue | 27 +--- 5 files changed, 4 insertions(+), 233 deletions(-) delete mode 100644 src/views/business/components/hcs-recon-dialog.vue diff --git a/src/api/business/hcs.js b/src/api/business/hcs.js index 4dac778..7039307 100644 --- a/src/api/business/hcs.js +++ b/src/api/business/hcs.js @@ -48,30 +48,6 @@ export const downloadAttachment = (fdId, name) => timeout: 120000, }); -export const reconProject = projectCode => - request({ - url: `${baseUrl}/recon/project`, - method: 'get', - params: { projectCode }, - timeout: 30000, - }); - -export const reconContract = contractNo => - request({ - url: `${baseUrl}/recon/contract`, - method: 'get', - params: { contractNo }, - timeout: 30000, - }); - -export const reconContractDetail = params => - request({ - url: `${baseUrl}/recon/contract/detail`, - method: 'get', - params, - timeout: 30000, - }); - export const applyContractSpec = data => request({ url: `${baseUrl}/contract/spec-apply`, diff --git a/src/views/business/components/hcs-contract-panel.vue b/src/views/business/components/hcs-contract-panel.vue index e0e7f47..2a3b1f3 100644 --- a/src/views/business/components/hcs-contract-panel.vue +++ b/src/views/business/components/hcs-contract-panel.vue @@ -20,7 +20,6 @@ > 确认特批结果 - 合规对账 @@ -103,12 +102,6 @@ 取消 - - @@ -117,10 +110,8 @@ import { UploadFilled } from '@element-plus/icons-vue'; import { applyContractSpec, confirmContractSpec, - reconContract, uploadAttachment, } from '@/api/business/hcs'; -import HcsReconDialog from './hcs-recon-dialog.vue'; const SYNC_STATUS_OPTIONS = [ { label: '待推送', value: 'unsynced', type: 'info' }, @@ -137,7 +128,7 @@ const SPEC_STATUS_OPTIONS = [ export default { name: 'HcsContractPanel', - components: { UploadFilled, HcsReconDialog }, + components: { UploadFilled }, props: { // 合同详情行(须含 id/hcsSyncStatus/hcsFdId/hcsRiskLevel/hcsBlackStatus/hcsSpecStatus 等) contract: { type: Object, required: true }, @@ -148,7 +139,6 @@ export default { specDialogVisible: false, specApplying: false, materials: [], - reconVisible: false, }; }, computed: { @@ -176,9 +166,6 @@ export default { canApplySpec() { return ['need', 'rejected'].includes(this.contract.hcsSpecStatus); }, - reconLoader() { - return () => reconContract(this.contract.contractNo); - }, }, methods: { openSpecDialog() { diff --git a/src/views/business/components/hcs-recon-dialog.vue b/src/views/business/components/hcs-recon-dialog.vue deleted file mode 100644 index 6e80405..0000000 --- a/src/views/business/components/hcs-recon-dialog.vue +++ /dev/null @@ -1,147 +0,0 @@ - - - - - diff --git a/src/views/business/contract-manage.vue b/src/views/business/contract-manage.vue index 33e1f36..a81b6f8 100644 --- a/src/views/business/contract-manage.vue +++ b/src/views/business/contract-manage.vue @@ -102,17 +102,10 @@ {{ hcsRepushingId === row.id ? '推送中…' : '推送合规' }} - 合规对账 删除 - - reconContract(row.contractNo); - this.hcsReconVisible = true; - }, canEdit(row) { return ( this.hasPermission(`${this.config.permission}_edit`) && diff --git a/src/views/business/project-apply.vue b/src/views/business/project-apply.vue index 6a55654..e923e9d 100644 --- a/src/views/business/project-apply.vue +++ b/src/views/business/project-apply.vue @@ -146,19 +146,10 @@ > {{ hcsRepushingId === row.id ? '推送中…' : '推送合规' }} - - 合规对账 - 删除 - - import('@/views/vehicle/customer-archive.vue')), - HcsReconDialog: defineAsyncComponent(() => import('./components/hcs-recon-dialog.vue')), ChangeRecordDetailDialog, ElImageViewer, OpenFileViewer, @@ -1136,10 +1126,8 @@ export default { { label: '高风险', value: 'high', statKey: 'high' }, { label: '中风险', value: 'medium', statKey: 'medium' }, ], - // 合规(HCS)推送/对账状态 + // 合规(HCS)推送状态 hcsRepushingId: null, - hcsReconVisible: false, - hcsReconLoader: null, page: { pageSize: 10, pageSizes: [10, 20, 50, 100], @@ -1625,9 +1613,6 @@ export default { row.hcsSyncStatus !== 'synced' ); }, - canHcsRecon(row) { - return ['approved', 'change_approved'].includes(row.approvalStatus); - }, handleHcsRepush(row) { if (this.hcsRepushingId) return; this.$confirm('确定推送该数据到合同合规系统?', '提示', { @@ -1653,14 +1638,6 @@ export default { }); }); }, - openHcsRecon(row) { - if (!row.projectCode) { - this.$message.warning('该项目无项目编号,无法对账'); - return; - } - this.hcsReconLoader = () => reconProject(row.projectCode); - this.hcsReconVisible = true; - }, canOperation(operation, row) { const permission = operation.permission || `${this.config.permission}_${operation.action}`; if (!this.hasPermission(permission) || this.readonlyScope || row.readonly) return false; From 9e5f53702fd0b6b52ad8b6aecb81d65bb165fd75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com> Date: Sun, 11 Oct 2026 18:08:27 +0800 Subject: [PATCH 03/27] =?UTF-8?q?feat:=20=E5=90=88=E5=90=8C=E5=AE=A2?= =?UTF-8?q?=E5=95=86=E9=A3=8E=E9=99=A9=E7=AE=A1=E6=8E=A7=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E6=8E=A5=E5=85=A5=E2=80=94=E2=80=94=E9=A3=8E=E9=99=A9=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E6=9D=A1=E4=B8=8E=E7=89=B9=E6=89=B9=E8=AF=B4=E6=98=8E?= =?UTF-8?q?=E9=99=84=E4=BB=B6=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 合同表单甲乙方选择后风险提示移至基本信息备注后(橙黄提示条+红色风险客商提示文案,按设计稿),改用后端 risky 统一判定(修复原 fdRiskLevel 等级值匹配不中的问题),展示后端 riskTip 完整风险明细 2. 合同详情基本信息备注后新增风险客商提示行(按推送留痕 hcs_risk_level/hcs_black_status 判定) 3. 附件类型兜底选项增加特批说明(高风险/黑名单客商合同必传,与后端 fileType 校验对齐;正式选项需在业务字典 contract_attachment_type 配置) --- .../contract-attachment-section.vue | 2 + src/views/business/contract-manage.vue | 70 ++++++++++++++----- 2 files changed, 53 insertions(+), 19 deletions(-) diff --git a/src/views/business/components/contract-attachment-section.vue b/src/views/business/components/contract-attachment-section.vue index 10119b9..23519e3 100644 --- a/src/views/business/components/contract-attachment-section.vue +++ b/src/views/business/components/contract-attachment-section.vue @@ -176,6 +176,8 @@ const CONTRACT_ATTACHMENT_TYPE_OTHER = '其他文件'; export const defaultContractAttachmentTypeOptions = [ { label: '合同文件', value: '合同文件' }, { label: '双章归档文件', value: '双章归档文件' }, + // 高风险/黑名单客商合同必传,后端按 fileType=特批说明 校验与转传特批(与后端 HcsSyncConstant.ATTACHMENT_TYPE_SPEC_EXPLANATION 对齐) + { label: '特批说明', value: '特批说明' }, { label: '其他文件', value: CONTRACT_ATTACHMENT_TYPE_OTHER }, ]; /** @deprecated 兼容旧引用,实际选项由业务字典 contract_attachment_type 动态加载 */ diff --git a/src/views/business/contract-manage.vue b/src/views/business/contract-manage.vue index a81b6f8..4d20756 100644 --- a/src/views/business/contract-manage.vue +++ b/src/views/business/contract-manage.vue @@ -184,6 +184,9 @@ {{ detailValue('remark') }} + + {{ detailRiskTipText }} + @@ -480,14 +483,6 @@ /> - - - + +
+
风险客商提示:
+
{{ partyRiskText }}
+
{ const risk = this.partyRisk[prop]; - if (!risk) return; - const name = this.form[prop] || ''; - const label = prop === 'partyA' ? '甲方' : '乙方'; - if (risk.blacklist) parts.push(`${label}[${name}]为黑名单客商`); - else if (['高', '高风险', 'high'].includes(String(risk.riskLevel).trim())) - parts.push(`${label}[${name}]为高风险客商`); + if (!risk?.risky) return; + // riskTip 由后端按风险项拼装(【公司名称】:黑名单;涉诉;被执行人等),缺失时兜底通用提示 + parts.push( + risk.riskTip || + `${prop === 'partyA' ? '甲方' : '乙方'}[${this.form[prop] || ''}]为高风险/黑名单客商` + ); }); if (!parts.length) return ''; - return `${parts.join(';')},按集团规定不可直接签约,审批通过后需在合同详情发起特批`; + 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; @@ -2227,7 +2238,7 @@ export default { if (requestId === this.contractPartyRequestId) this.contractPartyLoading = false; }); }, - // C101-1 客商风险查询(展示用,强校验在后端推送/付款链路):失败静默不阻塞填写 + // C101-1 客商风险查询(展示用,强校验在后端提交/推送链路):失败静默不阻塞填写 queryPartyRisk(prop) { const name = this.form[prop]; const code = name ? this.contractPartyCodeMap[name] : null; @@ -2241,8 +2252,10 @@ export default { this.partyRisk = { ...this.partyRisk, [prop]: { - riskLevel: risk.fdRiskLevel || '', - blacklist: String(risk.fdBlackStatus || '').toLowerCase() === 'yes', + customerKind: risk.customerKind || '', + // risky 由后端统一判定:外部客商高风险(等级=2)或黑名单;内部组织客商默认无风险恒为 false + risky: risk.risky === true, + riskTip: risk.riskTip || '', }, }; }) @@ -3088,6 +3101,25 @@ export default { 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; From b7af496a43d2dea2f34925985123b60b52213248 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com> Date: Wed, 14 Oct 2026 10:03:18 +0800 Subject: [PATCH 04/27] =?UTF-8?q?feat:=20=E4=BB=98=E6=AC=BE=E7=94=B3?= =?UTF-8?q?=E8=AF=B7=E6=94=B6=E6=AC=BE=E6=96=B9=E9=A3=8E=E9=99=A9=E5=AE=A2?= =?UTF-8?q?=E5=95=86=E6=8F=90=E7=A4=BA=E6=9D=A1=E4=B8=8E=E7=89=B9=E6=89=B9?= =?UTF-8?q?=E9=99=84=E4=BB=B6=E6=8F=90=E4=BA=A4=E6=A0=A1=E9=AA=8C=EF=BC=88?= =?UTF-8?q?C101-1=20=E9=A3=8E=E9=99=A9=E6=9F=A5=E8=AF=A2=E8=81=94=E5=8A=A8?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../payment/payment-application-form.vue | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/src/views/payment/payment-application-form.vue b/src/views/payment/payment-application-form.vue index 6f00473..e19a1a3 100644 --- a/src/views/payment/payment-application-form.vue +++ b/src/views/payment/payment-application-form.vue @@ -221,6 +221,11 @@ show-word-limit /> + +
+
风险客商提示:
+
{{ payeeRiskText }}
+
item.accountName || item.accountHolderName || item.bankAccount) .map(item => ({ @@ -2672,6 +2711,15 @@ export default { // 提交前静默保存,避免同时弹出「保存成功」与材料缺失 toast const saved = await this.saveDraft({ showSuccess: 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({ @@ -2700,6 +2748,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; From 411fdd24257ce8ffa567de44c65d0a4e79272b3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com> Date: Thu, 15 Oct 2026 19:42:02 +0800 Subject: [PATCH 05/27] =?UTF-8?q?:feat:=20=E9=A1=B9=E7=9B=AE=E7=AB=8B?= =?UTF-8?q?=E9=A1=B9=E4=B8=8E=E5=90=88=E5=90=8C=E5=88=97=E8=A1=A8=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E6=89=B9=E9=87=8F=E6=8E=A8=E9=80=81=E5=90=88=E8=A7=84?= =?UTF-8?q?=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/business/hcs.js | 9 +++++ src/views/business/contract-manage.vue | 46 ++++++++++++++++++++++++- src/views/business/project-apply.vue | 47 +++++++++++++++++++++++++- 3 files changed, 100 insertions(+), 2 deletions(-) diff --git a/src/api/business/hcs.js b/src/api/business/hcs.js index 7039307..8ccfd1b 100644 --- a/src/api/business/hcs.js +++ b/src/api/business/hcs.js @@ -10,6 +10,15 @@ export const repush = (bizType, id) => 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`, diff --git a/src/views/business/contract-manage.vue b/src/views/business/contract-manage.vue index e6b5df1..811b738 100644 --- a/src/views/business/contract-manage.vue +++ b/src/views/business/contract-manage.vue @@ -32,6 +32,14 @@ > 批量导出 + + 批量推送合规 + + + + + + + + + + + 所在部门及下级 + 全部组织 + 本人创建 + 自定义组织 + + + + + + + + @@ -196,37 +220,44 @@ import { add, getList, getRole, - getRoleAlias, getRoleTreeById, grant, grantTree, remove, update, } from '@/api/system/role'; +import { getRoleMenuDataScopeTree, submitRoleMenuDataScope } from '@/api/system/roleDataScope'; +import { getDeptTree } from '@/api/system/dept'; import { mapGetters } from 'vuex'; import website from '@/config/website'; -import { validatenull } from '@/utils/validate'; export default { data() { return { form: {}, box: false, + dataScopeSettingBox: false, props: { label: 'title', value: 'key', }, menuGrantList: [], - dataScopeGrantList: [], apiScopeGrantList: [], apiGrantList: [], menuTreeObj: [], - dataScopeTreeObj: [], apiScopeTreeObj: [], menuLinked: false, - dataScopeLinked: false, apiScopeLinked: false, currentRoleIds: [], + currentRoleName: '', + roleDataScopeMenuTree: [], + roleDataScopeMap: {}, + deptTree: [], + dataScopeSetting: { + roleName: '', + scopeType: 2, + menuIds: [], + }, selectionList: [], query: {}, loading: true, @@ -309,32 +340,19 @@ export default { ], }, { - label: '角色别名', - labelTip: '用于权限校验与标注的属性,可手动录入也可点击现有别名快速录入', + label: '角色标识', + labelTip: '用于权限校验与标注的属性', prop: 'roleAlias', search: true, span: 24, rules: [ { required: true, - message: '请输入角色别名', + message: '请输入角色标识', trigger: 'blur', }, ], }, - { - label: '现有别名', - labelTip: '当前系统已存在的角色别名,点击可快速录入', - prop: 'currentAlias', - type: 'radio', - dicData: [], - props: { - label: 'roleName', - value: 'roleAlias', - }, - hide: true, - span: 24, - }, { label: '角色排序', prop: 'sort', @@ -378,14 +396,6 @@ export default { return ids; }, }, - watch: { - 'form.currentAlias'() { - const alias = this.form.currentAlias; - if (!validatenull(alias)) { - this.form.roleAlias = alias; - } - }, - }, methods: { initData(roleId) { getRoleTreeById(roleId).then(res => { @@ -452,9 +462,9 @@ export default { }, submit() { const menuList = this.$refs.treeMenu.getCheckedKeys(); - const dataScopeList = this.$refs.treeDataScope.getCheckedKeys(); const apiScopeList = this.$refs.treeApiScope.getCheckedKeys(); - grant(this.currentRoleIds, menuList, dataScopeList, apiScopeList).then(() => { + // 数据权限已改为角色菜单配置;传 null 跳过原生 dataScope 更新,避免误清历史配置 + grant(this.currentRoleIds, menuList, null, apiScopeList).then(() => { this.box = false; this.$message({ type: 'success', @@ -465,6 +475,84 @@ export default { this.onLoad(this.page); }); }, + getMenuScopeLabel(menuId) { + const scope = this.roleDataScopeMap[String(menuId)]; + return scope ? scope.scopeTypeName : ''; + }, + loadRoleDataScopeTree(roleId) { + return getRoleMenuDataScopeTree(roleId).then(res => { + const data = res.data.data || {}; + this.currentRoleName = data.roleName || ''; + this.roleDataScopeMenuTree = data.menuTree || []; + const scopeMap = {}; + (data.scopes || []).forEach(item => { + scopeMap[String(item.menuId)] = item; + }); + this.roleDataScopeMap = scopeMap; + }); + }, + openDataScopeSetting() { + const tree = this.$refs.treeRoleDataScope; + if (!tree) { + return; + } + const menuIds = tree.getCheckedKeys(); + if (!menuIds || menuIds.length === 0) { + this.$message.warning('请先勾选要设置的菜单'); + return; + } + this.dataScopeSetting = { + roleName: this.currentRoleName, + scopeType: 2, + menuIds, + }; + const loadDept = + this.deptTree && this.deptTree.length + ? Promise.resolve() + : getDeptTree(this.userInfo.tenantId).then(res => { + this.deptTree = res.data.data || []; + }); + loadDept.then(() => { + this.dataScopeSettingBox = true; + this.$nextTick(() => { + if (this.$refs.treeCustomDept) { + this.$refs.treeCustomDept.setCheckedKeys([]); + } + }); + }); + }, + submitDataScopeSetting() { + const roleId = this.currentRoleIds && this.currentRoleIds[0]; + if (!roleId) { + this.$message.warning('角色信息异常,请重新打开权限配置'); + return; + } + const scopeType = Number(this.dataScopeSetting.scopeType || 2); + let deptIds = []; + if (scopeType === 4) { + deptIds = this.$refs.treeCustomDept ? this.$refs.treeCustomDept.getCheckedKeys() : []; + if (!deptIds.length) { + this.$message.warning('自定义组织时请勾选组织'); + return; + } + } + submitRoleMenuDataScope({ + roleId, + menuIds: this.dataScopeSetting.menuIds, + scopeType, + deptIds, + }).then(() => { + this.$message.success('数据权限设置成功'); + this.dataScopeSettingBox = false; + this.loadRoleDataScopeTree(roleId).then(() => { + this.$nextTick(() => { + if (this.$refs.treeRoleDataScope) { + this.$refs.treeRoleDataScope.setCheckedKeys([]); + } + }); + }); + }); + }, rowSave(row, done, loading) { add(this.withTenantId(row)).then( () => { @@ -547,10 +635,6 @@ export default { this.form.parentId = ''; } } - getRoleAlias().then(res => { - const column = this.findColumn(this.option.column, 'currentAlias'); - column.dicData = res.data.data; - }); done(); }, handleRole() { @@ -563,58 +647,52 @@ export default { return; } this.menuTreeObj = []; - this.dataScopeTreeObj = []; this.apiScopeTreeObj = []; - // 保存当前要配置权限的角色ID this.currentRoleIds = this.idsArray; - grantTree().then(res => { - this.menuGrantList = res.data.data.menu; - this.dataScopeGrantList = res.data.data.dataScope; - this.apiScopeGrantList = res.data.data.apiScope; - getRole(this.ids).then(res => { - this.menuTreeObj = res.data.data.menu; - this.dataScopeTreeObj = res.data.data.dataScope; - this.apiScopeTreeObj = res.data.data.apiScope; - this.box = true; - this.$nextTick(() => { - this.applyCheckedKeys('treeMenu', this.menuTreeObj); - this.applyCheckedKeys('treeDataScope', this.dataScopeTreeObj); - this.applyCheckedKeys('treeApiScope', this.apiScopeTreeObj); - }); + this.currentRoleName = (this.selectionList[0] && this.selectionList[0].roleName) || ''; + const roleId = this.currentRoleIds[0]; + Promise.all([grantTree(), getRole(this.ids), this.loadRoleDataScopeTree(roleId)]).then(results => { + const grantRes = results[0]; + const roleRes = results[1]; + this.menuGrantList = grantRes.data.data.menu; + this.apiScopeGrantList = grantRes.data.data.apiScope; + this.menuTreeObj = roleRes.data.data.menu; + this.apiScopeTreeObj = roleRes.data.data.apiScope; + this.box = true; + this.$nextTick(() => { + this.applyCheckedKeys('treeMenu', this.menuTreeObj); + this.applyCheckedKeys('treeApiScope', this.apiScopeTreeObj); }); }); }, handleRowRole(row) { this.menuTreeObj = []; - this.dataScopeTreeObj = []; this.apiScopeTreeObj = []; - // 保存当前要配置权限的角色ID(单个行操作) this.currentRoleIds = [row.id]; - grantTree().then(res => { - this.menuGrantList = res.data.data.menu; - this.dataScopeGrantList = res.data.data.dataScope; - this.apiScopeGrantList = res.data.data.apiScope; - getRole(row.id).then(res => { - this.menuTreeObj = res.data.data.menu; - this.dataScopeTreeObj = res.data.data.dataScope; - this.apiScopeTreeObj = res.data.data.apiScope; - this.box = true; - this.$nextTick(() => { - this.applyCheckedKeys('treeMenu', this.menuTreeObj); - this.applyCheckedKeys('treeDataScope', this.dataScopeTreeObj); - this.applyCheckedKeys('treeApiScope', this.apiScopeTreeObj); - }); + this.currentRoleName = row.roleName || ''; + Promise.all([grantTree(), getRole(row.id), this.loadRoleDataScopeTree(row.id)]).then(results => { + const grantRes = results[0]; + const roleRes = results[1]; + this.menuGrantList = grantRes.data.data.menu; + this.apiScopeGrantList = grantRes.data.data.apiScope; + this.menuTreeObj = roleRes.data.data.menu; + this.apiScopeTreeObj = roleRes.data.data.apiScope; + this.box = true; + this.$nextTick(() => { + this.applyCheckedKeys('treeMenu', this.menuTreeObj); + this.applyCheckedKeys('treeApiScope', this.apiScopeTreeObj); }); }); }, handleDialogClose() { - // 对话框关闭时清空选择状态和权限树数据 this.menuTreeObj = []; - this.dataScopeTreeObj = []; this.apiScopeTreeObj = []; this.menuLinked = false; - this.dataScopeLinked = false; this.apiScopeLinked = false; + this.roleDataScopeMenuTree = []; + this.roleDataScopeMap = {}; + this.currentRoleName = ''; + this.dataScopeSettingBox = false; this.selectionClear(); }, handleDelete() { @@ -659,3 +737,15 @@ export default { }, }; + + diff --git a/src/views/system/user.vue b/src/views/system/user.vue index ae2f53a..20fcb3b 100644 --- a/src/views/system/user.vue +++ b/src/views/system/user.vue @@ -175,8 +175,6 @@ - 仅所属组织全部组织自定义 - 包含下级仅本级 - - - - -
包含新增的客商
- -