🔀 合并 master 回 dev,追平 HCS/BFI 与项目变更菜单
This commit is contained in:
@@ -36,6 +36,8 @@ export const approve = id =>
|
||||
url: `${baseUrl}/approve`,
|
||||
method: 'post',
|
||||
params: { id },
|
||||
// 审批通过后同步推送合规(附件转传可达 20s+),覆盖全局 10s 超时
|
||||
timeout: 120000,
|
||||
});
|
||||
|
||||
export const reject = id =>
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -43,6 +43,8 @@ export const approve = id =>
|
||||
url: `${baseUrl}/approve`,
|
||||
method: 'post',
|
||||
params: { id },
|
||||
// 审批通过后同步推送合规(远程多次调用),覆盖全局 10s 超时
|
||||
timeout: 60000,
|
||||
});
|
||||
|
||||
export const reject = id =>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { createCrudApi } from './common';
|
||||
import request from '@/axios';
|
||||
|
||||
const baseUrl = '/blade-transport/project-change';
|
||||
const api = createCrudApi(baseUrl);
|
||||
|
||||
export const getList = api.getList;
|
||||
export const getDetail = api.getDetail;
|
||||
export const submit = api.submit;
|
||||
export const remove = api.remove;
|
||||
|
||||
export const createFromProject = (projectId, changeType) =>
|
||||
request({
|
||||
url: `${baseUrl}/create-from-project`,
|
||||
method: 'post',
|
||||
params: { projectId, changeType },
|
||||
});
|
||||
|
||||
export const saveDraft = row =>
|
||||
request({
|
||||
url: `${baseUrl}/save-draft`,
|
||||
method: 'post',
|
||||
data: row,
|
||||
});
|
||||
|
||||
export const submitApproval = id =>
|
||||
request({
|
||||
url: `${baseUrl}/submit-approval`,
|
||||
method: 'post',
|
||||
params: { id },
|
||||
});
|
||||
|
||||
export const approve = id =>
|
||||
request({
|
||||
url: `${baseUrl}/approve`,
|
||||
method: 'post',
|
||||
params: { id },
|
||||
});
|
||||
|
||||
export const reject = id =>
|
||||
request({
|
||||
url: `${baseUrl}/reject`,
|
||||
method: 'post',
|
||||
params: { id },
|
||||
});
|
||||
|
||||
export const withdraw = id =>
|
||||
request({
|
||||
url: `${baseUrl}/withdraw`,
|
||||
method: 'post',
|
||||
params: { id },
|
||||
});
|
||||
@@ -1,6 +1,9 @@
|
||||
import request from '@/axios';
|
||||
import { ElLoading, ElMessage } from 'element-plus';
|
||||
|
||||
/** 审核按钮统一超时:审批通过后可能同步推送金蝶等耗时操作,覆盖全局 10s */
|
||||
export const APPROVE_TIMEOUT = 30000;
|
||||
|
||||
/**
|
||||
* 文件流返回
|
||||
* @param url 接口地址
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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 超时
|
||||
|
||||
@@ -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 超时
|
||||
|
||||
@@ -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 });
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import request from '@/axios';
|
||||
|
||||
export const getRoleMenuDataScopeTree = roleId => {
|
||||
return request({
|
||||
url: '/blade-system/role-menu-data-scope/tree',
|
||||
method: 'get',
|
||||
params: {
|
||||
roleId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const submitRoleMenuDataScope = data => {
|
||||
return request({
|
||||
url: '/blade-system/role-menu-data-scope/submit',
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
};
|
||||
@@ -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: '到场' },
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
import {
|
||||
approvalStatusOptions,
|
||||
createCrudOption,
|
||||
hcsSyncStatusOptions,
|
||||
projectTypeOptions,
|
||||
} from './common';
|
||||
|
||||
export const changeTypeOptions = [
|
||||
{ label: '项目变更', value: '项目变更' },
|
||||
{ label: '项目备案调整', value: '项目备案调整' },
|
||||
];
|
||||
|
||||
export const config = {
|
||||
title: '项目变更',
|
||||
permission: 'project_change',
|
||||
statusProp: 'approvalStatus',
|
||||
statusTextProp: 'approvalStatusName',
|
||||
deleteStatus: ['draft', 'withdrawn', 'rejected'],
|
||||
editStatus: ['draft', 'withdrawn', 'rejected'],
|
||||
viewStatus: ['approved', 'rejected', 'withdrawn'],
|
||||
operations: [
|
||||
{
|
||||
action: 'submitApproval',
|
||||
label: '提交',
|
||||
statusProp: 'approvalStatus',
|
||||
status: ['draft'],
|
||||
permission: 'project_change_submit',
|
||||
},
|
||||
{
|
||||
action: 'withdraw',
|
||||
label: '撤回',
|
||||
statusProp: 'approvalStatus',
|
||||
status: ['reviewing'],
|
||||
permission: 'project_change_withdraw',
|
||||
},
|
||||
{
|
||||
action: 'mkAudit',
|
||||
label: '流程',
|
||||
statusProp: 'approvalStatus',
|
||||
status: ['reviewing'],
|
||||
permission: 'project_change_approve',
|
||||
mkOnly: true,
|
||||
},
|
||||
{
|
||||
action: 'approve',
|
||||
label: '通过',
|
||||
type: 'success',
|
||||
statusProp: 'approvalStatus',
|
||||
status: ['reviewing'],
|
||||
permission: 'project_change_approve',
|
||||
localOnly: true,
|
||||
},
|
||||
{
|
||||
action: 'reject',
|
||||
label: '驳回',
|
||||
type: 'warning',
|
||||
statusProp: 'approvalStatus',
|
||||
status: ['reviewing'],
|
||||
permission: 'project_change_reject',
|
||||
localOnly: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const option = createCrudOption([
|
||||
{
|
||||
label: '项目编号',
|
||||
prop: 'projectCode',
|
||||
search: true,
|
||||
searchPlaceholder: '请输入',
|
||||
searchOrder: 70,
|
||||
minWidth: 160,
|
||||
},
|
||||
{
|
||||
label: '项目名称',
|
||||
prop: 'projectName',
|
||||
search: true,
|
||||
searchPlaceholder: '请输入',
|
||||
searchOrder: 60,
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
label: '变更类型',
|
||||
prop: 'changeType',
|
||||
type: 'select',
|
||||
search: true,
|
||||
searchPlaceholder: '请选择',
|
||||
searchOrder: 10,
|
||||
dicData: changeTypeOptions,
|
||||
minWidth: 130,
|
||||
},
|
||||
{
|
||||
label: '项目类型',
|
||||
prop: 'projectType',
|
||||
type: 'select',
|
||||
search: true,
|
||||
searchPlaceholder: '请选择',
|
||||
searchOrder: 20,
|
||||
dicData: projectTypeOptions,
|
||||
minWidth: 110,
|
||||
},
|
||||
{
|
||||
label: '客户名称',
|
||||
prop: 'customerNames',
|
||||
minWidth: 160,
|
||||
},
|
||||
{
|
||||
label: '下游承运商',
|
||||
prop: 'carrierNames',
|
||||
minWidth: 160,
|
||||
},
|
||||
{
|
||||
label: '承办部门',
|
||||
prop: 'undertakeDeptName',
|
||||
search: true,
|
||||
searchPlaceholder: '请输入',
|
||||
searchOrder: 50,
|
||||
minWidth: 140,
|
||||
},
|
||||
{
|
||||
label: '项目负责人',
|
||||
prop: 'principalUserName',
|
||||
search: true,
|
||||
searchPlaceholder: '请输入',
|
||||
searchOrder: 40,
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
label: '审批状态',
|
||||
prop: 'approvalStatus',
|
||||
type: 'select',
|
||||
search: true,
|
||||
searchPlaceholder: '请选择',
|
||||
searchOrder: 30,
|
||||
dicData: approvalStatusOptions,
|
||||
minWidth: 110,
|
||||
},
|
||||
{
|
||||
label: '当前节点',
|
||||
prop: 'currentNode',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
label: '当前处理人',
|
||||
prop: 'currentProcessor',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
label: '审核通过时间',
|
||||
prop: 'approvedTime',
|
||||
type: 'datetime',
|
||||
minWidth: 170,
|
||||
},
|
||||
{
|
||||
label: '同步状态',
|
||||
prop: 'syncStatus',
|
||||
type: 'select',
|
||||
dicData: hcsSyncStatusOptions,
|
||||
minWidth: 110,
|
||||
},
|
||||
]);
|
||||
@@ -315,29 +315,6 @@ export const userOption = safe => {
|
||||
{ label: '承运商', value: 2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '数据权限范围',
|
||||
prop: 'dataScopeRange',
|
||||
type: 'radio',
|
||||
span: 24,
|
||||
value: 1,
|
||||
dicData: [
|
||||
{ label: '仅所属组织', value: 1 },
|
||||
{ label: '全部组织', value: 2 },
|
||||
{ label: '自定义', value: 3 },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '数据层级范围',
|
||||
prop: 'dataLevelRange',
|
||||
type: 'radio',
|
||||
span: 24,
|
||||
value: 1,
|
||||
dicData: [
|
||||
{ label: '包含下级', value: 1 },
|
||||
{ label: '仅本级', value: 2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '备注',
|
||||
prop: 'remark',
|
||||
|
||||
@@ -95,6 +95,17 @@ export default [
|
||||
bizType: 'project-apply',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/business/project-change/public-view',
|
||||
name: '查看项目变更信息',
|
||||
component: () => import('@/views/business/project-apply-public-view.vue'),
|
||||
meta: {
|
||||
keepAlive: false,
|
||||
isTab: false,
|
||||
isAuth: false,
|
||||
bizType: 'project-change',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/business/contract-manage/public-view',
|
||||
name: '查看合同信息',
|
||||
|
||||
@@ -198,6 +198,18 @@ export default [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/business/project-change/form',
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: '项目变更表单',
|
||||
meta: { keepAlive: false, activeMenu: '/business/project-change' },
|
||||
component: loadView(() => import('@/views/business/project-apply.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/business/waybill-manage/form',
|
||||
component: Layout,
|
||||
|
||||
@@ -15,6 +15,12 @@ export const MK_BIZ = {
|
||||
rejected: ['rejected', 'change_rejected', 'withdrawn'],
|
||||
approved: ['approved', 'change_approved'],
|
||||
},
|
||||
'project-change': {
|
||||
dictName: '提交项目变更审核流',
|
||||
subjectPrefix: '项目变更审批',
|
||||
rejected: ['rejected', 'withdrawn'],
|
||||
approved: ['approved'],
|
||||
},
|
||||
'contract-manage': {
|
||||
dictName: '提交合同审核流',
|
||||
subjectPrefix: '合同审批',
|
||||
|
||||
+190
-100
@@ -50,7 +50,7 @@
|
||||
title="角色权限配置"
|
||||
append-to-body
|
||||
v-model="box"
|
||||
width="345px"
|
||||
width="960px"
|
||||
@close="handleDialogClose"
|
||||
>
|
||||
<el-tabs type="border-card">
|
||||
@@ -100,41 +100,26 @@
|
||||
align="middle"
|
||||
style="margin-bottom: 12px; background: #f5f7fa; padding: 6px 10px; border-radius: 4px"
|
||||
>
|
||||
<span style="display: inline-flex; align-items: center">
|
||||
<el-switch
|
||||
v-model="dataScopeLinked"
|
||||
active-text="节点联动"
|
||||
size="small"
|
||||
@change="handleLinkedChange('treeDataScope')"
|
||||
/>
|
||||
<el-tooltip content="开启后勾选父节点会自动勾选所有子节点,关闭则可独立勾选任意节点" placement="top">
|
||||
<el-icon style="margin-left: 4px; color: #909399; cursor: pointer"><el-icon-question-filled /></el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
<el-button-group>
|
||||
<el-button
|
||||
size="small"
|
||||
plain
|
||||
@click="handleSelectAll('treeDataScope', dataScopeGrantList)"
|
||||
>全选</el-button
|
||||
>
|
||||
<el-button
|
||||
size="small"
|
||||
plain
|
||||
@click="handleInvertSelect('treeDataScope', dataScopeGrantList, dataScopeLinked)"
|
||||
>反选</el-button
|
||||
>
|
||||
</el-button-group>
|
||||
<span style="color: #606266; font-size: 13px"
|
||||
>勾选多个菜单后点击「设置」,可批量配置数据权限;设置后菜单旁展示当前范围</span
|
||||
>
|
||||
<el-button type="primary" size="small" @click="openDataScopeSetting">设置</el-button>
|
||||
</el-row>
|
||||
<el-tree
|
||||
:data="dataScopeGrantList"
|
||||
:data="roleDataScopeMenuTree"
|
||||
show-checkbox
|
||||
:check-strictly="!dataScopeLinked"
|
||||
node-key="id"
|
||||
ref="treeDataScope"
|
||||
:default-checked-keys="dataScopeTreeObj"
|
||||
ref="treeRoleDataScope"
|
||||
:props="props"
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<span class="role-data-scope-node">
|
||||
<span>{{ data.title }}</span>
|
||||
<span v-if="getMenuScopeLabel(data.id)" class="role-data-scope-tag"
|
||||
>【{{ getMenuScopeLabel(data.id) }}】</span
|
||||
>
|
||||
</span>
|
||||
</template>
|
||||
</el-tree>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="接口权限">
|
||||
@@ -188,6 +173,45 @@
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
title="数据权限设置"
|
||||
append-to-body
|
||||
v-model="dataScopeSettingBox"
|
||||
width="560px"
|
||||
class="role-data-scope-setting-dialog"
|
||||
>
|
||||
<el-form label-position="right" label-width="auto">
|
||||
<el-form-item label="角色名称">
|
||||
<el-input v-model="dataScopeSetting.roleName" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="数据权限">
|
||||
<el-radio-group v-model="dataScopeSetting.scopeType">
|
||||
<el-radio :value="1">所在部门及下级</el-radio>
|
||||
<el-radio :value="2">全部组织</el-radio>
|
||||
<el-radio :value="3">本人创建</el-radio>
|
||||
<el-radio :value="4">自定义组织</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="Number(dataScopeSetting.scopeType) === 4" label="组织">
|
||||
<el-tree
|
||||
ref="treeCustomDept"
|
||||
:data="deptTree"
|
||||
show-checkbox
|
||||
node-key="id"
|
||||
default-expand-all
|
||||
:props="{ label: 'title', children: 'children' }"
|
||||
style="width: 100%; max-height: 320px; overflow: auto; border: 1px solid #ebeef5; padding: 8px"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="dataScopeSettingBox = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitDataScopeSetting">保存</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</basic-container>
|
||||
</template>
|
||||
|
||||
@@ -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 {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.role-data-scope-node {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.role-data-scope-tag {
|
||||
color: #409eff;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
@@ -84,7 +84,8 @@ export default {
|
||||
this.postFormData('save', lbpmFormValues, this.buildFormData());
|
||||
},
|
||||
postFormData(status, formValues, formData) {
|
||||
postMkPublicProcessMessage('project-apply', {
|
||||
const bizType = this.$route.meta?.bizType || 'project-apply';
|
||||
postMkPublicProcessMessage(bizType, {
|
||||
status,
|
||||
formValues: formValues || {},
|
||||
formData,
|
||||
|
||||
@@ -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>
|
||||
@@ -778,7 +810,21 @@
|
||||
<template v-if="isChangeDialog">
|
||||
<el-button v-if="!isProjectFormPage" @click="handleCancelProject">取消</el-button>
|
||||
<el-button v-if="isProjectFormPage" @click="closeProjectForm">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="submitChangeProject">
|
||||
<el-button
|
||||
v-if="isProjectChangeModule && !dialogReadonly"
|
||||
type="primary"
|
||||
plain
|
||||
:loading="submitLoading"
|
||||
@click="saveProjectChangeDraft"
|
||||
>
|
||||
保存
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="!dialogReadonly"
|
||||
type="primary"
|
||||
:loading="submitLoading"
|
||||
@click="submitChangeProject"
|
||||
>
|
||||
提交
|
||||
</el-button>
|
||||
</template>
|
||||
@@ -916,6 +962,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,
|
||||
@@ -951,6 +999,7 @@ import {
|
||||
projectTypeOptions,
|
||||
} from '@/option/business/common';
|
||||
import { config, option } from '@/option/business/project-apply';
|
||||
import * as projectChangeApi from '@/api/business/project-change';
|
||||
|
||||
const emptyForm = () => ({
|
||||
projectType: '',
|
||||
@@ -1026,6 +1075,9 @@ const attachmentViewerPlugins = [
|
||||
const projectApplyListRoute = '/business/project-apply';
|
||||
const projectApplyFormRoute = '/business/project-apply/form';
|
||||
const projectApplyListRefreshKey = 'project-apply-list-refresh';
|
||||
const projectChangeListRoute = '/business/project-change';
|
||||
const projectChangeFormRoute = '/business/project-change/form';
|
||||
const projectChangeListRefreshKey = 'project-change-list-refresh';
|
||||
|
||||
const otherAttachmentType = { label: '其他附件', value: '其他附件' };
|
||||
|
||||
@@ -1060,6 +1112,7 @@ export default {
|
||||
ElImageViewer,
|
||||
OpenFileViewer,
|
||||
PdfPreview,
|
||||
Position,
|
||||
},
|
||||
data() {
|
||||
const validateAmount = (rule, value, callback) => {
|
||||
@@ -1121,6 +1174,8 @@ export default {
|
||||
{ label: '高风险', value: 'high', statKey: 'high' },
|
||||
{ label: '中风险', value: 'medium', statKey: 'medium' },
|
||||
],
|
||||
// 合规(HCS)推送状态
|
||||
hcsRepushingId: null,
|
||||
page: {
|
||||
pageSize: 10,
|
||||
pageSizes: [10, 20, 50, 100],
|
||||
@@ -1208,7 +1263,40 @@ export default {
|
||||
},
|
||||
],
|
||||
changeReason: [
|
||||
{ max: 2000, message: '变更原因不能超过2000个字符', trigger: 'blur' },
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (!this.isChangeDialog) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
if (!value || !String(value).trim()) {
|
||||
callback(new Error('请输入变更原因'));
|
||||
return;
|
||||
}
|
||||
if (String(value).length > 2000) {
|
||||
callback(new Error('变更原因不能超过2000个字符'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
changeType: [
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (!this.isChangeDialog) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
if (!value) {
|
||||
callback(new Error('请选择变更类型'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
projectIntro: [
|
||||
{ max: 1000, message: '项目简介不能超过1000个字符', trigger: 'blur' },
|
||||
@@ -1337,8 +1425,11 @@ export default {
|
||||
isMajorProjectSupplementDialog() {
|
||||
return this.dialogType === 'majorSupplement';
|
||||
},
|
||||
isProjectChangeModule() {
|
||||
return this.$route.path === projectChangeFormRoute;
|
||||
},
|
||||
isChangeDialog() {
|
||||
return this.dialogType === 'change';
|
||||
return this.dialogType === 'change' || this.isProjectChangeModule;
|
||||
},
|
||||
isNewProjectDialog() {
|
||||
return ['add', 'majorSupplement'].includes(this.dialogType);
|
||||
@@ -1347,7 +1438,13 @@ export default {
|
||||
return this.isFormPageInstance;
|
||||
},
|
||||
isPublicViewPage() {
|
||||
return this.$route.path === '/business/project-apply/public-view';
|
||||
return (
|
||||
this.$route.path === '/business/project-apply/public-view' ||
|
||||
this.$route.path === '/business/project-change/public-view'
|
||||
);
|
||||
},
|
||||
publicBizType() {
|
||||
return this.$route.meta?.bizType || 'project-apply';
|
||||
},
|
||||
projectFormContainer() {
|
||||
// 表单页实例始终用页面容器,避免 keep-alive 失活时误切弹窗
|
||||
@@ -1414,7 +1511,7 @@ export default {
|
||||
this.loadCargoTypeOptions();
|
||||
this.loadTransportTypeOptions();
|
||||
this.loadSettlementModeOptions();
|
||||
if (this.$route.path === projectApplyFormRoute) {
|
||||
if (this.$route.path === projectApplyFormRoute || this.$route.path === projectChangeFormRoute) {
|
||||
this.isFormPageInstance = true;
|
||||
this.openProjectFormPage();
|
||||
}
|
||||
@@ -1480,14 +1577,14 @@ export default {
|
||||
},
|
||||
openPublicProjectForm() {
|
||||
const id = this.$route.query.id;
|
||||
this.dialogType = 'view';
|
||||
this.dialogType = this.publicBizType === 'project-change' ? 'change' : 'view';
|
||||
this.dialogReadonly = true;
|
||||
this.projectBox = true;
|
||||
if (!id) {
|
||||
this.$message.error('缺少项目ID');
|
||||
return;
|
||||
}
|
||||
getMkPublicDetail('project-apply', id)
|
||||
getMkPublicDetail(this.publicBizType, id)
|
||||
.then(res => {
|
||||
const detail = res.data?.data || {};
|
||||
this.applyPublicDictOptions(detail);
|
||||
@@ -1550,9 +1647,25 @@ export default {
|
||||
openProjectFormPage() {
|
||||
const type = this.$route.query.mode || 'add';
|
||||
const id = this.$route.query.id;
|
||||
this.dialogType = type;
|
||||
this.dialogType = this.isProjectChangeModule ? (type === 'view' ? 'view' : 'change') : type;
|
||||
this.dialogReadonly = type === 'view';
|
||||
this.projectBox = true;
|
||||
if (this.isProjectChangeModule) {
|
||||
if (!id) {
|
||||
this.$message.error('缺少项目变更ID');
|
||||
return;
|
||||
}
|
||||
projectChangeApi.getDetail(id).then(res => {
|
||||
const detail = res.data.data || {};
|
||||
this.applyProjectDetail({
|
||||
...detail,
|
||||
changeType: detail.changeType || '项目变更',
|
||||
changeNo: detail.changeNo || '',
|
||||
changeDate: detail.changeDate || this.$dayjs().format('YYYY-MM-DD'),
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (['add', 'majorSupplement'].includes(type)) {
|
||||
this.applyProjectDetail({
|
||||
...emptyForm(),
|
||||
@@ -1576,6 +1689,12 @@ export default {
|
||||
},
|
||||
closeProjectForm() {
|
||||
if (this.isProjectFormPage) {
|
||||
if (this.isProjectChangeModule) {
|
||||
sessionStorage.setItem(projectChangeListRefreshKey, '1');
|
||||
this.$router.$avueRouter?.closeTag?.();
|
||||
this.$router.push(projectChangeListRoute);
|
||||
return;
|
||||
}
|
||||
this.markProjectApplyListNeedRefresh();
|
||||
this.$router.$avueRouter?.closeTag?.();
|
||||
this.$router.push(projectApplyListRoute);
|
||||
@@ -1613,6 +1732,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;
|
||||
@@ -1734,7 +1940,7 @@ export default {
|
||||
},
|
||||
handleCustomOperation(operation, row) {
|
||||
if (operation.action === 'startChange') {
|
||||
this.openProjectDialog('change', row);
|
||||
this.startIndependentProjectChange(row);
|
||||
return;
|
||||
}
|
||||
if (operation.action === 'mkAudit') {
|
||||
@@ -1754,14 +1960,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, {
|
||||
@@ -2028,6 +2251,52 @@ export default {
|
||||
this.closeProjectForm();
|
||||
});
|
||||
},
|
||||
startIndependentProjectChange(row) {
|
||||
if (!row?.id) {
|
||||
this.$message.warning('项目ID为空,无法发起变更');
|
||||
return;
|
||||
}
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
text: '正在创建项目变更单…',
|
||||
background: 'rgba(255, 255, 255, 0.6)',
|
||||
});
|
||||
projectChangeApi
|
||||
.createFromProject(row.id)
|
||||
.then(res => {
|
||||
if (res.data?.success === false) {
|
||||
this.$message.error(res.data?.msg || '创建项目变更失败');
|
||||
return;
|
||||
}
|
||||
const detail = res.data?.data || {};
|
||||
if (!detail.id) {
|
||||
this.$message.error('创建项目变更失败:未返回变更单ID');
|
||||
return;
|
||||
}
|
||||
this.$router.push({
|
||||
path: projectChangeFormRoute,
|
||||
query: { mode: 'edit', id: detail.id, name: '项目变更' },
|
||||
});
|
||||
})
|
||||
.finally(() => loading.close());
|
||||
},
|
||||
saveProjectChangeDraft() {
|
||||
if (!this.isProjectChangeModule) return;
|
||||
this.submitLoading = true;
|
||||
projectChangeApi
|
||||
.saveDraft(this.normalizeSubmitForm({ includeChangeType: true }))
|
||||
.then(res => {
|
||||
if (res.data?.success === false || res.data?.data === false) {
|
||||
this.$message.error(res.data?.msg || '保存失败,请联系管理员');
|
||||
return;
|
||||
}
|
||||
this.$message.success('保存成功!');
|
||||
this.closeProjectForm();
|
||||
})
|
||||
.finally(() => {
|
||||
this.submitLoading = false;
|
||||
});
|
||||
},
|
||||
submitChangeProject() {
|
||||
this.$refs.projectForm.validate(valid => {
|
||||
if (!valid) {
|
||||
@@ -2036,7 +2305,34 @@ export default {
|
||||
}
|
||||
if (!this.validateAttachmentFileTypes()) return;
|
||||
this.submitLoading = true;
|
||||
// 变更记录仅在提交时生成,保存草稿不落记录
|
||||
if (this.isProjectChangeModule) {
|
||||
const payload = this.normalizeSubmitForm({ includeChangeType: true });
|
||||
const approvalStatus = this.form.approvalStatus || '';
|
||||
projectChangeApi
|
||||
.submit(payload)
|
||||
.then(res => {
|
||||
if (res.data?.success === false || res.data?.data === false) {
|
||||
this.$message.error(res.data?.msg || '提交失败,请联系管理员');
|
||||
return;
|
||||
}
|
||||
const id = this.form.id;
|
||||
return submitMkApprovalFlow({
|
||||
bizType: 'project-change',
|
||||
bizId: id,
|
||||
subjectName: this.form.projectName || '',
|
||||
approvalStatus,
|
||||
mkEnabled: this.mkEnabled,
|
||||
}).then(() => {
|
||||
this.$message.success('提交成功!');
|
||||
this.closeProjectForm();
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
this.submitLoading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 兼容旧入口:变更记录仅在提交时生成,保存草稿不落记录
|
||||
this.api
|
||||
.submitChange(this.normalizeSubmitForm({ includeChangeType: true }))
|
||||
.then(res => {
|
||||
|
||||
@@ -0,0 +1,362 @@
|
||||
<template>
|
||||
<basic-container class="project-change-page">
|
||||
<avue-crud
|
||||
:option="tableOption"
|
||||
:table-loading="loading"
|
||||
:data="data"
|
||||
v-model:page="page"
|
||||
v-model="tableForm"
|
||||
ref="crud"
|
||||
:permission="permissionList"
|
||||
@search-change="searchChange"
|
||||
@search-reset="searchReset"
|
||||
@selection-change="selectionChange"
|
||||
@current-change="currentChange"
|
||||
@size-change="sizeChange"
|
||||
@refresh-change="refreshChange"
|
||||
@on-load="onLoad"
|
||||
>
|
||||
<template #menu-left>
|
||||
<el-button
|
||||
type="danger"
|
||||
icon="el-icon-delete"
|
||||
plain
|
||||
v-if="hasPermission(`${config.permission}_delete`)"
|
||||
@click="handleDelete"
|
||||
>
|
||||
批量删除
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<template #approvalStatus="{ row }">
|
||||
{{ row.approvalStatusName || statusLabel(row.approvalStatus) }}
|
||||
</template>
|
||||
|
||||
<template #syncStatus="{ row }">
|
||||
<el-tag v-if="row.syncStatus" :type="syncTagType(row.syncStatus)" class="status-text">
|
||||
{{ syncLabel(row.syncStatus) }}
|
||||
</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
|
||||
<template #menu="{ row }">
|
||||
<el-link
|
||||
type="primary"
|
||||
v-if="canView(row)"
|
||||
@click="openForm('view', row)"
|
||||
>
|
||||
查看
|
||||
</el-link>
|
||||
<el-link type="primary" v-if="canEdit(row)" @click="openForm('edit', row)">
|
||||
编辑
|
||||
</el-link>
|
||||
<el-link
|
||||
type="danger"
|
||||
v-if="canDelete(row)"
|
||||
@click="rowDel(row)"
|
||||
>
|
||||
删除
|
||||
</el-link>
|
||||
<el-link
|
||||
v-for="operation in customOperations(row)"
|
||||
:key="operation.action"
|
||||
:type="operation.type === 'warning' ? 'warning' : operation.type === 'success' ? 'success' : 'primary'"
|
||||
@click="handleCustomOperation(operation, row)"
|
||||
>
|
||||
{{ operation.label }}
|
||||
</el-link>
|
||||
</template>
|
||||
</avue-crud>
|
||||
</basic-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as api from '@/api/business/project-change';
|
||||
import { getMKApprovalUrl } from '@/api/system/business-process';
|
||||
import { config, option } from '@/option/business/project-change';
|
||||
import { approvalStatusOptions } from '@/option/business/common';
|
||||
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
import { isAdministrator } from '@/utils/auth';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
const projectChangeFormRoute = '/business/project-change/form';
|
||||
const projectChangeListRefreshKey = 'project-change-list-refresh';
|
||||
|
||||
export default {
|
||||
name: 'ProjectChange',
|
||||
data() {
|
||||
return {
|
||||
api,
|
||||
config,
|
||||
tableForm: {},
|
||||
tableOption: this.buildTableOption(),
|
||||
loading: true,
|
||||
data: [],
|
||||
page: {
|
||||
pageSize: 10,
|
||||
currentPage: 1,
|
||||
total: 0,
|
||||
},
|
||||
query: {},
|
||||
selectionList: [],
|
||||
mkEnabled: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['permission', 'userInfo', 'website']),
|
||||
permissionList() {
|
||||
return {
|
||||
addBtn: false,
|
||||
viewBtn: false,
|
||||
delBtn: false,
|
||||
editBtn: false,
|
||||
};
|
||||
},
|
||||
isAdmin() {
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
ids() {
|
||||
return this.selectionList.map(item => item.id).join(',');
|
||||
},
|
||||
},
|
||||
created() {
|
||||
loadMkEnabled()
|
||||
.then(enabled => {
|
||||
this.mkEnabled = enabled;
|
||||
})
|
||||
.catch(() => {
|
||||
this.mkEnabled = false;
|
||||
});
|
||||
},
|
||||
activated() {
|
||||
if (sessionStorage.getItem(projectChangeListRefreshKey)) {
|
||||
sessionStorage.removeItem(projectChangeListRefreshKey);
|
||||
this.onLoad(this.page, this.query);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
buildTableOption() {
|
||||
return {
|
||||
...option,
|
||||
addBtn: false,
|
||||
viewBtn: false,
|
||||
editBtn: false,
|
||||
delBtn: false,
|
||||
menuWidth: 320,
|
||||
column: (option.column || []).map(column => ({
|
||||
...column,
|
||||
display: false,
|
||||
align: 'center',
|
||||
headerAlign: 'center',
|
||||
})),
|
||||
};
|
||||
},
|
||||
hasPermission(code) {
|
||||
return this.isAdmin || this.validData(this.permission && this.permission[code], false);
|
||||
},
|
||||
statusLabel(status) {
|
||||
return approvalStatusOptions.find(item => item.value === status)?.label || status || '-';
|
||||
},
|
||||
syncLabel(value) {
|
||||
return { unsynced: '待推送', synced: '已推送', failed: '推送失败' }[value] || value;
|
||||
},
|
||||
syncTagType(value) {
|
||||
return { unsynced: 'info', synced: 'success', failed: 'danger' }[value] || 'info';
|
||||
},
|
||||
inStatus(row, statusList) {
|
||||
if (!statusList || !statusList.length) return true;
|
||||
return statusList.includes(row.approvalStatus);
|
||||
},
|
||||
canEdit(row) {
|
||||
return (
|
||||
this.hasPermission(`${this.config.permission}_edit`) &&
|
||||
!row.readonly &&
|
||||
this.inStatus(row, this.config.editStatus)
|
||||
);
|
||||
},
|
||||
canDelete(row) {
|
||||
return (
|
||||
this.hasPermission(`${this.config.permission}_delete`) &&
|
||||
!row.readonly &&
|
||||
this.inStatus(row, this.config.deleteStatus)
|
||||
);
|
||||
},
|
||||
canView(row) {
|
||||
return (
|
||||
this.hasPermission(`${this.config.permission}_view`) &&
|
||||
this.inStatus(row, this.config.viewStatus)
|
||||
);
|
||||
},
|
||||
canOperation(operation, row) {
|
||||
if (operation.permission && !this.hasPermission(operation.permission)) return false;
|
||||
if (row.readonly && ['submitApproval', 'withdraw', 'approve', 'reject'].includes(operation.action)) {
|
||||
return false;
|
||||
}
|
||||
return this.inStatus(row, operation.status);
|
||||
},
|
||||
customOperations(row) {
|
||||
return (this.config.operations || []).filter(operation => {
|
||||
if (operation.mkOnly && !this.mkEnabled) return false;
|
||||
if (operation.localOnly && this.mkEnabled) return false;
|
||||
return this.canOperation(operation, row);
|
||||
});
|
||||
},
|
||||
openForm(mode, row = {}) {
|
||||
const query = { mode, id: row.id };
|
||||
if (mode === 'edit') query.name = '编辑项目变更';
|
||||
else if (mode === 'view') query.name = '查看项目变更';
|
||||
this.$router.push({ path: projectChangeFormRoute, query });
|
||||
},
|
||||
handleCustomOperation(operation, row) {
|
||||
if (operation.action === 'mkAudit') {
|
||||
this.handleMkAudit(row);
|
||||
return;
|
||||
}
|
||||
const run = () => {
|
||||
const request = () => this.api[operation.action](row.id);
|
||||
const afterMk =
|
||||
operation.action === 'submitApproval'
|
||||
? submitMkApprovalFlow({
|
||||
bizType: 'project-change',
|
||||
bizId: row.id,
|
||||
subjectName: row.projectName || '',
|
||||
approvalStatus: row.approvalStatus || '',
|
||||
mkEnabled: this.mkEnabled,
|
||||
}).then(() => request())
|
||||
: request();
|
||||
const loading =
|
||||
operation.action === 'approve'
|
||||
? this.$loading({
|
||||
lock: true,
|
||||
text: '正在推送合规,请稍候…',
|
||||
background: 'rgba(255, 255, 255, 0.6)',
|
||||
})
|
||||
: null;
|
||||
afterMk
|
||||
.then(res => {
|
||||
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());
|
||||
};
|
||||
this.$confirm(operation.confirm || `确定${operation.label}该${this.config.title}?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(() => run());
|
||||
},
|
||||
handleMkAudit(row) {
|
||||
if (!row?.id) {
|
||||
this.$message.warning('变更单ID为空,无法打开审核页');
|
||||
return;
|
||||
}
|
||||
getMKApprovalUrl({ bizId: String(row.id) })
|
||||
.then(res => {
|
||||
const url = res?.data?.data;
|
||||
if (!url) {
|
||||
this.$message.error('获取审核跳转地址失败');
|
||||
return;
|
||||
}
|
||||
window.open(url, '_blank');
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.error('获取审核跳转地址失败');
|
||||
});
|
||||
},
|
||||
searchReset() {
|
||||
this.query = {};
|
||||
this.onLoad(this.page);
|
||||
},
|
||||
searchChange(params, done) {
|
||||
this.query = params;
|
||||
this.page.currentPage = 1;
|
||||
this.onLoad(this.page, params);
|
||||
done();
|
||||
},
|
||||
selectionChange(list) {
|
||||
this.selectionList = list;
|
||||
},
|
||||
selectionClear() {
|
||||
this.selectionList = [];
|
||||
this.$refs.crud?.toggleSelection?.();
|
||||
},
|
||||
currentChange(currentPage) {
|
||||
this.page.currentPage = currentPage;
|
||||
},
|
||||
sizeChange(pageSize) {
|
||||
this.page.pageSize = pageSize;
|
||||
},
|
||||
refreshChange() {
|
||||
this.onLoad(this.page, this.query);
|
||||
},
|
||||
rowDel(row) {
|
||||
this.$confirm('确定将选择数据删除?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
this.api.remove(row.id).then(() => {
|
||||
this.$message.success('删除成功!');
|
||||
this.onLoad(this.page, this.query);
|
||||
});
|
||||
});
|
||||
},
|
||||
handleDelete() {
|
||||
if (!this.selectionList.length) {
|
||||
this.$message.warning('请选择至少一条数据');
|
||||
return;
|
||||
}
|
||||
this.$confirm('确定将选择数据删除?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
this.api.remove(this.ids).then(() => {
|
||||
this.$message.success('删除成功!');
|
||||
this.selectionClear();
|
||||
this.onLoad(this.page, this.query);
|
||||
});
|
||||
});
|
||||
},
|
||||
onLoad(page, params = {}) {
|
||||
this.loading = true;
|
||||
const values = {
|
||||
...this.query,
|
||||
...params,
|
||||
};
|
||||
this.api
|
||||
.getList(page.currentPage, page.pageSize, values)
|
||||
.then(res => {
|
||||
const data = res.data.data || {};
|
||||
this.page.total = data.total || 0;
|
||||
this.data = data.records || [];
|
||||
this.loading = false;
|
||||
this.selectionClear();
|
||||
})
|
||||
.catch(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.project-change-page {
|
||||
:deep(.avue-crud__menu) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -156,9 +156,17 @@ export default {
|
||||
});
|
||||
},
|
||||
async handleApprove(row) {
|
||||
await api.approve({ id: row.id });
|
||||
this.$message.success('审批通过');
|
||||
this.loadTable();
|
||||
await this.$confirm('确认审批通过该汇票付款?', '提示', { type: 'warning' });
|
||||
// 防止重复点击:请求期间表格遮罩,后续点击直接忽略
|
||||
if (this.loading) return;
|
||||
this.loading = true;
|
||||
try {
|
||||
await api.approve({ id: row.id });
|
||||
this.$message.success('审批通过');
|
||||
await this.loadTable();
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
async handleReturn(row) {
|
||||
const { value } = await this.$prompt('请输入驳回原因', '审批驳回');
|
||||
|
||||
@@ -323,17 +323,8 @@
|
||||
<el-table-column label="可开票金额" min-width="140" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.availableInvoiceAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="分摊发票金额" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-model="row.allocatedInvoiceAmount"
|
||||
:disabled="readonly"
|
||||
:min="0"
|
||||
:max="Number(row.availableInvoiceAmount || 0)"
|
||||
:precision="2"
|
||||
:controls="false"
|
||||
/>
|
||||
</template>
|
||||
<el-table-column label="分摊发票金额" min-width="140" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.allocatedInvoiceAmount) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section-card>
|
||||
@@ -454,6 +445,9 @@
|
||||
<el-table-column label="结算金额" min-width="130" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.settlementAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="已申请开票金额" min-width="140" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.invoicedAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="可开票金额" min-width="140" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.availableInvoiceAmount) }}</template>
|
||||
</el-table-column>
|
||||
@@ -861,7 +855,6 @@ export default {
|
||||
keyword: this.settlementDialog.keyword,
|
||||
contractCategory: '客户合同',
|
||||
settlementType: 'receivable',
|
||||
invoiceStatus: 'unreceived',
|
||||
}
|
||||
)
|
||||
);
|
||||
@@ -913,7 +906,7 @@ export default {
|
||||
...item,
|
||||
formalSettlementId: item.id,
|
||||
settlementId: item.id,
|
||||
allocatedInvoiceAmount: rows.length === 1 ? Number(item.availableInvoiceAmount || 0) : 0,
|
||||
allocatedInvoiceAmount: 0,
|
||||
}));
|
||||
this.form.projectName = first.projectName;
|
||||
this.form.deptName = first.deptName;
|
||||
|
||||
@@ -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('请输入驳回原因', '审批驳回');
|
||||
|
||||
@@ -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('请输入驳回原因', '审批驳回');
|
||||
|
||||
@@ -219,6 +219,11 @@
|
||||
show-word-limit /></el-form-item
|
||||
></el-col>
|
||||
</el-row>
|
||||
<!-- 风险客商提示(基本信息备注后,同合同新增页):收款方外部客商高风险/黑名单时展示,文案由后端 riskTip 拼装 -->
|
||||
<div v-if="payeeRiskText" class="payment-form-page__risk-tip">
|
||||
<div class="payment-form-page__risk-tip-title">风险客商提示:</div>
|
||||
<div class="payment-form-page__risk-tip-content">{{ payeeRiskText }}</div>
|
||||
</div>
|
||||
</section-card>
|
||||
<section-card v-if="isSettlementRelatedType" title="结算信息">
|
||||
<el-table :data="settlementRows" border
|
||||
@@ -719,6 +724,7 @@ import * as billLedgerApi from '@/api/payment/billLedger';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { getList as getProjectList } from '@/api/business/project-apply';
|
||||
import { getDetail as getContractDetail } from '@/api/business/contract-manage';
|
||||
import { getRisk as getCustomerRisk } from '@/api/business/hcs';
|
||||
import * as formalApi from '@/api/settlement/formalSettlement';
|
||||
import * as preApi from '@/api/settlement/preSettlement';
|
||||
import {
|
||||
@@ -874,6 +880,11 @@ export default {
|
||||
/** 合同付款比例设置(按第1笔、第2笔…顺序) */
|
||||
contractPaymentRatioRows: [],
|
||||
payeeCustomerLevel: '',
|
||||
// 收款方 C101-1 风险查询留档:risky=高风险/黑名单,tip=后端拼装提示文案
|
||||
payeeRisky: false,
|
||||
payeeRiskTip: '',
|
||||
payeeRiskLevel: '',
|
||||
payeeBlackStatus: '',
|
||||
attachmentRuleLoading: false,
|
||||
// 最近一次从结算单同步过来的附件 URL,切换结算单时用于整批替换
|
||||
linkedSettlementAttachmentUrls: [],
|
||||
@@ -1047,11 +1058,22 @@ export default {
|
||||
});
|
||||
},
|
||||
highRiskPayee() {
|
||||
// 收款方 C101-1 高风险/黑名单,或客商等级 D/E/F(本地评级口径),命中须上传特批附件
|
||||
if (this.payeeRisky) return true;
|
||||
const level = String(this.payeeCustomerLevel || '')
|
||||
.trim()
|
||||
.toUpperCase();
|
||||
return ['D', 'D级', 'E', 'E级', 'F', 'F级'].includes(level) || level.includes('高风险');
|
||||
},
|
||||
// 高风险/黑名单收款方提示(C101-1,同合同新增页风险客商提示口径;强制拦截在后端提交校验与金蝶推送门禁)
|
||||
payeeRiskText() {
|
||||
if (!this.payeeRisky) return '';
|
||||
const levelMap = { 0: '低风险', 1: '中风险', 2: '高风险' };
|
||||
const level = levelMap[this.payeeRiskLevel] || '未知';
|
||||
const blacklist = String(this.payeeBlackStatus || '').trim().toLowerCase() === 'yes' ? '是' : '否';
|
||||
const tip = this.payeeRiskTip || `收款方[${this.form.payeeName || ''}]为高风险/黑名单客商`;
|
||||
return `风险等级:${level}|黑名单:${blacklist}。${tip},按集团规定不可直接付款,提交前须上传「特批附件」类型附件,审批通过后将自动发起付款特批`;
|
||||
},
|
||||
requiredAttachmentMaterials() {
|
||||
if (this.form.paymentType === 'project_advance') {
|
||||
return [ATTACHMENT_MATERIALS.contract];
|
||||
@@ -2317,6 +2339,10 @@ export default {
|
||||
const preserveCurrentValue = autoSelectDefault === false;
|
||||
this.receiptAccountOptions = [];
|
||||
this.payeeCustomerLevel = '';
|
||||
this.payeeRisky = false;
|
||||
this.payeeRiskTip = '';
|
||||
this.payeeRiskLevel = '';
|
||||
this.payeeBlackStatus = '';
|
||||
if (!partyBName) {
|
||||
if (!preserveCurrentValue) this.clearReceiptAccount();
|
||||
return;
|
||||
@@ -2348,6 +2374,19 @@ export default {
|
||||
}
|
||||
const detail = this.unwrapData(await getCustomerArchiveDetail(customer.id));
|
||||
this.payeeCustomerLevel = detail.customerLevel || '';
|
||||
// 收款方实时查 C101-1 客商风险(后端对内部组织客商默认返回无风险):高风险/黑名单在
|
||||
// 基本信息"风险客商提示"条常驻展示并要求特批附件;查询失败不阻断填写,提交时后端强校验兜底
|
||||
if (detail.customerCode) {
|
||||
try {
|
||||
const risk = this.unwrapData(await getCustomerRisk(detail.customerCode)) || {};
|
||||
this.payeeRisky = Boolean(risk.risky);
|
||||
this.payeeRiskTip = risk.riskTip || '';
|
||||
this.payeeRiskLevel = risk.fdRiskLevel || '';
|
||||
this.payeeBlackStatus = risk.fdBlackStatus || '';
|
||||
} catch (error) {
|
||||
// 忽略风险查询异常,提交时后端 C101-1 强校验兜底
|
||||
}
|
||||
}
|
||||
this.receiptAccountOptions = (detail.receiptAccounts || [])
|
||||
.filter(item => 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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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('请输入驳回原因', '审批驳回', {
|
||||
|
||||
@@ -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('请输入驳回原因', '审批驳回', {
|
||||
|
||||
@@ -317,11 +317,18 @@ export default {
|
||||
},
|
||||
async handleApprove(row) {
|
||||
await this.$confirm('审批通过后将更新关联正式结算单,确认继续?', '提示');
|
||||
const res = await api.approve(row.id);
|
||||
// data=false:自动同步金蝶失败;data=true 且 msg 带已推送场景的手工冲销提示
|
||||
if (res.data.data === true || res.data.data === false) this.$message.warning(res.data.msg);
|
||||
else this.$message.success(res.data.msg || '审批通过');
|
||||
this.loadTable();
|
||||
// 防止重复点击:请求期间表格遮罩,后续点击直接忽略
|
||||
if (this.loading) return;
|
||||
this.loading = true;
|
||||
try {
|
||||
const res = await api.approve(row.id);
|
||||
// data=false:自动同步金蝶失败;data=true 且 msg 带已推送场景的手工冲销提示
|
||||
if (res.data.data === true || res.data.data === false) this.$message.warning(res.data.msg);
|
||||
else this.$message.success(res.data.msg || '审批通过');
|
||||
await this.loadTable();
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
async handleReturn(row) {
|
||||
const { value } = await this.$prompt('请输入驳回原因', '驳回', {
|
||||
|
||||
@@ -175,8 +175,6 @@
|
||||
<el-col :span="6"><el-form-item label="工号"><el-input v-model="form.code" :disabled="isUserViewMode" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="OA登录名"><el-input v-model="form.oaLoginId" :disabled="isUserViewMode" maxlength="128" placeholder="请输入OA登录名" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="人员类别"><el-select v-model="form.personCategory" :disabled="isUserViewMode"><el-option label="内部员工" :value="1" /><el-option label="承运商" :value="2" /></el-select></el-form-item></el-col>
|
||||
<el-col :span="18"><el-form-item label="数据权限范围"><el-radio-group v-model="form.dataScopeRange" :disabled="isUserViewMode"><el-radio :value="1">仅所属组织</el-radio><el-radio :value="2">全部组织</el-radio><el-radio :value="3">自定义</el-radio></el-radio-group></el-form-item></el-col>
|
||||
<el-col :span="18"><el-form-item label="数据层级范围"><el-radio-group v-model="form.dataLevelRange" :disabled="isUserViewMode"><el-radio :value="1">包含下级</el-radio><el-radio :value="2">仅本级</el-radio></el-radio-group></el-form-item></el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="备注">
|
||||
<el-input
|
||||
@@ -202,13 +200,6 @@
|
||||
<el-table-column v-if="!isUserViewMode" label="操作" width="90"><template #default="{ row }"><el-link type="danger" @click="removeFormRole(row.id)">删除</el-link></template></el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane v-if="form.dataScopeRange === 3" label="数据权限">
|
||||
<el-tree :data="permissionDeptTree" show-checkbox node-key="id" :default-checked-keys="form.dataScopeDeptIds || []" :props="{ label: 'title', children: 'children' }" @check="handleDataScopeCheck" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="客商权限">
|
||||
<div class="permission-toolbar customer-toolbar"><el-checkbox v-model="form.includeNewCustomer" :true-value="1" :false-value="0" :disabled="isUserViewMode">包含新增的客商</el-checkbox></div>
|
||||
<el-table ref="customerTable" :data="customerList" border @selection-change="handleCustomerSelection"><el-table-column type="selection" width="55" :selectable="isCustomerSelectable" /><el-table-column prop="customerCode" label="客商编码" /><el-table-column prop="fullName" label="客商名称" /></el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<template #footer>
|
||||
<span v-if="isUserViewMode" class="dialog-footer">
|
||||
@@ -407,7 +398,6 @@ import {
|
||||
auditRefuse,
|
||||
setLeader,
|
||||
getLeaderList,
|
||||
getCustomerOptions,
|
||||
} from '@/api/system/user';
|
||||
import { exportBlob } from '@/api/common';
|
||||
import { getDeptTree } from '@/api/system/dept';
|
||||
@@ -477,7 +467,6 @@ export default {
|
||||
treeData: [],
|
||||
permissionDeptTree: [],
|
||||
postList: [],
|
||||
customerList: [],
|
||||
treeOption: treeOption,
|
||||
userOption: userOption(this),
|
||||
data: [],
|
||||
@@ -952,9 +941,6 @@ export default {
|
||||
const column = this.findColumn(this.userOption.group, 'leaderId');
|
||||
column.dicData = res.data.data;
|
||||
});
|
||||
getCustomerOptions().then(res => {
|
||||
this.customerList = res.data.data || [];
|
||||
});
|
||||
},
|
||||
// 由部门 id 反查「根 → 该节点」的 id 路径,供级联回显(找不到返回 null)
|
||||
findDeptPath(id, tree = this.permissionDeptTree, parents = []) {
|
||||
@@ -1000,21 +986,6 @@ export default {
|
||||
.split(this.form.roleId || '')
|
||||
.filter(id => String(id) !== String(roleId));
|
||||
},
|
||||
handleDataScopeCheck(_, checked) {
|
||||
if (this.isUserViewMode) {
|
||||
return;
|
||||
}
|
||||
this.form.dataScopeDeptIds = checked.checkedKeys;
|
||||
},
|
||||
handleCustomerSelection(rows) {
|
||||
if (this.isUserViewMode) {
|
||||
return;
|
||||
}
|
||||
this.form.customerIds = rows.map(row => row.id);
|
||||
},
|
||||
isCustomerSelectable() {
|
||||
return !this.isUserViewMode;
|
||||
},
|
||||
normalizeSexDisplay(sex) {
|
||||
if (sex === -1 || sex === '-1' || sex === '' || sex === null || sex === undefined) {
|
||||
return undefined;
|
||||
@@ -1065,13 +1036,8 @@ export default {
|
||||
deptId: [],
|
||||
code: '',
|
||||
personCategory: 1,
|
||||
dataScopeRange: 1,
|
||||
dataLevelRange: 1,
|
||||
remark: '',
|
||||
roleId: '',
|
||||
dataScopeDeptIds: [],
|
||||
customerIds: [],
|
||||
includeNewCustomer: 0,
|
||||
};
|
||||
this.userDialog = true;
|
||||
this.$nextTick(() => this.$refs.userFormRef && this.$refs.userFormRef.clearValidate());
|
||||
@@ -1085,22 +1051,13 @@ export default {
|
||||
deptId: Array.isArray(deptIds) ? deptIds : [],
|
||||
postId: func.split(user.postId || '')[0] || '',
|
||||
roleId: func.join(user.roleId || ''),
|
||||
dataScopeDeptIds: user.dataScopeDeptIds || [],
|
||||
customerIds: user.customerIds || [],
|
||||
includeNewCustomer: user.includeNewCustomer || 0,
|
||||
personCategory: user.personCategory || 1,
|
||||
dataScopeRange: user.dataScopeRange || 1,
|
||||
dataLevelRange: user.dataLevelRange || 1,
|
||||
sex: this.normalizeSexDisplay(user.sex),
|
||||
};
|
||||
this.sensitiveManager.saveInitialData(this.form);
|
||||
this.userDialog = true;
|
||||
this.$nextTick(() => {
|
||||
this.$refs.userFormRef && this.$refs.userFormRef.clearValidate();
|
||||
(this.form.customerIds || []).forEach(id => {
|
||||
const customer = this.customerList.find(item => String(item.id) === String(id));
|
||||
if (customer) this.$refs.customerTable && this.$refs.customerTable.toggleRowSelection(customer, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
@@ -47,6 +47,29 @@
|
||||
{{ row.customerCode }}
|
||||
</el-button>
|
||||
</template>
|
||||
<template #customerRisk="{ row }">
|
||||
<el-tooltip
|
||||
v-if="customerRiskMap[row.customerCode] && customerRiskMap[row.customerCode].risky"
|
||||
placement="top"
|
||||
effect="dark"
|
||||
popper-class="customer-risk-popper"
|
||||
>
|
||||
<template #content>
|
||||
<div>{{ customerRiskMap[row.customerCode].riskTip || '该客商为高风险/黑名单客商' }}</div>
|
||||
</template>
|
||||
<el-tag type="danger" class="status-text">
|
||||
{{ customerRiskLabel(customerRiskMap[row.customerCode]) }}
|
||||
</el-tag>
|
||||
</el-tooltip>
|
||||
<el-tag
|
||||
v-else-if="customerRiskMap[row.customerCode]"
|
||||
:type="customerRiskMap[row.customerCode].fdRiskLevel === '1' ? 'warning' : 'success'"
|
||||
class="status-text"
|
||||
>
|
||||
{{ customerRiskLabel(customerRiskMap[row.customerCode]) }}
|
||||
</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template #accessType="{ row }">
|
||||
<el-tag
|
||||
class="status-text"
|
||||
@@ -332,7 +355,19 @@
|
||||
maxlength="100"
|
||||
show-word-limit
|
||||
:disabled="kingdeeLocked"
|
||||
@blur="checkArchiveNameRisk"
|
||||
/>
|
||||
<el-tooltip
|
||||
v-if="archiveNameRisky"
|
||||
placement="top"
|
||||
effect="dark"
|
||||
popper-class="customer-risk-popper"
|
||||
>
|
||||
<template #content>
|
||||
<div>{{ archiveNameRiskTip || '该客商为高风险/黑名单客商' }}</div>
|
||||
</template>
|
||||
<div class="archive-name-risk">高风险/黑名单客商</div>
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
@@ -1556,6 +1591,7 @@ import {
|
||||
queryKingdeeCustomer,
|
||||
syncKingdee,
|
||||
} from '@/api/vehicle/customer-archive';
|
||||
import { getRiskBatch, getRiskByName } from '@/api/business/hcs';
|
||||
import {
|
||||
getList as getCreditScoreQuantificationList,
|
||||
getDetail as getCreditScoreQuantificationDetail,
|
||||
@@ -1657,6 +1693,9 @@ export default {
|
||||
deptSearchInitialized: false,
|
||||
loading: true,
|
||||
data: [],
|
||||
customerRiskMap: {},
|
||||
archiveNameRisky: false,
|
||||
archiveNameRiskTip: '',
|
||||
page: {
|
||||
pageSize: 10,
|
||||
pageSizes: [10, 20, 50, 100],
|
||||
@@ -1914,6 +1953,12 @@ export default {
|
||||
{ label: '内部组织', value: 'internal' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '客商风险',
|
||||
prop: 'customerRisk',
|
||||
slot: true,
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
label: '客户性质',
|
||||
prop: 'customerNature',
|
||||
@@ -2417,6 +2462,7 @@ export default {
|
||||
resetKingdeeBackfill() {
|
||||
const form = this.archiveForm;
|
||||
form.kingdeeStatus = 'none';
|
||||
form.customerCode = '';
|
||||
form.fullName = '';
|
||||
form.legalPerson = '';
|
||||
form.registeredCapital = '';
|
||||
@@ -2472,8 +2518,11 @@ export default {
|
||||
applyKingdeeCustomer(customer) {
|
||||
const form = this.archiveForm;
|
||||
form.kingdeeStatus = 'exist';
|
||||
// 客商编号回显金蝶客商编码,与后端同步金蝶时的采纳口径保持一致
|
||||
if (customer.number) form.customerCode = customer.number;
|
||||
// 工商信息以金蝶为准,直接覆盖(后续字段锁定)
|
||||
if (customer.name) form.fullName = customer.name;
|
||||
if (customer.name) this.checkArchiveNameRisk();
|
||||
if (customer.artificialperson) form.legalPerson = customer.artificialperson;
|
||||
if (customer.bizpartnerAddress) form.registeredDetailAddress = customer.bizpartnerAddress;
|
||||
if (customer.regcapital) {
|
||||
@@ -3929,6 +3978,7 @@ export default {
|
||||
const scope = this.getBusinessLicenseOcrValue(result, ['经营范围']);
|
||||
if (code) this.archiveForm.unifiedCreditCode = code.replace(/\s/g, '').toUpperCase();
|
||||
if (name) this.archiveForm.fullName = name;
|
||||
if (name) this.checkArchiveNameRisk();
|
||||
if (legalPerson) this.archiveForm.legalPerson = legalPerson;
|
||||
if (address) {
|
||||
this.archiveForm.registeredAddress = address;
|
||||
@@ -4349,6 +4399,7 @@ export default {
|
||||
this.selectedQualificationFiles = [];
|
||||
if (this.isPublicViewPage) this.ensurePublicViewOptions(this.archiveForm);
|
||||
this.applyFixedOwnerDept();
|
||||
this.checkArchiveNameRisk();
|
||||
this.archiveBox = true;
|
||||
this.loadChangeRecords();
|
||||
if (this.reviewMode) this.openReviewScore();
|
||||
@@ -4367,6 +4418,7 @@ export default {
|
||||
this.ocrQualificationUploadFiles = [];
|
||||
this.selectedQualificationFiles = [];
|
||||
this.applyFixedOwnerDept();
|
||||
this.checkArchiveNameRisk();
|
||||
this.archiveBox = true;
|
||||
},
|
||||
resetArchive() {
|
||||
@@ -5476,12 +5528,52 @@ export default {
|
||||
const pageData = res.data.data;
|
||||
this.page.total = pageData.total;
|
||||
this.data = pageData.records;
|
||||
this.loadCustomerRisk(this.data);
|
||||
this.selectionClear();
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
// 客商风险标签文案:后端 riskLevelName 转义(黑名单/高风险/中风险/低风险),缺失时兜底
|
||||
customerRiskLabel(risk) {
|
||||
if (!risk) return '';
|
||||
return risk.riskLevelName || (risk.risky ? '风险客商' : '正常');
|
||||
},
|
||||
// 客商名称风险校验(C101-1,仅展示不作为安全边界):名称失焦/回显时查询,
|
||||
// risky 在名称框下展示红色提示,悬浮见风险项明细;查询失败静默不阻断填写,提交链路由后端强校验兜底
|
||||
checkArchiveNameRisk() {
|
||||
this.archiveNameRisky = false;
|
||||
this.archiveNameRiskTip = '';
|
||||
const customerName = String(this.archiveForm.fullName || '').trim();
|
||||
if (!customerName) return;
|
||||
getRiskByName(customerName)
|
||||
.then(res => {
|
||||
const risk = (res.data && res.data.data) || {};
|
||||
this.archiveNameRisky = risk.risky === true;
|
||||
this.archiveNameRiskTip = risk.riskTip || '';
|
||||
})
|
||||
.catch(() => {
|
||||
// 忽略风险查询异常,表单正常填写
|
||||
});
|
||||
},
|
||||
// 客商列表风险批量查询(C101-1,仅展示不作为安全边界):失败静默,行内显示"-",提交链路由后端强校验兜底
|
||||
loadCustomerRisk(rows) {
|
||||
this.customerRiskMap = {};
|
||||
const customerCodes = [...new Set(rows.map(row => row.customerCode).filter(Boolean))];
|
||||
if (!customerCodes.length) return;
|
||||
getRiskBatch(customerCodes)
|
||||
.then(res => {
|
||||
const riskMap = {};
|
||||
((res.data && res.data.data) || []).forEach(risk => {
|
||||
if (risk && risk.customerCode) riskMap[risk.customerCode] = risk;
|
||||
});
|
||||
this.customerRiskMap = riskMap;
|
||||
})
|
||||
.catch(() => {
|
||||
// 忽略风险查询异常,列表正常展示
|
||||
});
|
||||
},
|
||||
handleExport() {
|
||||
this.$confirm('是否导出客商档案数据?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
@@ -5577,6 +5669,20 @@ export default {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
// 风险客商悬浮提示:popper 挂在 body 下,需用 :global 约束宽度
|
||||
:global(.customer-risk-popper) {
|
||||
max-width: 360px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
// 客商名称框下方的高风险/黑名单提示(对齐表单错误文案形态)
|
||||
.archive-name-risk {
|
||||
color: var(--el-color-danger);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.fund-use-risk-none {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user