This commit is contained in:
2025-12-16 12:27:12 +08:00
parent 89fd3c8bd9
commit d576d8e734
38 changed files with 6175 additions and 5168 deletions

View File

@@ -170,7 +170,7 @@ const featureMap = {
name: "关联风险监督",
component: defineAsyncComponent(() => import("@/ui/DWBG6A2C/components/RiskSupervisionSection.vue")),
},
// 司法涉诉
FLXG0V4B: {
name: "司法涉诉",
@@ -178,7 +178,7 @@ const featureMap = {
import("@/ui/CFLXG0V4B/index.vue")
),
},
// 个人涉诉
FLXG7E8F: {
name: "个人涉诉",
@@ -187,7 +187,7 @@ const featureMap = {
),
remark: '个人涉诉风险展示申请人相关的诉讼情况,包括民事诉讼、刑事诉讼、行政诉讼、执行案件、失信被执行人、限制消费等。数据来源于各级法院的公开判决书和法官网等权威渠道。'
},
// 人企关系加强版
QYGL3F8E: {
name: "人企关系加强版",
@@ -229,69 +229,83 @@ const featureMap = {
name: "税务风险",
component: defineAsyncComponent(() => import("@/ui/CQYGL3F8E/components/TaxRisk/index.vue")),
},
// 信贷表现
JRZQ4B6C: {
name: "信贷表现",
component: defineAsyncComponent(() => import("@/ui/JRZQ4B6C/index.vue")),
remark: '信贷表现主要为企业在背景调查过程中探查用户近期信贷表现时提供参考,帮助企业对其内部员工、外部业务进行个人信用过滤。数据来源于多个征信机构,可能存在数据延迟或不完整的情况。'
},
// 收入评估
JRZQ09J8: {
name: "收入评估",
component: defineAsyncComponent(() => import("@/ui/JRZQ09J8/index.vue")),
remark: '基于全国社会保险信息系统的缴费基数数据进行收入水平评估。评级反映相对收入水平,实际收入可能因地区差异而有所不同,建议结合其他收入证明材料进行综合评估。'
},
// 个人消费能力等级
JRZQ8B3C: {
name: "个人消费能力等级",
component: defineAsyncComponent(() => import("@/ui/JRZQ8B3C/index.vue")),
remark: '基于个人收入指数进行消费能力等级评估,展示用户的月消费能力范围。消费能力等级反映用户的消费水平,等级越高对应的月消费能力越强。数据来源于个人收入指数评分,实际消费能力可能因个人消费习惯、地区差异等因素而有所不同,建议结合其他消费行为数据进行综合评估。'
},
// 名下车辆
QCXG9P1C: {
name: "名下车辆",
component: defineAsyncComponent(() => import("@/ui/CQCXG9P1C.vue")),
},
// 网络社交异常
IVYZ8I9J: {
name: "网络社交异常",
component: defineAsyncComponent(() => import("@/ui/IVYZ8I9J.vue")),
remark: '网络社交异常通过分析用户在互联网上的行为特征,检测资料包装中介、异常行业、虚假资料、羊毛党、身份信息存疑、严重异常行为、失信行为、支付异常行为、其他异常行为和上网环境异常等多维度风险。'
},
// 多头借贷行业风险版
DWBG7F3A: {
name: "多头借贷行业风险版",
component: defineAsyncComponent(() => import("@/ui/DWBG7F3A/index.vue")),
remark: '多头借贷行业风险版提供全面的多头借贷风险评估,包括多头共债子分、多头申请、多头逾期、圈团风险和可疑欺诈风险等多维度分析。'
},
// 特殊名单验证B
JRZQ8A2D: {
name: "特殊名单验证",
component: defineAsyncComponent(() => import("@/ui/JRZQ8A2D.vue")),
remark: '特殊名单验证用于验证个人在各类金融机构和法院系统的信用状况,包括法院失信、银行风险、非银机构风险等多个维度。'
},
// 全景雷达
JRZQ7F1A: {
name: "全景雷达",
component: defineAsyncComponent(() => import("@/ui/JRZQ7F1A/index.vue")),
remark: '全景雷达提供全面的信用评估,包括申请行为详情、放款还款详情和信用详情。通过多维度的数据分析,全面展示申请人的信用状况和借贷行为。'
},
// 借贷意向验证A
JRZQ6F2A: {
name: "借贷意向验证",
component: defineAsyncComponent(() => import("@/ui/JRZQ6F2A/index.vue")),
remark: '借贷意向验证提供全面的借贷申请行为分析,包括申请次数、申请总次数(银行+非银)和申请机构总数(银行+非银)等多维度数据。通过不同时间段的统计分析,全面展示申请人的借贷申请行为。'
},
// 手机携号转网
YYSY7D3E: {
name: "手机携号转网",
component: defineAsyncComponent(() => import("@/ui/YYSY7D3E/index.vue")),
remark: '手机携号转网查询用于检测用户手机号码是否发生过携号转网操作,以及转网前后的运营商信息。携号转网可能影响用户身份验证和信用评估。'
},
// 手机在网时长
YYSY8B1C: {
name: "手机在网时长",
component: defineAsyncComponent(() => import("@/ui/YYSY8B1C/index.vue")),
remark: '手机在网时长查询用于检测用户手机号码的在网使用时长。在网时长越长,通常表示用户身份越稳定,信用风险越低。需要注意的是,如果手机号码存在携号转网的情况,那么在网时长会从转网的时候重新计算,转网前的在网时长不计入当前在网时长。建议结合手机携号转网查询结果进行综合评估。'
},
// 谛听多维报告
DWBG8B4D: {
name: "谛听多维报告",
@@ -414,6 +428,7 @@ const featureRiskLevels = {
'IVYZ8I9J': 7, // 网络社交异常
'JRZQ8A2D': 9, // 特殊名单验证
'JRZQ7F1A': 8, // 全景雷达
'JRZQ6F2A': 7, // 借贷意向验证A
'YYSY7D3E': 5, // 手机携号转网
'YYSY8B1C': 5, // 手机在网时长
@@ -421,6 +436,7 @@ const featureRiskLevels = {
'QYGL3F8E': 5, // 人企关系加强版
'QCXG9P1C': 5, // 名下车辆
'JRZQ09J8': 5, // 收入评估
'JRZQ8B3C': 5, // 个人消费能力等级
// 📊 复合报告类 - 按子模块动态计算
'DWBG8B4D': 0, // 谛听多维报告(由子模块计算)
@@ -679,6 +695,7 @@ watch([reportData, componentRiskScores], () => {
@apply p-3;
box-shadow: 0px 0px 24px 0px #3F3F3F0F;
}
/* 梯形背景图片样式 */
.trapezoid-bg-image {
background-size: contain;

BIN
src/ui/CFLXG0V4B.zip Normal file

Binary file not shown.

View File

@@ -1,303 +1,337 @@
// 案件类型映射表
export const lawsuitTypeMap = {
breachCase: {
text: '失信被执行',
color: 'text-red-600 bg-red-50',
darkColor: 'bg-red-500',
riskLevel: 'high', // 高风险
},
consumptionRestriction: {
text: '限高被执行',
color: 'text-orange-600 bg-orange-50',
darkColor: 'bg-orange-500',
riskLevel: 'high', // 高风险
},
criminal: {
text: '刑事案件',
color: 'text-red-600 bg-red-50',
darkColor: 'bg-red-500',
riskLevel: 'high', // 高风险
},
civil: {
text: '民事案件',
color: 'text-blue-600 bg-blue-50',
darkColor: 'bg-blue-500',
riskLevel: 'medium', // 中风险
},
administrative: {
text: '行政案件',
color: 'text-purple-600 bg-purple-50',
darkColor: 'bg-purple-500',
riskLevel: 'medium', // 中风险
},
implement: {
text: '执行案件',
color: 'text-orange-600 bg-orange-50',
darkColor: 'bg-orange-500',
riskLevel: 'medium', // 中风险
},
bankrupt: {
text: '强制清算与破产案件',
color: 'text-rose-600 bg-rose-50',
darkColor: 'bg-rose-500',
riskLevel: 'high', // 高风险
},
preservation: {
text: '非诉保全审查',
color: 'text-amber-600 bg-amber-50',
darkColor: 'bg-amber-500',
riskLevel: 'low', // 低风险
},
}
breachCase: {
text: "失信被执行",
color: "text-red-600 bg-red-50",
darkColor: "bg-red-500",
riskLevel: "high", // 高风险
},
consumptionRestriction: {
text: "限高被执行",
color: "text-orange-600 bg-orange-50",
darkColor: "bg-orange-500",
riskLevel: "high", // 高风险
},
criminal: {
text: "刑事案件",
color: "text-red-600 bg-red-50",
darkColor: "bg-red-500",
riskLevel: "high", // 高风险
},
civil: {
text: "民事案件",
color: "text-blue-600 bg-blue-50",
darkColor: "bg-blue-500",
riskLevel: "medium", // 中风险
},
administrative: {
text: "行政案件",
color: "text-purple-600 bg-purple-50",
darkColor: "bg-purple-500",
riskLevel: "medium", // 中风险
},
implement: {
text: "执行案件",
color: "text-orange-600 bg-orange-50",
darkColor: "bg-orange-500",
riskLevel: "medium", // 中风险
},
bankrupt: {
text: "强制清算与破产案件",
color: "text-rose-600 bg-rose-50",
darkColor: "bg-rose-500",
riskLevel: "high", // 高风险
},
preservation: {
text: "非诉保全审查",
color: "text-amber-600 bg-amber-50",
darkColor: "bg-amber-500",
riskLevel: "low", // 低风险
},
};
// 案件类型文本
export const getCaseTypeText = type => {
return lawsuitTypeMap[type]?.text || '其他案件'
}
export const getCaseTypeText = (type) => {
return lawsuitTypeMap[type]?.text || "其他案件";
};
// 案件类型颜色
export const getCaseTypeColor = type => {
return lawsuitTypeMap[type]?.color || 'text-gray-600 bg-gray-50'
}
export const getCaseTypeColor = (type) => {
return lawsuitTypeMap[type]?.color || "text-gray-600 bg-gray-50";
};
// 案件类型深色
export const getCaseTypeDarkColor = type => {
return lawsuitTypeMap[type]?.darkColor || 'bg-gray-500'
}
export const getCaseTypeDarkColor = (type) => {
return lawsuitTypeMap[type]?.darkColor || "bg-gray-500";
};
// 格式化日期显示
export const formatDate = dateStr => {
if (!dateStr) return '—'
// 转换YYYY-MM-DD为年月日格式
if (dateStr.includes('-')) {
const parts = dateStr.split('-')
if (parts.length === 3) {
return `${parts[0]}${parts[1]}${parts[2]}`
export const formatDate = (dateStr) => {
if (!dateStr) return "—";
// 转换YYYY-MM-DD为年月日格式
if (dateStr.includes("-")) {
const parts = dateStr.split("-");
if (parts.length === 3) {
return `${parts[0]}${parts[1]}${parts[2]}`;
}
}
}
return dateStr // 如果不是标准格式则返回原始字符串
}
return dateStr; // 如果不是标准格式则返回原始字符串
};
// 格式化金额显示(单位:元)
export const formatLawsuitMoney = money => {
if (!money) return '—'
// 格式化金额显示(默认单位:元)
export const formatLawsuitMoney = (money) => {
if (!money) return "—";
const value = parseFloat(money)
if (isNaN(value)) return '—'
const value = parseFloat(money);
if (isNaN(value)) return "—";
// 超过1亿显示亿元
if (value >= 10000) {
// 超过1亿100000000元显示亿元
if (value >= 100000000) {
return (
(value / 100000000).toLocaleString("zh-CN", {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
}) + " 亿元"
);
}
// 超过1万10000元显示万元
if (value >= 10000) {
return (
(value / 10000).toLocaleString("zh-CN", {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
}) + " 万元"
);
}
// 小于1万直接显示元
return (
(value / 10000).toLocaleString('zh-CN', {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
}) + ' 亿元'
)
}
// 否则显示万元
return (
value.toLocaleString('zh-CN', {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
}) + ' 万元'
)
}
value.toLocaleString("zh-CN", {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
}) + " 元"
);
};
// 获取案件状态样式
export const getCaseStatusClass = status => {
if (!status) return 'bg-gray-100 text-gray-500'
export const getCaseStatusClass = (status) => {
if (!status) return "bg-gray-100 text-gray-500";
if (status.includes('已结') || status.includes('已办结')) {
return 'bg-green-50 text-green-600'
} else if (status.includes('执行中') || status.includes('审理中')) {
return 'bg-blue-50 text-blue-600'
} else if (status.includes('未执行')) {
return 'bg-amber-50 text-amber-600'
} else {
return 'bg-gray-100 text-gray-500'
}
}
if (status.includes("已结") || status.includes("已办结")) {
return "bg-green-50 text-green-600";
} else if (status.includes("执行中") || status.includes("审理中")) {
return "bg-blue-50 text-blue-600";
} else if (status.includes("未执行")) {
return "bg-amber-50 text-amber-600";
} else {
return "bg-gray-100 text-gray-500";
}
};
// 获取企业状态对应的样式
export const getStatusClass = status => {
if (!status) return 'bg-gray-100 text-gray-500'
export const getStatusClass = (status) => {
if (!status) return "bg-gray-100 text-gray-500";
if (status.includes('注销') || status.includes('吊销')) {
return 'bg-red-50 text-red-600'
} else if (status.includes('存续') || status.includes('在营')) {
return 'bg-green-50 text-green-600'
} else if (status.includes('筹建') || status.includes('新设')) {
return 'bg-blue-50 text-blue-600'
} else {
return 'bg-yellow-50 text-yellow-600'
}
}
if (status.includes("注销") || status.includes("吊销")) {
return "bg-red-50 text-red-600";
} else if (status.includes("存续") || status.includes("在营")) {
return "bg-green-50 text-green-600";
} else if (status.includes("筹建") || status.includes("新设")) {
return "bg-blue-50 text-blue-600";
} else {
return "bg-yellow-50 text-yellow-600";
}
};
// 格式化资本金额显示
export const formatCapital = (capital, currency) => {
if (!capital) return '—'
if (!capital) return "—";
// 检查是否包含"万"字或需要显示为万元
let unit = ''
let value = parseFloat(capital)
// 检查是否包含"万"字或需要显示为万元
let unit = "";
let value = parseFloat(capital);
// 处理原始数据中可能带有的单位
if (typeof capital === 'string' && capital.includes('万')) {
unit = '万'
// 提取数字部分
const numMatch = capital.match(/[\d.]+/)
value = numMatch ? parseFloat(numMatch[0]) : 0
} else if (value >= 10000) {
// 大额数字转换为万元显示
value = value / 10000
unit = '万'
}
// 处理原始数据中可能带有的单位
if (typeof capital === "string" && capital.includes("万")) {
unit = "万";
// 提取数字部分
const numMatch = capital.match(/[\d.]+/);
value = numMatch ? parseFloat(numMatch[0]) : 0;
} else if (value >= 10000) {
// 大额数字转换为万元显示
value = value / 10000;
unit = "万";
}
// 格式化数字,保留两位小数(如果有小数部分)
const formattedValue = value.toLocaleString('zh-CN', {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
})
// 格式化数字,保留两位小数(如果有小数部分)
const formattedValue = value.toLocaleString("zh-CN", {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
});
return `${formattedValue}${unit} ${currency || '人民币'}`
}
return `${formattedValue}${unit} ${currency || "人民币"}`;
};
// 获取涉诉风险等级
export const getRiskLevel = lawsuitInfo => {
if (!lawsuitInfo) {
export const getRiskLevel = (lawsuitInfo) => {
if (!lawsuitInfo) {
return {
level: "low",
text: "低风险",
color: "text-green-600 bg-green-50",
};
}
// 失信被执行人是最高风险
if (lawsuitInfo.breachCaseList && lawsuitInfo.breachCaseList.length > 0) {
return {
level: "high",
text: "高风险",
color: "text-red-600 bg-red-50",
};
}
// 限高被执行人是最高风险
if (
lawsuitInfo.consumptionRestrictionList &&
lawsuitInfo.consumptionRestrictionList.length > 0
) {
return {
level: "high",
text: "高风险",
color: "text-red-600 bg-red-50",
};
}
// 有涉诉数据的风险级别
if (
lawsuitInfo.lawsuitStat &&
Object.keys(lawsuitInfo.lawsuitStat).length > 0
) {
// 检查是否有未结案的案件
const data = lawsuitInfo.lawsuitStat;
if (
data.count &&
data.count.count_wei_total &&
data.count.count_wei_total > 0
) {
return {
level: "medium",
text: "中风险",
color: "text-amber-600 bg-amber-50",
};
}
// 只有已结案的为低中风险
return {
level: "low-medium",
text: "低中风险",
color: "text-yellow-600 bg-yellow-50",
};
}
return {
level: 'low',
text: '低风险',
color: 'text-green-600 bg-green-50',
}
}
// 失信被执行人是最高风险
if (lawsuitInfo.breachCaseList && lawsuitInfo.breachCaseList.length > 0) {
return {
level: 'high',
text: '高风险',
color: 'text-red-600 bg-red-50',
}
}
// 限高被执行人是最高风险
if (lawsuitInfo.consumptionRestrictionList && lawsuitInfo.consumptionRestrictionList.length > 0) {
return {
level: 'high',
text: '高风险',
color: 'text-red-600 bg-red-50',
}
}
// 有涉诉数据的风险级别
if (lawsuitInfo.lawsuitStat && Object.keys(lawsuitInfo.lawsuitStat).length > 0) {
// 检查是否有未结案的案件
const data = lawsuitInfo.lawsuitStat
if (data.count && data.count.count_wei_total && data.count.count_wei_total > 0) {
return {
level: 'medium',
text: '中风险',
color: 'text-amber-600 bg-amber-50',
}
}
// 只有已结案的为低中风险
return {
level: 'low-medium',
text: '低中风险',
color: 'text-yellow-600 bg-yellow-50',
}
}
return {
level: 'low',
text: '低风险',
color: 'text-green-600 bg-green-50',
}
}
level: "low",
text: "低风险",
color: "text-green-600 bg-green-50",
};
};
// 获取涉诉案件统计
export const getLawsuitStats = lawsuitInfo => {
if (!lawsuitInfo) return null
export const getLawsuitStats = (lawsuitInfo) => {
if (!lawsuitInfo) return null;
const stats = {
total: 0,
types: [],
}
const stats = {
total: 0,
types: [],
};
// 统计各类型案件数量
Object.keys(lawsuitTypeMap).forEach(type => {
let count = 0
// 统计各类型案件数量
Object.keys(lawsuitTypeMap).forEach((type) => {
let count = 0;
if (type === 'breachCase') {
count = lawsuitInfo.breachCaseList && lawsuitInfo.breachCaseList.length > 0 ? lawsuitInfo.breachCaseList.length : 0
} else if (type === 'consumptionRestriction') {
count = lawsuitInfo.consumptionRestrictionList && lawsuitInfo.consumptionRestrictionList.length > 0 ? lawsuitInfo.consumptionRestrictionList.length : 0
} else if (lawsuitInfo.lawsuitStat && lawsuitInfo.lawsuitStat[type] && Object.keys(lawsuitInfo.lawsuitStat[type]).length > 0) {
const typeData = lawsuitInfo.lawsuitStat[type]
count = typeData.cases && typeData.cases.length ? typeData.cases.length : 0
}
if (type === "breachCase") {
count =
lawsuitInfo.breachCaseList &&
lawsuitInfo.breachCaseList.length > 0
? lawsuitInfo.breachCaseList.length
: 0;
} else if (type === "consumptionRestriction") {
count =
lawsuitInfo.consumptionRestrictionList &&
lawsuitInfo.consumptionRestrictionList.length > 0
? lawsuitInfo.consumptionRestrictionList.length
: 0;
} else if (
lawsuitInfo.lawsuitStat &&
lawsuitInfo.lawsuitStat[type] &&
Object.keys(lawsuitInfo.lawsuitStat[type]).length > 0
) {
const typeData = lawsuitInfo.lawsuitStat[type];
count =
typeData.cases && typeData.cases.length
? typeData.cases.length
: 0;
}
if (count > 0) {
stats.total += count
stats.types.push({
type,
count,
name: getCaseTypeText(type),
color: getCaseTypeColor(type),
darkColor: getCaseTypeDarkColor(type),
})
}
})
if (count > 0) {
stats.total += count;
stats.types.push({
type,
count,
name: getCaseTypeText(type),
color: getCaseTypeColor(type),
darkColor: getCaseTypeDarkColor(type),
});
}
});
return stats
}
return stats;
};
// 获取案件类型优先级顺序
export const getCaseTypePriority = () => {
return [
'breachCase', // 失信被执行人(最高风险)
'consumptionRestriction', // 限高被执行人
'criminal', // 刑事案件
'civil', // 民事案件
'administrative', // 行政案件
'implement', // 执行案件
'bankrupt', // 强制清算与破产案件
'preservation', // 非诉保全审查
]
}
return [
"breachCase", // 失信被执行人(最高风险)
"consumptionRestriction", // 限高被执行人
"criminal", // 刑事案件
"civil", // 民事案件
"administrative", // 行政案件
"implement", // 执行案件
"bankrupt", // 强制清算与破产案件
"preservation", // 非诉保全审查
];
};
// 根据案件类型获取风险等级
export const getCaseTypeRiskLevel = caseType => {
const typeInfo = lawsuitTypeMap[caseType]
if (!typeInfo) {
return {
level: 'low',
text: '低风险',
color: 'text-green-600 bg-green-50',
export const getCaseTypeRiskLevel = (caseType) => {
const typeInfo = lawsuitTypeMap[caseType];
if (!typeInfo) {
return {
level: "low",
text: "低风险",
color: "text-green-600 bg-green-50",
};
}
}
const riskLevelMap = {
high: {
text: '高风险',
color: 'text-red-600 bg-red-50',
},
medium: {
text: '中风险',
color: 'text-amber-600 bg-amber-50',
},
low: {
text: '低风险',
color: 'text-green-600 bg-green-50',
},
}
return {
level: typeInfo.riskLevel,
...riskLevelMap[typeInfo.riskLevel],
}
}
const riskLevelMap = {
high: {
text: "高风险",
color: "text-red-600 bg-red-50",
},
medium: {
text: "中风险",
color: "text-amber-600 bg-amber-50",
},
low: {
text: "低风险",
color: "text-green-600 bg-green-50",
},
};
return {
level: typeInfo.riskLevel,
...riskLevelMap[typeInfo.riskLevel],
};
};

View File

@@ -0,0 +1,125 @@
<template>
<div class="card application-count-section">
<div class="rounded-lg border border-gray-200 pb-2 mb-4">
<div class="flex items-center mb-4 p-4">
<span class="font-bold text-gray-800 text-lg">申请次数 {{ totalCount }}</span>
</div>
<!-- 柱状图 -->
<div class="px-4 mb-4">
<div class="h-64">
<v-chart class="chart-container" :option="chartOption" autoresize />
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import VChart from 'vue-echarts'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { BarChart } from 'echarts/charts'
import {
TitleComponent,
TooltipComponent,
LegendComponent,
GridComponent
} from 'echarts/components'
import { getApplicationCounts, PERIOD_MAP } from '../utils/dataParser'
// 注册ECharts组件
use([
CanvasRenderer,
BarChart,
TitleComponent,
TooltipComponent,
LegendComponent,
GridComponent
])
const props = defineProps({
data: {
type: Object,
required: true,
default: () => ({})
}
})
// 计算总申请次数12个月
const totalCount = computed(() => {
const counts = getApplicationCounts(props.data, 'm12')
return counts.total
})
// 图表配置
const chartOption = computed(() => {
const periodKeys = ['d7', 'd15', 'm1', 'm3', 'm6', 'm12']
const labels = periodKeys.map(key => PERIOD_MAP[key].label)
const data = periodKeys.map(key => {
const counts = getApplicationCounts(props.data, key)
return counts.total
})
return {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
formatter: (params) => {
const param = params[0]
return `${param.name}<br/>${param.seriesName}: ${param.value}`
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: labels,
axisLabel: {
fontSize: 12,
color: '#666'
}
},
yAxis: {
type: 'value',
axisLabel: {
fontSize: 12,
color: '#666',
formatter: '{value}次'
}
},
series: [
{
name: '申请次数',
type: 'bar',
data: data,
itemStyle: {
color: '#4A90E2'
},
label: {
show: true,
position: 'top',
formatter: '{c}次',
fontSize: 12,
color: '#333'
}
}
]
}
})
</script>
<style scoped>
.card {
background: #ffffff;
}
</style>

View File

@@ -0,0 +1,64 @@
<template>
<div class="card application-total-section">
<div class="rounded-lg border border-gray-200 pb-2 mb-4">
<div class="flex items-center mb-4 p-4">
<span class="font-bold text-gray-800 text-lg">申请总次数 (银行+非银) {{ totalCount }}</span>
</div>
<!-- Tab切换 -->
<div class="px-4 mb-4">
<van-tabs v-model:active="activeTab" color="var(--color-primary)">
<van-tab v-for="(period, index) in periods" :key="period.key" :title="period.label">
<div class="p-4">
<!-- 银行机构 -->
<BankInstitutionSection :data="data" :period="period.key" />
<!-- 非银机构 -->
<NBankInstitutionSection :data="data" :period="period.key" />
</div>
</van-tab>
</van-tabs>
</div>
</div>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
import { getApplicationCounts, PERIOD_MAP } from '../utils/dataParser'
import BankInstitutionSection from './BankInstitutionSection.vue'
import NBankInstitutionSection from './NBankInstitutionSection.vue'
const props = defineProps({
data: {
type: Object,
required: true,
default: () => ({})
}
})
const activeTab = ref(5) // 默认显示12个月
const periods = [
{ key: 'd7', label: '7天' },
{ key: 'd15', label: '15天' },
{ key: 'm1', label: '1个月' },
{ key: 'm3', label: '3个月' },
{ key: 'm6', label: '6个月' },
{ key: 'm12', label: '12个月' }
]
// 计算总申请次数12个月
const totalCount = computed(() => {
const counts = getApplicationCounts(props.data, 'm12')
return counts.total
})
</script>
<style scoped>
.card {
background: #ffffff;
}
</style>

View File

@@ -0,0 +1,158 @@
<template>
<div class="bank-institution-section mb-6">
<div class="flex items-center mb-4">
<span class="font-bold text-gray-800">银行机构 {{ bankTotal }}</span>
</div>
<div class="grid grid-cols-2 gap-4">
<!-- 饼图 -->
<div class="h-64">
<v-chart class="chart-container" :option="pieChartOption" autoresize />
</div>
<!-- 详细列表 -->
<div class="space-y-2">
<div
v-for="(item, index) in detailList"
:key="index"
class="flex justify-between items-center text-sm py-1 border-b border-gray-100"
>
<span class="text-gray-600">{{ item.label }}</span>
<span class="text-[#333333] font-bold">{{ item.value }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import VChart from 'vue-echarts'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { PieChart } from 'echarts/charts'
import {
TitleComponent,
TooltipComponent,
LegendComponent
} from 'echarts/components'
import { getBankApplicationDetails, FIELD_LABELS } from '../utils/dataParser'
// 注册ECharts组件
use([
CanvasRenderer,
PieChart,
TitleComponent,
TooltipComponent,
LegendComponent
])
const props = defineProps({
data: {
type: Object,
required: true,
default: () => ({})
},
period: {
type: String,
required: true
}
})
// 获取银行机构申请详情
const bankDetails = computed(() => getBankApplicationDetails(props.data, props.period))
// 计算银行机构总次数
const bankTotal = computed(() => {
const details = bankDetails.value
return Object.values(details).reduce((sum, val) => sum + (val || 0), 0)
})
// 详细列表
const detailList = computed(() => {
const details = bankDetails.value
const labels = FIELD_LABELS.bank
return Object.entries(details)
.filter(([key, value]) => value > 0)
.map(([key, value]) => ({
key,
label: labels[key] || key,
value
}))
.sort((a, b) => b.value - a.value)
})
// 饼图配置
const pieChartOption = computed(() => {
const list = detailList.value
if (list.length === 0) {
return {
title: {
text: '暂无数据',
left: 'center',
top: 'center',
textStyle: {
color: '#999',
fontSize: 14
}
}
}
}
return {
tooltip: {
trigger: 'item',
formatter: '{b}: {c}次 ({d}%)'
},
legend: {
orient: 'vertical',
left: 'left',
top: 'middle',
textStyle: {
fontSize: 11
}
},
series: [
{
name: '申请次数',
type: 'pie',
radius: ['40%', '70%'],
center: ['60%', '50%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 4,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: true,
formatter: '{b}\n{c}次'
},
emphasis: {
label: {
show: true,
fontSize: 14,
fontWeight: 'bold'
}
},
data: list.map(item => ({
value: item.value,
name: item.label
}))
}
]
}
})
</script>
<style scoped>
.bank-institution-section {
background: #f9fafb;
padding: 16px;
border-radius: 8px;
}
</style>

View File

@@ -0,0 +1,158 @@
<template>
<div class="bank-org-section mb-6">
<div class="flex items-center mb-4">
<span class="font-bold text-gray-800">银行机构 {{ bankTotal }}</span>
</div>
<div class="grid grid-cols-2 gap-4">
<!-- 饼图 -->
<div class="h-64">
<v-chart class="chart-container" :option="pieChartOption" autoresize />
</div>
<!-- 详细列表 -->
<div class="space-y-2">
<div
v-for="(item, index) in detailList"
:key="index"
class="flex justify-between items-center text-sm py-1 border-b border-gray-100"
>
<span class="text-gray-600">{{ item.label }}</span>
<span class="text-[#333333] font-bold">{{ item.value }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import VChart from 'vue-echarts'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { PieChart } from 'echarts/charts'
import {
TitleComponent,
TooltipComponent,
LegendComponent
} from 'echarts/components'
import { getBankOrgDetails, FIELD_LABELS } from '../utils/dataParser'
// 注册ECharts组件
use([
CanvasRenderer,
PieChart,
TitleComponent,
TooltipComponent,
LegendComponent
])
const props = defineProps({
data: {
type: Object,
required: true,
default: () => ({})
},
period: {
type: String,
required: true
}
})
// 获取银行机构数详情
const bankOrgs = computed(() => getBankOrgDetails(props.data, props.period))
// 计算银行机构总数
const bankTotal = computed(() => {
const orgs = bankOrgs.value
return Object.values(orgs).reduce((sum, val) => sum + (val || 0), 0)
})
// 详细列表
const detailList = computed(() => {
const orgs = bankOrgs.value
const labels = FIELD_LABELS.bank
return Object.entries(orgs)
.filter(([key, value]) => value > 0)
.map(([key, value]) => ({
key,
label: labels[key] || key,
value
}))
.sort((a, b) => b.value - a.value)
})
// 饼图配置
const pieChartOption = computed(() => {
const list = detailList.value
if (list.length === 0) {
return {
title: {
text: '暂无数据',
left: 'center',
top: 'center',
textStyle: {
color: '#999',
fontSize: 14
}
}
}
}
return {
tooltip: {
trigger: 'item',
formatter: '{b}: {c}家 ({d}%)'
},
legend: {
orient: 'vertical',
left: 'left',
top: 'middle',
textStyle: {
fontSize: 11
}
},
series: [
{
name: '机构数',
type: 'pie',
radius: ['40%', '70%'],
center: ['60%', '50%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 4,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: true,
formatter: '{b}\n{c}家'
},
emphasis: {
label: {
show: true,
fontSize: 14,
fontWeight: 'bold'
}
},
data: list.map(item => ({
value: item.value,
name: item.label
}))
}
]
}
})
</script>
<style scoped>
.bank-org-section {
background: #f9fafb;
padding: 16px;
border-radius: 8px;
}
</style>

View File

@@ -0,0 +1,67 @@
<template>
<div class="card institution-total-section">
<div class="rounded-lg border border-gray-200 pb-2 mb-4">
<div class="flex items-center mb-4 p-4">
<span class="font-bold text-gray-800 text-lg">申请机构总数 (银行+非银) {{ totalCount }}</span>
</div>
<!-- Tab切换 -->
<div class="px-4 mb-4">
<van-tabs v-model:active="activeTab" color="var(--color-primary)">
<van-tab v-for="(period, index) in periods" :key="period.key" :title="period.label">
<div class="p-4">
<!-- 银行机构 -->
<BankOrgSection :data="data" :period="period.key" />
<!-- 非银机构 -->
<NBankOrgSection :data="data" :period="period.key" />
</div>
</van-tab>
</van-tabs>
</div>
</div>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
import { getBankOrgDetails, getNBankOrgDetails } from '../utils/dataParser'
import BankOrgSection from './BankOrgSection.vue'
import NBankOrgSection from './NBankOrgSection.vue'
const props = defineProps({
data: {
type: Object,
required: true,
default: () => ({})
}
})
const activeTab = ref(5) // 默认显示12个月
const periods = [
{ key: 'd7', label: '7天' },
{ key: 'd15', label: '15天' },
{ key: 'm1', label: '1个月' },
{ key: 'm3', label: '3个月' },
{ key: 'm6', label: '6个月' },
{ key: 'm12', label: '12个月' }
]
// 计算总机构数12个月
const totalCount = computed(() => {
const bankOrgs = getBankOrgDetails(props.data, 'm12')
const nbankOrgs = getNBankOrgDetails(props.data, 'm12')
const bankTotal = Object.values(bankOrgs).reduce((sum, val) => sum + (val || 0), 0)
const nbankTotal = Object.values(nbankOrgs).reduce((sum, val) => sum + (val || 0), 0)
return bankTotal + nbankTotal
})
</script>
<style scoped>
.card {
background: #ffffff;
}
</style>

View File

@@ -0,0 +1,158 @@
<template>
<div class="nbank-institution-section mb-6">
<div class="flex items-center mb-4">
<span class="font-bold text-gray-800">非银机构 {{ nbankTotal }}</span>
</div>
<div class="grid grid-cols-2 gap-4">
<!-- 饼图 -->
<div class="h-64">
<v-chart class="chart-container" :option="pieChartOption" autoresize />
</div>
<!-- 详细列表 -->
<div class="space-y-2">
<div
v-for="(item, index) in detailList"
:key="index"
class="flex justify-between items-center text-sm py-1 border-b border-gray-100"
>
<span class="text-gray-600">{{ item.label }}</span>
<span class="text-[#333333] font-bold">{{ item.value }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import VChart from 'vue-echarts'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { PieChart } from 'echarts/charts'
import {
TitleComponent,
TooltipComponent,
LegendComponent
} from 'echarts/components'
import { getNBankApplicationDetails, FIELD_LABELS } from '../utils/dataParser'
// 注册ECharts组件
use([
CanvasRenderer,
PieChart,
TitleComponent,
TooltipComponent,
LegendComponent
])
const props = defineProps({
data: {
type: Object,
required: true,
default: () => ({})
},
period: {
type: String,
required: true
}
})
// 获取非银机构申请详情
const nbankDetails = computed(() => getNBankApplicationDetails(props.data, props.period))
// 计算非银机构总次数
const nbankTotal = computed(() => {
const details = nbankDetails.value
return Object.values(details).reduce((sum, val) => sum + (val || 0), 0)
})
// 详细列表
const detailList = computed(() => {
const details = nbankDetails.value
const labels = FIELD_LABELS.nbank
return Object.entries(details)
.filter(([key, value]) => value > 0)
.map(([key, value]) => ({
key,
label: labels[key] || key,
value
}))
.sort((a, b) => b.value - a.value)
})
// 饼图配置
const pieChartOption = computed(() => {
const list = detailList.value
if (list.length === 0) {
return {
title: {
text: '暂无数据',
left: 'center',
top: 'center',
textStyle: {
color: '#999',
fontSize: 14
}
}
}
}
return {
tooltip: {
trigger: 'item',
formatter: '{b}: {c}次 ({d}%)'
},
legend: {
orient: 'vertical',
left: 'left',
top: 'middle',
textStyle: {
fontSize: 11
}
},
series: [
{
name: '申请次数',
type: 'pie',
radius: ['40%', '70%'],
center: ['60%', '50%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 4,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: true,
formatter: '{b}\n{c}次'
},
emphasis: {
label: {
show: true,
fontSize: 14,
fontWeight: 'bold'
}
},
data: list.map(item => ({
value: item.value,
name: item.label
}))
}
]
}
})
</script>
<style scoped>
.nbank-institution-section {
background: #f9fafb;
padding: 16px;
border-radius: 8px;
}
</style>

View File

@@ -0,0 +1,158 @@
<template>
<div class="nbank-org-section mb-6">
<div class="flex items-center mb-4">
<span class="font-bold text-gray-800">非银机构 {{ nbankTotal }}</span>
</div>
<div class="grid grid-cols-2 gap-4">
<!-- 饼图 -->
<div class="h-64">
<v-chart class="chart-container" :option="pieChartOption" autoresize />
</div>
<!-- 详细列表 -->
<div class="space-y-2">
<div
v-for="(item, index) in detailList"
:key="index"
class="flex justify-between items-center text-sm py-1 border-b border-gray-100"
>
<span class="text-gray-600">{{ item.label }}</span>
<span class="text-[#333333] font-bold">{{ item.value }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import VChart from 'vue-echarts'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { PieChart } from 'echarts/charts'
import {
TitleComponent,
TooltipComponent,
LegendComponent
} from 'echarts/components'
import { getNBankOrgDetails, FIELD_LABELS } from '../utils/dataParser'
// 注册ECharts组件
use([
CanvasRenderer,
PieChart,
TitleComponent,
TooltipComponent,
LegendComponent
])
const props = defineProps({
data: {
type: Object,
required: true,
default: () => ({})
},
period: {
type: String,
required: true
}
})
// 获取非银机构数详情
const nbankOrgs = computed(() => getNBankOrgDetails(props.data, props.period))
// 计算非银机构总数
const nbankTotal = computed(() => {
const orgs = nbankOrgs.value
return Object.values(orgs).reduce((sum, val) => sum + (val || 0), 0)
})
// 详细列表
const detailList = computed(() => {
const orgs = nbankOrgs.value
const labels = FIELD_LABELS.nbank
return Object.entries(orgs)
.filter(([key, value]) => value > 0)
.map(([key, value]) => ({
key,
label: labels[key] || key,
value
}))
.sort((a, b) => b.value - a.value)
})
// 饼图配置
const pieChartOption = computed(() => {
const list = detailList.value
if (list.length === 0) {
return {
title: {
text: '暂无数据',
left: 'center',
top: 'center',
textStyle: {
color: '#999',
fontSize: 14
}
}
}
}
return {
tooltip: {
trigger: 'item',
formatter: '{b}: {c}家 ({d}%)'
},
legend: {
orient: 'vertical',
left: 'left',
top: 'middle',
textStyle: {
fontSize: 11
}
},
series: [
{
name: '机构数',
type: 'pie',
radius: ['40%', '70%'],
center: ['60%', '50%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 4,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: true,
formatter: '{b}\n{c}家'
},
emphasis: {
label: {
show: true,
fontSize: 14,
fontWeight: 'bold'
}
},
data: list.map(item => ({
value: item.value,
name: item.label
}))
}
]
}
})
</script>
<style scoped>
.nbank-org-section {
background: #f9fafb;
padding: 16px;
border-radius: 8px;
}
</style>

118
src/ui/JRZQ6F2A/index.vue Normal file
View File

@@ -0,0 +1,118 @@
<template>
<div class="card shadow-sm rounded-xl overflow-hidden flex flex-col gap-4">
<!-- 申请次数 -->
<ApplicationCountSection :data="variableValue" />
<!-- 申请总次数 (银行+非银) -->
<ApplicationTotalSection :data="variableValue" />
<!-- 申请机构总数 (银行+非银) -->
<InstitutionTotalSection :data="variableValue" />
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useRiskNotifier } from '@/composables/useRiskNotifier'
import { extractVariableValue } from './utils/dataParser'
import ApplicationCountSection from './components/ApplicationCountSection.vue'
import ApplicationTotalSection from './components/ApplicationTotalSection.vue'
import InstitutionTotalSection from './components/InstitutionTotalSection.vue'
const props = defineProps({
data: {
type: Object,
required: true,
default: () => ({})
},
apiId: {
type: String,
default: '',
},
index: {
type: Number,
default: 0,
},
notifyRiskStatus: {
type: Function,
default: () => { },
},
})
// 获取数据
const rawData = computed(() => props.data?.data || props.data || {})
// 提取 variableValue
const variableValue = computed(() => extractVariableValue(rawData.value))
// 计算风险评分0-100分分数越高越安全
const riskScore = computed(() => {
const data = variableValue.value
if (!data || Object.keys(data).length === 0) {
return 100 // 无数据视为最安全
}
let score = 100 // 初始满分
// 近7天申请次数评估
const d7Total = parseInt(data.als_d7_id_bank_allnum || 0) + parseInt(data.als_d7_id_nbank_allnum || 0)
if (d7Total > 5) {
score -= 20 // 近7天申请过多
} else if (d7Total > 3) {
score -= 10
}
// 近15天申请次数评估
const d15Total = parseInt(data.als_d15_id_bank_allnum || 0) + parseInt(data.als_d15_id_nbank_allnum || 0)
if (d15Total > 10) {
score -= 15
} else if (d15Total > 5) {
score -= 8
}
// 近1个月申请次数评估
const m1Total = parseInt(data.als_m1_id_bank_allnum || 0) + parseInt(data.als_m1_id_nbank_allnum || 0)
if (m1Total > 15) {
score -= 20
} else if (m1Total > 8) {
score -= 10
}
// 近3个月申请次数评估
const m3Total = parseInt(data.als_m3_id_bank_allnum || 0) + parseInt(data.als_m3_id_nbank_allnum || 0)
if (m3Total > 20) {
score -= 15
} else if (m3Total > 10) {
score -= 8
}
// 近6个月申请次数评估
const m6Total = parseInt(data.als_m6_id_bank_allnum || 0) + parseInt(data.als_m6_id_nbank_allnum || 0)
if (m6Total > 30) {
score -= 10
} else if (m6Total > 15) {
score -= 5
}
// 确保分数在10-100范围内
return Math.max(10, Math.min(100, score))
})
// 使用 composable 通知父组件风险评分
useRiskNotifier(props, riskScore)
// 暴露给父组件
defineExpose({
riskScore
})
</script>
<style scoped>
.card {
background: #ffffff;
border: 1px solid #e5e7eb;
}
</style>

View File

@@ -0,0 +1,209 @@
/**
* 数据解析工具函数
* 用于解析借贷意向验证A的返回数据
*/
/**
* 获取字段值,处理空值
*/
export function getValue(value) {
if (value === undefined || value === null || value === '' || value === '空') {
return 0
}
// 如果是字符串数字,转换为数字
if (typeof value === 'string' && /^\d+(\.\d+)?$/.test(value)) {
return parseFloat(value)
}
return value
}
/**
* 从原始数据中提取 variableValue
*/
export function extractVariableValue(data) {
try {
return data?.risk_screen_v2?.variables?.find(
v => v.variableName === 'bairong_applyloan_extend'
)?.variableValue || {}
} catch (error) {
console.error('提取数据失败:', error)
return {}
}
}
/**
* 时间段映射
*/
export const PERIOD_MAP = {
d7: { label: '7天', prefix: 'als_d7' },
d15: { label: '15天', prefix: 'als_d15' },
m1: { label: '1个月', prefix: 'als_m1' },
m3: { label: '3个月', prefix: 'als_m3' },
m6: { label: '6个月', prefix: 'als_m6' },
m12: { label: '12个月', prefix: 'als_m12' }
}
/**
* 获取申请次数(按时间段)
*/
export function getApplicationCounts(variableValue, period) {
const { prefix } = PERIOD_MAP[period]
// 计算总申请次数(所有类型的申请次数之和)
const types = [
'id_pdl_allnum', // 线上小额现金贷
'id_caon_allnum', // 线上现金分期
'id_rel_allnum', // 信用卡(类信用卡)
'id_caoff_allnum', // 线下现金分期
'id_cooff_allnum', // 线下消费分期
'id_af_allnum', // 汽车金融
'id_coon_allnum', // 线上消费分期
'id_oth_allnum' // 其他
]
let total = 0
types.forEach(type => {
const value = getValue(variableValue[`${prefix}_${type}`])
total += value
})
// 银行机构申请次数
const bankTotal = getValue(variableValue[`${prefix}_id_bank_allnum`]) || 0
// 非银机构申请次数
const nbankTotal = getValue(variableValue[`${prefix}_id_nbank_allnum`]) || 0
// 如果计算出的total为0则使用银行+非银的总和
const finalTotal = total > 0 ? total : (bankTotal + nbankTotal)
return {
total: finalTotal,
bank: bankTotal,
nbank: nbankTotal
}
}
/**
* 获取银行机构申请次数详情
*/
export function getBankApplicationDetails(variableValue, period) {
const { prefix } = PERIOD_MAP[period]
return {
pdl: getValue(variableValue[`${prefix}_id_pdl_allnum`]), // 线上小额现金贷
caon: getValue(variableValue[`${prefix}_id_caon_allnum`]), // 线上现金分期
rel: getValue(variableValue[`${prefix}_id_rel_allnum`]), // 信用卡(类信用卡)
caoff: getValue(variableValue[`${prefix}_id_caoff_allnum`]), // 线下现金分期
cooff: getValue(variableValue[`${prefix}_id_cooff_allnum`]), // 线下消费分期
af: getValue(variableValue[`${prefix}_id_af_allnum`]), // 汽车金融
coon: getValue(variableValue[`${prefix}_id_coon_allnum`]), // 线上消费分期
oth: getValue(variableValue[`${prefix}_id_oth_allnum`]), // 其他
bank: getValue(variableValue[`${prefix}_id_bank_allnum`]), // 银行机构申请
tra: getValue(variableValue[`${prefix}_id_bank_tra_allnum`]), // 传统银行申请
ret: getValue(variableValue[`${prefix}_id_bank_ret_allnum`]) // 网络零售银行申请
}
}
/**
* 获取非银机构申请次数详情
*/
export function getNBankApplicationDetails(variableValue, period) {
const { prefix } = PERIOD_MAP[period]
return {
nbank: getValue(variableValue[`${prefix}_id_nbank_allnum`]), // 非银机构
p2p: getValue(variableValue[`${prefix}_id_nbank_p2p_allnum`]), // 改制机构
mc: getValue(variableValue[`${prefix}_id_nbank_mc_allnum`]), // 小贷机构
ca: getValue(variableValue[`${prefix}_id_nbank_ca_allnum`]), // 现金类分期机构
cf: getValue(variableValue[`${prefix}_id_nbank_cf_allnum`]), // 消费类分期机构
com: getValue(variableValue[`${prefix}_id_nbank_com_allnum`]), // 代偿类分期机构
oth: getValue(variableValue[`${prefix}_id_nbank_oth_allnum`]), // 其他申请
nsloan: getValue(variableValue[`${prefix}_id_nbank_nsloan_allnum`]), // 持牌网络小贷机构
autofin: getValue(variableValue[`${prefix}_id_nbank_autofin_allnum`]), // 持牌汽车金融机构
sloan: getValue(variableValue[`${prefix}_id_nbank_sloan_allnum`]), // 持牌小贷机构
cons: getValue(variableValue[`${prefix}_id_nbank_cons_allnum`]), // 持牌消费金融机构
finlea: getValue(variableValue[`${prefix}_id_nbank_finlea_allnum`]), // 持牌融资租赁机构
else: getValue(variableValue[`${prefix}_id_nbank_else_allnum`]) // 其他申请
}
}
/**
* 获取银行机构申请机构数详情
*/
export function getBankOrgDetails(variableValue, period) {
const { prefix } = PERIOD_MAP[period]
return {
pdl: getValue(variableValue[`${prefix}_id_pdl_orgnum`]),
caon: getValue(variableValue[`${prefix}_id_caon_orgnum`]),
rel: getValue(variableValue[`${prefix}_id_rel_orgnum`]),
caoff: getValue(variableValue[`${prefix}_id_caoff_orgnum`]),
cooff: getValue(variableValue[`${prefix}_id_cooff_orgnum`]),
af: getValue(variableValue[`${prefix}_id_af_orgnum`]),
coon: getValue(variableValue[`${prefix}_id_coon_orgnum`]),
oth: getValue(variableValue[`${prefix}_id_oth_orgnum`]),
bank: getValue(variableValue[`${prefix}_id_bank_orgnum`]),
tra: getValue(variableValue[`${prefix}_id_bank_tra_orgnum`]),
ret: getValue(variableValue[`${prefix}_id_bank_ret_orgnum`])
}
}
/**
* 获取非银机构申请机构数详情
*/
export function getNBankOrgDetails(variableValue, period) {
const { prefix } = PERIOD_MAP[period]
return {
nbank: getValue(variableValue[`${prefix}_id_nbank_orgnum`]),
p2p: getValue(variableValue[`${prefix}_id_nbank_p2p_orgnum`]),
mc: getValue(variableValue[`${prefix}_id_nbank_mc_orgnum`]),
ca: getValue(variableValue[`${prefix}_id_nbank_ca_orgnum`]),
cf: getValue(variableValue[`${prefix}_id_nbank_cf_orgnum`]),
com: getValue(variableValue[`${prefix}_id_nbank_com_orgnum`]),
oth: getValue(variableValue[`${prefix}_id_nbank_oth_orgnum`]),
nsloan: getValue(variableValue[`${prefix}_id_nbank_nsloan_orgnum`]),
autofin: getValue(variableValue[`${prefix}_id_nbank_autofin_orgnum`]),
sloan: getValue(variableValue[`${prefix}_id_nbank_sloan_orgnum`]),
cons: getValue(variableValue[`${prefix}_id_nbank_cons_orgnum`]),
finlea: getValue(variableValue[`${prefix}_id_nbank_finlea_orgnum`]),
else: getValue(variableValue[`${prefix}_id_nbank_else_orgnum`])
}
}
/**
* 字段名称映射
*/
export const FIELD_LABELS = {
// 银行机构申请类型
bank: {
pdl: '申请线上小额现金贷',
caon: '申请线上现金分期',
rel: '申请信用卡(类信用卡)',
caoff: '申请线下现金分期',
cooff: '申请线下消费分期',
af: '申请汽车金融',
coon: '申请线上消费分期',
oth: '申请其他',
bank: '银行机构申请',
tra: '银行机构-传统银行申请',
ret: '银行机构-网络零售银行申请'
},
// 非银机构申请类型
nbank: {
nbank: '非银机构',
p2p: '改制机构',
mc: '小贷机构',
ca: '现金类分期机构',
cf: '消费类分期机构',
com: '代偿类分期机构',
oth: '其他申请',
nsloan: '持牌网络小贷机构',
autofin: '汽车金融',
sloan: '持牌小贷机构',
cons: '持牌消费金融机构',
finlea: '持牌融资租赁机构',
else: '其他申请'
}
}

BIN
src/ui/JRZQ7F1A.zip Normal file

Binary file not shown.

BIN
src/ui/JRZQ8B3C.zip Normal file

Binary file not shown.

149
src/ui/JRZQ8B3C/README.md Normal file
View File

@@ -0,0 +1,149 @@
# 个人消费能力等级组件 (JRZQ8B3C)
## 组件概述
基于个人收入指数评分进行消费能力等级评估,为企业提供专业的消费能力分析和风险评估服务。
## 组件结构
```
JRZQ8B3C/
├── index.vue # 主组件
└── README.md # 说明文档
```
## 使用方法
### 基本用法
```vue
<template>
<div>
<JRZQ8B3C :data="consumptionData" />
</div>
</template>
<script setup>
import JRZQ8B3C from '@/ui/JRZQ8B3C/index.vue'
// 个人消费能力等级数据示例
const consumptionData = {
personincome_index_2.0: "200" // 个人收入指数评分(字符串类型)
}
</script>
```
## 数据字段说明
| 字段名 | 类型 | 必填 | 描述 | 示例值 |
|-------|------|------|------|--------|
| personincome_index_2.0 | String | 是 | 个人收入指数评分 | "200" |
## 评分分档说明
| 分值 | 收入区间(元/月) | 消费能力等级 | 风险等级 |
|------|----------------|------------|----------|
| -1 | **未命中** | 无法获取收入信息 | 高风险 |
| 100 | (1000, 2000] | 第1档 | 高风险 |
| 200 | (2000, 4000] | 第2档 | 高风险 |
| 300 | (4000, 6000] | 第3档 | 高风险 |
| 400 | (6000, 8000] | 第4档 | 中等风险 |
| 500 | (8000, 10000] | 第5档 | 中等风险 |
| 600 | (10000, 12000] | 第6档 | 中等风险 |
| 700 | (12000, 15000] | 第7档 | 低风险 |
| 800 | (15000, 20000] | 第8档 | 低风险 |
| 900 | (20000, 25000] | 第9档 | 低风险 |
| 1000 | (25000, +∞) | 第10档 | 低风险 |
## 特殊值说明
- **-1**: 表示未命中(无法获取收入信息)
- **评分范围**: 100-1000分共10个等级
- **等级意义**: 等级越高,对应的消费能力越强
- **区间定义**: 收入区间为左开右闭区间1000 < 收入 2000
## 组件特性
### 1. 专业的视觉展示
- **评分展示**大数字显示个人收入指数评分
- **进度条可视化**直观展示评分在100-1000分范围内的位置
- **颜色编码**根据评分等级使用不同颜色=红色,中=黄色,高=绿色)
- **响应式设计**完美适配各种屏幕尺寸
### 2. 全面的数据分析
- **收入区间显示**清晰展示对应的月收入范围
- **等级描述**显示当前评分对应的消费能力等级
- **市场对比分析**与市场平均水平对比
- **消费能力评估**基于收入指数的消费能力分析
### 3. 智能风险评估
- **动态评分**根据收入指数自动计算风险分数30-100分
- **风险等级标签**直观显示当前风险等级
- **个性化建议**针对不同等级的专业建议
## 视觉设计亮点
### 1. 色彩系统
- **低风险700-1000分**绿色系表示消费能力强
- **中等风险400-600分**黄色系表示消费能力中等
- **高风险100-300分/-1**红色系表示消费能力有限
### 2. 交互体验
- 平滑的动画过渡
- 直观的视觉反馈
- 清晰的信息层次
### 3. 信息架构
- 层次分明的信息展示
- 重点突出的核心数据
- 完整的补充说明
## 数据说明
### 评估依据
- 基于个人收入指数评分
- 使用10档分级评分体系
- 数据准确可靠
### 使用限制
- 收入范围为税前月收入
- 存在地区差异仅供参考
- 建议结合其他收入证明材料
### 评分计算
- 风险评分范围30-100分
- 100分对应30分风险评分
- 1000分对应100分风险评分最安全
- -1未命中对应30分风险评分
## 业务价值
### 1. 风险控制
- 精确的消费能力评估降低信贷风险
- 多维度风险分析提升决策质量
- 智能化评分系统提高效率
### 2. 客户分层
- 基于消费能力的客户分级管理
- 个性化服务策略制定
- 精准的市场定位分析
### 3. 合规要求
- 符合金融监管要求
- 数据来源权威可靠
- 评估过程透明公开
## 注意事项
1. 确保传入正确的 `personincome_index_2.0`
2. 组件会自动处理 -1 特殊值未命中
3. 建议在网络良好的环境下使用
4. 定期更新评估标准以保持准确性
## 更新日志
- v1.0.0 - 初始版本支持基础消费能力等级评估功能
- 专业的视觉展示效果
- 完整的评分分档系统
- 专业的风险分析功能

377
src/ui/JRZQ8B3C/index.vue Normal file
View File

@@ -0,0 +1,377 @@
<template>
<div class="card">
<div class="rounded-lg border border-gray-200 pb-2 mb-4">
<!-- 标题区域 -->
<div class="flex items-center mb-4 p-4">
<div class="w-8 h-8 flex items-center justify-center mr-2">
<img src="@/assets/images/report/srpg.png" alt="个人消费能力等级" class="w-8 h-8 object-contain" />
</div>
<span class="font-bold text-gray-800">个人消费能力等级</span>
</div>
<div class="px-4 pb-4">
<!-- 月消费能力 -->
<div class="mb-6 text-center">
<div class="text-sm text-gray-600 mb-2">月消费能力</div>
<div class="text-3xl font-bold mb-3 text-[#333333]">
<span class="amount-number">{{ getConsumptionAmount(score) }}</span>
<span class="amount-unit">/</span>
</div>
<div class="level-bar" :style="getLevelBarBgStyle(score)">
<div class="level-fill" :style="getLevelBarStyle(score)"></div>
</div>
<div class="text-sm text-gray-600 mt-2">{{ getConsumptionDescription(score) }}</div>
</div>
<!-- 评估结果 -->
<div class="assessment-card">
<div class="flex items-center">
<div class="flex-1">
<div class="flex items-center justify-between mb-2">
<h4 class="font-semibold text-gray-800">评估结果</h4>
</div>
<p class="text-gray-400 text-sm">
{{ getAssessmentDescription(score) }}
</p>
</div>
</div>
</div>
<!-- 市场对比 -->
<div class="assessment-card">
<div class="flex items-center mb-8">
<div class="flex-1">
<div class="flex items-center justify-between mb-2">
<h4 class="font-semibold text-gray-800">市场对比</h4>
</div>
<p class="text-gray-400 text-sm">
{{ getMarketComparison(score) }}
</p>
</div>
</div>
<div class="comparison-indicator mt-4">
<div class="indicator-bar">
<div class="indicator-fill" :style="getIndicatorStyle(score)"></div>
<div class="indicator-marker" :style="getMarkerPosition(score)">
<img src="@/assets/images/report/srbq.png" alt="市场对比" class="marker-image" />
<div class="marker-dot"></div>
</div>
</div>
<div class="indicator-labels">
<span>低消费</span>
<span>高消费</span>
</div>
</div>
</div>
<!-- 消费能力 -->
<div class="assessment-card">
<div class="flex items-center">
<div class="flex-1">
<div class="mb-2">
<h4 class="font-semibold text-gray-800">消费能力</h4>
</div>
<p class="text-gray-400 text-sm">
{{ getConsumptionCapacity(score) }}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useRiskNotifier } from '@/composables/useRiskNotifier';
const props = defineProps({
data: {
type: Object,
required: true,
default: () => ({})
},
apiId: {
type: String,
default: '',
},
index: {
type: Number,
default: 0,
},
notifyRiskStatus: {
type: Function,
default: () => { },
},
})
// 确保data是响应式的
const data = computed(() => props.data || {})
// 获取评分值(字符串转数字,处理-1特殊值
const score = computed(() => {
const value = data.value['personincome_index_2.0'];
if (!value || value === '-1') return -1;
const num = parseInt(value, 10);
return isNaN(num) ? -1 : num;
})
// 获取消费金额显示(直接显示金额区间)
const getConsumptionAmount = (score) => {
if (score === -1) return '无法评估'
const amountMap = {
100: '2,000 - 4,000',
200: '2,000 - 4,000',
300: '4,000 - 6,000',
400: '6,000 - 8,000',
500: '8,000 - 10,000',
600: '10,000 - 12,000',
700: '12,000 - 15,000',
800: '15,000 - 20,000',
900: '20,000 - 25,000',
1000: '25,000+'
}
return amountMap[score] || '数据异常'
}
// 消费能力描述
const getConsumptionDescription = (score) => {
if (score === -1) return '暂未发现消费能力信息'
const descriptionMap = {
100: '基础消费能力',
200: '基础消费能力',
300: '中等消费能力',
400: '中等消费能力',
500: '良好消费能力',
600: '良好消费能力',
700: '较强消费能力',
800: '较强消费能力',
900: '很强消费能力',
1000: '很强消费能力'
}
return descriptionMap[score] || '数据异常'
}
// 等级进度条样式
const getLevelBarStyle = (score) => {
if (score === -1) {
return {
width: '0%',
background: '#94a3b8'
}
}
// 计算百分比100分=10%, 200分=20%, ..., 1000分=100%
const percentage = (score / 1000) * 100
// 统一使用蓝色渐变
return {
width: percentage + '%',
background: 'linear-gradient(90deg, #3b82f6 0%, #2563eb 100%)'
}
}
// 进度条背景色样式
const getLevelBarBgStyle = (score) => {
// 统一使用淡蓝色背景
return {
background: '#eff6ff'
}
}
// 评估描述
const getAssessmentDescription = (score) => {
if (score === -1) {
return '根据个人消费能力等级分析,无法获取该用户的消费能力信息,无法进行评估。'
}
const descriptions = {
100: '根据个人消费能力等级分析,该用户月消费能力较低,消费水平有限。',
200: '根据个人消费能力等级分析,该用户月消费能力较低,消费水平有限。',
300: '根据个人消费能力等级分析,该用户月消费能力中等,消费水平良好。',
400: '根据个人消费能力等级分析,该用户月消费能力中等,消费水平良好。',
500: '根据个人消费能力等级分析,该用户月消费能力中等偏上,消费水平较强。',
600: '根据个人消费能力等级分析,该用户月消费能力中等偏上,消费水平较强。',
700: '根据个人消费能力等级分析,该用户月消费能力较高,消费水平很强。',
800: '根据个人消费能力等级分析,该用户月消费能力较高,消费水平很强。',
900: '根据个人消费能力等级分析,该用户月消费能力很高,消费水平顶级。',
1000: '根据个人消费能力等级分析,该用户月消费能力很高,消费水平顶级。'
}
return descriptions[score] || '数据异常,无法进行准确评估。'
}
// 市场对比分析
const getMarketComparison = (score) => {
if (score === -1) {
return '无消费能力信息,无法与市场平均水平进行对比。'
}
const comparisons = {
100: '低于市场平均消费水平,处于消费分布的底部区间。',
200: '低于市场平均消费水平,处于消费分布的中下区间。',
300: '接近市场平均消费水平,处于消费分布的中等区间。',
400: '接近市场平均消费水平,处于消费分布的中等区间。',
500: '高于市场平均消费水平,处于消费分布的中上区间。',
600: '高于市场平均消费水平,处于消费分布的中上区间。',
700: '明显高于市场平均消费水平,处于消费分布的上层区间。',
800: '显著高于市场平均消费水平,处于消费分布的高层区间。',
900: '远高于市场平均消费水平,处于消费分布的顶部区间。',
1000: '超越市场绝大多数消费水平,处于消费分布的顶级区间。'
}
return comparisons[score] || '数据异常,无法进行市场对比。'
}
// 消费能力分析
const getConsumptionCapacity = (score) => {
if (score === -1) {
return '缺乏消费能力信息,月消费能力存在不确定性,需要谨慎评估。'
}
const capacities = {
100: '月消费能力较低,消费水平有限,建议理性消费。',
200: '月消费能力较低,消费水平有限,建议理性消费。',
300: '月消费能力稳定,消费水平良好,具备一定的消费潜力。',
400: '月消费能力稳定,消费水平良好,具备一定的消费潜力。',
500: '月消费能力较高,消费水平较强,可以支持中等消费。',
600: '月消费能力较高,消费水平较强,可以支持中等消费。',
700: '月消费能力很强,消费水平很高,可以支持较高消费。',
800: '月消费能力很强,消费水平很高,可以支持较高消费。',
900: '月消费能力顶级,消费水平极高,可以支持高端消费。',
1000: '月消费能力顶级,消费水平极高,可以支持顶级消费。'
}
return capacities[score] || '数据异常,无法进行消费能力分析。'
}
// 指示器样式
const getIndicatorStyle = (score) => {
if (score === -1) return { width: '0%', background: 'transparent' }
// 100分=0%, 1000分=100%
const percentage = ((score - 100) / 900) * 100
return {
width: percentage + '%',
background: `linear-gradient(90deg, #93c5fd 0%, #3b82f6 50%, #1d4ed8 100%)`
}
}
// 标记位置
const getMarkerPosition = (score) => {
if (score === -1) return { left: '0%' }
// 100分=0%, 1000分=100%
const percentage = ((score - 100) / 900) * 100
return {
left: percentage + '%'
}
}
// 计算风险评分0-100分分数越高越安全
const riskScore = computed(() => {
if (score.value === -1) return 30 // 未命中,风险较高
// 100分对应30分1000分对应100分
return 30 + ((score.value - 100) / 900) * 70
});
// 使用 composable 通知父组件风险评分
useRiskNotifier(props, riskScore);
// 暴露给父组件
defineExpose({
riskScore
});
</script>
<style scoped>
/* 金额数字样式 */
.amount-number {
color: #333333;
font-weight: bold;
}
.amount-unit {
font-size: 0.5em;
color: #999999;
margin-left: 4px;
}
/* 进度条 */
.level-bar {
height: 12px;
border-radius: 8px;
overflow: hidden;
}
.level-fill {
height: 100%;
border-radius: 4px;
transition: all 0.6s ease;
}
/* 评估卡片 */
.assessment-card {
padding: 16px;
border-radius: 8px;
margin-bottom: 16px;
border: 1px solid;
}
/* 评估卡片统一样式 */
.assessment-card {
background: #f8fafc;
border-color: #e2e8f0;
}
/* 对比指示器 */
.comparison-indicator {
margin-top: 12px;
}
.indicator-bar {
position: relative;
height: 6px;
border-radius: 3px;
margin-bottom: 8px;
background: linear-gradient(90deg, #93c5fd 0%, #3b82f6 50%, #1d4ed8 100%);
}
.indicator-fill {
height: 100%;
border-radius: 3px;
transition: all 0.5s ease;
background: transparent;
}
.indicator-marker {
position: absolute;
top: -26px;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
}
.marker-image {
width: 24px;
margin-bottom: 0px;
}
.marker-dot {
width: 8px;
height: 8px;
background: white;
border-radius: 50%;
box-shadow: 0px 4px 4px 0px #00000040;
}
.indicator-labels {
display: flex;
justify-content: space-between;
font-size: 0.7rem;
color: #9ca3af;
}
</style>