Compare commits

...

12 Commits

Author SHA1 Message Date
刘泉佳 d76977c07c fix: 新增客商档案风险提示按等级与黑名单细分并优先按编号查询 2026-10-30 20:09:57 +08:00
刘泉佳 e95151ad51 feat: 合同列表新增客商风险列并抽取风险标签公共展示方法(客商页复用) 2026-10-30 14:41:49 +08:00
刘泉佳 5351911d50 refactor: 合同侧黑名单状态字段改kingdeeBlackStatus(与后端列名统一) 2026-10-30 14:10:30 +08:00
刘泉佳 e602eecee8 fix: 客商列表黑名单标签判定改为仅金蝶黑名单命中(与后端口径一致) 2026-10-30 13:42:20 +08:00
刘泉佳 70801096dd feat: 付款/收款结算单查看页面新增同步金蝶按钮,单张推送并提示结果与异常原因 2026-10-30 12:21:16 +08:00
刘泉佳 025cba317d fix: 修复客商档案清空客商风险后仍按上次条件查询
Avue 搜索事件只回传非空筛选项,被清空的字段不会出现在参数里,
与上一次查询条件合并会保留旧值,改为点击查询时整体重建查询条件。
2026-10-30 12:12:57 +08:00
刘泉佳 26140aecb9 refactor: 客商列表风险筛选项改为纯字典加载(customer_risk)去除硬编码兜底 2026-10-30 11:47:37 +08:00
刘泉佳 8b2fa09cb9 fix: 客商列表风险列加宽至150适配组合文案 2026-10-30 11:46:07 +08:00
刘泉佳 4d78466824 fix: 客商列表风险列黑名单与任意等级组合展示(如低风险、黑名单) 2026-10-30 11:44:55 +08:00
刘泉佳 c6c7c80d97 feat: 客商列表风险列内部组织客商默认无风险并组合展示高风险与黑名单 2026-10-30 11:35:46 +08:00
刘泉佳 3397bd6a7b feat: 客商列表风险列改读落库字段并支持字典筛选与后台明细悬浮展示 2026-10-30 11:33:38 +08:00
刘泉佳 4b060fc045 feat: 开票申请查看展示每张发票金蝶开票状态与结果备注 2026-10-29 18:31:35 +08:00
9 changed files with 167 additions and 48 deletions
+2 -2
View File
@@ -32,11 +32,11 @@ export const refreshMasterData = () =>
timeout: 60000,
});
export const getRisk = customerCode =>
export const getRisk = (customerCode, creditCode) =>
request({
url: `${baseUrl}/risk`,
method: 'get',
params: { customerCode },
params: { customerCode, creditCode },
timeout: 30000,
});
+8
View File
@@ -621,6 +621,14 @@ export const option = createCrudOption([
addDisplay: false,
editDisplay: false,
},
{
label: '客商风险',
prop: 'customerRisk',
slot: true,
minWidth: 150,
addDisplay: false,
editDisplay: false,
},
{
label: '金蝶合同ID',
prop: 'kingdeeId',
+28
View File
@@ -0,0 +1,28 @@
/**
* 客商/合同风险标签公共展示逻辑(黑名单=金蝶黑名单 A801-5 命中,与后端 HcsCreditCheckSupport 口径一致):
* 等级与黑名单组合展示、等级在前;内部组织客商默认无风险
*/
/** 金蝶黑名单命中判定(客商档案行:失信类别/列入事由任一非空) */
export function isKingdeeBlack(category, reason) {
return !!(category || reason);
}
/** 合同行黑名单判定(推送链路按金蝶命中口径落库 kingdeeBlackStatus=yes) */
export function isContractBlack(blackStatus) {
return String(blackStatus || '').toLowerCase() === 'yes';
}
/** 风险标签文案:riskLevel=C101-1 等级(0/1/2);isBlack=金蝶黑名单命中;customerKind=internal 显示"无风险" */
export function resolveRiskTagName(riskLevel, isBlack, customerKind) {
const levelName = { 2: '高风险', 1: '中风险', 0: '低风险' }[riskLevel] || '';
if (isBlack) return levelName ? `${levelName}、黑名单` : '黑名单';
if (levelName) return levelName;
if (customerKind === 'internal') return '无风险';
return '';
}
/** 是否红色风险标签(黑名单命中或高风险) */
export function isRiskyTag(riskLevel, isBlack) {
return isBlack || riskLevel === '2';
}
@@ -38,7 +38,7 @@
contract.hcsFdId || '-'
}}</el-descriptions-item>
<el-descriptions-item label="客商风险">
<template v-if="contract.hcsRiskLevel || contract.hcsBlackStatus">
<template v-if="contract.hcsRiskLevel || contract.kingdeeBlackStatus">
<el-tag
v-if="isBlacklist"
type="danger"
@@ -130,7 +130,7 @@ export default {
name: 'HcsContractPanel',
components: { UploadFilled },
props: {
// 合同详情行(须含 id/hcsSyncStatus/hcsFdId/hcsRiskLevel/hcsBlackStatus/hcsSpecStatus 等)
// 合同详情行(须含 id/hcsSyncStatus/hcsFdId/hcsRiskLevel/kingdeeBlackStatus/hcsSpecStatus 等)
contract: { type: Object, required: true },
},
emits: ['refresh'],
@@ -152,7 +152,7 @@ export default {
return this.contract.hcsSyncMsg || '';
},
isBlacklist() {
return String(this.contract.hcsBlackStatus || '').toLowerCase() === 'yes';
return String(this.contract.kingdeeBlackStatus || '').toLowerCase() === 'yes';
},
specLabel() {
return SPEC_STATUS_OPTIONS.find(item => item.value === this.contract.hcsSpecStatus)?.label || '';
@@ -745,7 +745,7 @@ export default {
// 高风险/黑名单客商提示:按合同推送合规留痕 hcs_risk_level/hcs_black_status 判定(与合同详情口径一致)
riskTipText() {
const riskLevel = String(this.form.hcsRiskLevel || '').trim();
const blacklist = String(this.form.hcsBlackStatus || '').toLowerCase() === 'yes';
const blacklist = String(this.form.kingdeeBlackStatus || '').toLowerCase() === 'yes';
if (riskLevel !== '2' && !blacklist) return '';
const parties = [this.form.partyA, this.form.partyB]
.map(name => String(name || '').trim())
+21 -1
View File
@@ -92,6 +92,18 @@
<span v-else>-</span>
</template>
<!-- 客商风险:等级与金蝶黑名单组合展示(推送链路按金蝶命中口径落库),复用公共展示方法 -->
<template #customerRisk="{ row }">
<el-tag
v-if="contractRiskTagName(row)"
:type="isContractRisky(row) ? 'danger' : row.hcsRiskLevel === '1' ? 'warning' : 'success'"
class="status-text"
>
{{ contractRiskTagName(row) }}
</el-tag>
<span v-else>-</span>
</template>
<template #menu="{ row }">
<el-link
v-if="hasPermission(`${config.permission}_view`)"
@@ -1091,6 +1103,7 @@ import { exportBlob } from '@/api/common';
import * as api from '@/api/business/contract-manage';
import { createFromContract } from '@/api/business/contract-change';
import { repush as hcsRepush, repushBatch as hcsRepushBatch, getRisk } from '@/api/business/hcs';
import { isContractBlack, isRiskyTag, resolveRiskTagName } from '@/utils/risk-display';
import { Position } from '@element-plus/icons-vue';
import { getMkPublicDetail } from '@/api/mk-process';
import { getList as getProjectList } from '@/api/business/project-apply';
@@ -1623,7 +1636,7 @@ export default {
// 详情风险客商提示(基本信息备注后展示):按推送留痕 hcs_risk_level/hcs_black_status 判定(甲乙取高/任一),文案与后端 riskTip 一致
detailRiskTipText() {
const riskLevel = String(this.detailRow.hcsRiskLevel || '').trim();
const blacklist = String(this.detailRow.hcsBlackStatus || '').toLowerCase() === 'yes';
const blacklist = String(this.detailRow.kingdeeBlackStatus || '').toLowerCase() === 'yes';
if (riskLevel !== '2' && !blacklist) return '';
const parties = [this.detailRow.partyA, this.detailRow.partyB]
.map(name => String(name || '').trim())
@@ -1756,6 +1769,13 @@ export default {
hcsSyncLabel(value) {
return { unsynced: '待推送', synced: '已推送', failed: '推送失败' }[value] || value;
},
// ==================== 客商风险(推送链路按金蝶黑名单命中口径落库,展示复用公共方法) ====================
contractRiskTagName(row) {
return resolveRiskTagName(row.hcsRiskLevel, isContractBlack(row.kingdeeBlackStatus));
},
isContractRisky(row) {
return isRiskyTag(row.hcsRiskLevel, isContractBlack(row.kingdeeBlackStatus));
},
hcsSyncTagType(value) {
return { unsynced: 'info', synced: 'success', failed: 'danger' }[value] || 'info';
},
@@ -293,6 +293,12 @@
<el-input v-model="row.remark" :disabled="readonly" maxlength="200" />
</template>
</el-table-column>
<el-table-column v-if="readonly" label="开票状态" min-width="90" align="center">
<template #default="{ row }">{{ sheet.invoiceStatusName || '未开票' }}</template>
</el-table-column>
<el-table-column v-if="readonly" label="金蝶开票结果备注" min-width="200">
<template #default="{ row }">{{ sheet.kingdeeResultRemark }}</template>
</el-table-column>
<el-table-column v-if="!readonly" label="操作" width="110" fixed="right" align="center">
<template #default="{ $index }">
<div class="invoice-form-page__links">
@@ -708,7 +708,9 @@
:rows="changeRecordDetailRows"
/>
<template v-if="!pageMode" #footer>
<el-button v-if="editable" @click="handleSyncPlaceholder">同步</el-button>
<el-button v-if="canSync" type="primary" plain :loading="syncLoading" @click="handleSync"
>同步</el-button
>
<el-button @click="visible = false">取消</el-button>
<el-button
v-if="editable"
@@ -724,7 +726,9 @@
>
</template>
<div v-if="pageMode && !publicMode" class="formal-editor__page-actions">
<el-button v-if="editable" @click="handleSyncPlaceholder">同步</el-button>
<el-button v-if="canSync" type="primary" plain :loading="syncLoading" @click="handleSync"
>同步</el-button
>
<el-button @click="visible = false">取消</el-button>
<el-button
v-if="editable"
@@ -1051,6 +1055,7 @@ import {
claimInvoices as claimInvoicesApi,
queryInvoicePool,
save,
syncKingdee,
} from '@/api/settlement/formalSettlement';
import {
getDetail as getPreSettlementDetail,
@@ -1128,6 +1133,7 @@ export default {
loading: false,
draftSaving: false,
saving: false,
syncLoading: false,
form: createFormalSettlementForm(),
sources: [],
details: [],
@@ -1271,6 +1277,16 @@ export default {
editable() {
return !this.readonly;
},
// 查看页面「同步」按钮:仅已审批且未同步金蝶(含推送失败)的单据显示,口径与后端推送闸门一致
canSync() {
return (
this.readonly &&
!this.publicMode &&
this.hasPermission('sync') &&
this.form.approvalStatus === 'approved' &&
this.form.kingdeeSyncStatus !== 'synced'
);
},
isAdmin() {
return isAdministrator(this.userInfo);
},
@@ -2887,8 +2903,30 @@ export default {
remark: this.form.remark,
};
},
handleSyncPlaceholder() {
this.$message.info('开发中');
// 查看页面单张推送金蝶(A102-2/A302-2):结果与异常原因弹框展示,成败后均刷新详情与列表
async handleSync() {
if (this.syncLoading) return;
this.syncLoading = true;
try {
const res = await syncKingdee(this.form.id, { meta: { suppressErrorMessage: true } });
await this.loadFormalDetail(this.recordId);
this.$emit('success');
let message = `同步成功:${this.unwrapData(res) || '-'}`;
if (['pending', 'partial'].includes(this.form.attachmentSyncStatus)) {
message += '\n该单附件未全部上传,可重推补传';
}
await this.$alert(message, '同步结果', { type: 'success', confirmButtonText: '知道了' });
} catch (error) {
const reason = String(error?.message || error || '未知错误').trim() || '未知错误';
await this.loadFormalDetail(this.recordId);
this.$emit('success');
await this.$alert(`同步失败,${reason}`, '同步结果', {
type: 'error',
confirmButtonText: '知道了',
});
} finally {
this.syncLoading = false;
}
},
async handleDraftSave() {
// 保存草稿:不做表单校验,直接落草稿并返回列表刷新
+56 -37
View File
@@ -49,24 +49,24 @@
</template>
<template #customerRisk="{ row }">
<el-tooltip
v-if="customerRiskMap[row.customerCode] && customerRiskMap[row.customerCode].risky"
v-if="customerRowRisky(row)"
placement="top"
effect="dark"
popper-class="customer-risk-popper"
>
<template #content>
<div>{{ customerRiskMap[row.customerCode].riskTip || '该客商为高风险/黑名单客商' }}</div>
<div>{{ row.hcsRiskDetail || '该客商为高风险/黑名单客商' }}</div>
</template>
<el-tag type="danger" class="status-text">
{{ customerRiskLabel(customerRiskMap[row.customerCode]) }}
{{ customerRowRiskName(row) }}
</el-tag>
</el-tooltip>
<el-tag
v-else-if="customerRiskMap[row.customerCode]"
:type="customerRiskMap[row.customerCode].fdRiskLevel === '1' ? 'warning' : 'success'"
v-else-if="customerRowRiskName(row)"
:type="row.hcsRiskLevel === '1' ? 'warning' : 'success'"
class="status-text"
>
{{ customerRiskLabel(customerRiskMap[row.customerCode]) }}
{{ customerRowRiskName(row) }}
</el-tag>
<span v-else>-</span>
</template>
@@ -372,7 +372,7 @@
<template #content>
<div>{{ archiveNameRiskTip || '该客商为高风险/黑名单客商' }}</div>
</template>
<div class="archive-name-risk">高风险/黑名单客商</div>
<div class="archive-name-risk">{{ archiveNameRiskLabel }}</div>
</el-tooltip>
</el-form-item>
</el-col>
@@ -1597,7 +1597,8 @@ import {
queryKingdeeCustomer,
syncKingdee,
} from '@/api/vehicle/customer-archive';
import { getRiskBatch, getRiskByName } from '@/api/business/hcs';
import { getRisk, getRiskByName } from '@/api/business/hcs';
import { isKingdeeBlack, isRiskyTag, resolveRiskTagName } from '@/utils/risk-display';
import {
getList as getCreditScoreQuantificationList,
getDetail as getCreditScoreQuantificationDetail,
@@ -1699,9 +1700,9 @@ export default {
deptSearchInitialized: false,
loading: true,
data: [],
customerRiskMap: {},
archiveNameRisky: false,
archiveNameRiskTip: '',
archiveNameRiskLabel: '',
page: {
pageSize: 10,
pageSizes: [10, 20, 50, 100],
@@ -1963,7 +1964,13 @@ export default {
label: '客商风险',
prop: 'customerRisk',
slot: true,
minWidth: 100,
search: true,
searchType: 'select',
searchOrder: 4,
searchPlaceholder: '请选择',
minWidth: 150,
// 选项走业务字典 customer_risk(initBusinessDictionaries 加载:dictKey=筛选值,dictValue=名称),未配置时下拉为空
dicData: [],
},
{
label: '客户性质',
@@ -3157,6 +3164,15 @@ export default {
});
},
initBusinessDictionaries() {
// 客商风险筛选项走业务字典 customer_risk(dictKey=筛选值 blacklist/high/middle/low,dictValue=名称),字典缺失保留固定兜底项
getDictionary({ code: 'customer_risk' }).then(res => {
const options = (res.data.data || [])
.filter(item => item.dictKey !== '-1')
.map(item => ({ label: item.dictValue, value: item.dictKey }));
if (options.length) {
this.findColumn(this.option.column, 'customerRisk').dicData = options;
}
});
this.loadDictOptions('nature_of_client', 'customerNatureOptions', [], options => {
const customerNatureColumn = this.findColumn(this.option.column, 'customerNature');
customerNatureColumn.dicData = [{ label: '全部', value: '' }, ...options];
@@ -5505,7 +5521,9 @@ export default {
this.onLoad(this.page);
},
searchChange(params, done) {
this.query = this.buildQuery(params);
// Avue 只回传非空筛选项,被清空的字段不会出现在 params 里,
// 因此查询条件必须整体重建,不能与上一次的 this.query 合并,否则清空后仍会带上次的值
this.query = this.buildQuery(params, true);
this.page.currentPage = 1;
this.onLoad(this.page, this.query);
done();
@@ -5534,52 +5552,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 ? '风险客商' : '正常');
// 黑名单判定:仅以金蝶黑名单(A801-5)命中为准(失信类别/列入事由任一非空);C101-1 的 hcsBlackStatus 不作为黑名单依据
customerRowIsBlack(row) {
return isKingdeeBlack(row.kingdeeBlackCategory, row.kingdeeBlackReason);
},
// 客商风险名称(每日定时刷新落库字段):等级与黑名单组合展示(如"低风险、黑名单");内部组织默认无风险;未查询显示"-"
customerRowRiskName(row) {
return resolveRiskTagName(row.hcsRiskLevel, this.customerRowIsBlack(row), row.customerKind);
},
// 高风险/黑名单判定(与列表筛选口径一致,命中显示红色标签)
customerRowRisky(row) {
return isRiskyTag(row.hcsRiskLevel, this.customerRowIsBlack(row));
},
// 客商名称风险校验(C101-1,仅展示不作为安全边界):名称失焦/回显时查询,
// risky 在名称框下展示红色提示,悬浮见风险项明细;查询失败静默不阻断填写,提交链路由后端强校验兜底
checkArchiveNameRisk() {
this.archiveNameRisky = false;
this.archiveNameRiskTip = '';
this.archiveNameRiskLabel = '';
const customerName = String(this.archiveForm.fullName || '').trim();
if (!customerName) return;
getRiskByName(customerName)
// 新增客商档案本地反查不到编号:金蝶带出后表单已有客商编号,优先按编号查 C101-1,
// 并传信用代码供金蝶黑名单(A801-5)兜底;无编号时按名称反查档案
const customerCode = String(this.archiveForm.customerCode || '').trim();
const request = customerCode
? getRisk(customerCode, String(this.archiveForm.unifiedCreditCode || '').trim())
: getRiskByName(customerName);
request
.then(res => {
const risk = (res.data && res.data.data) || {};
this.archiveNameRisky = risk.risky === true;
this.archiveNameRiskTip = risk.riskTip || '';
// 红字与列表标签同口径(risk-display):等级按合同系统返回值翻译,黑名单按金蝶命中标记独立展示,组合不分家
this.archiveNameRiskLabel = this.archiveNameRisky
? (resolveRiskTagName(risk.fdRiskLevel, risk.kingdeeBlackHit === true) || '高风险/黑名单') + '客商'
: '';
})
.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: '确定',
@@ -5607,9 +5625,10 @@ export default {
[this.website.tokenHeader]: getToken(),
};
},
buildQuery(params = {}) {
// 新条件覆盖旧条件;本次显式清空的字段必须从查询里去掉,避免第二次搜索仍带着上一次的客商名称。
const merged = { ...this.query, ...params };
// replace=true 表示以本次搜索条件为准整体重建(清空字段不会回传,需丢弃旧条件);
// 翻页、刷新等场景传 false,沿用上一次的查询条件
buildQuery(params = {}, replace = false) {
const merged = replace ? { ...params } : { ...this.query, ...params };
Object.keys(params).forEach(key => {
const value = params[key];
if (