1049 lines
37 KiB
Vue
1049 lines
37 KiB
Vue
<script setup>
|
||
import LTitle from '@/components/LTitle.vue'
|
||
import { ref, onMounted, computed } from 'vue'
|
||
|
||
const props = defineProps({
|
||
data: {
|
||
type: Object,
|
||
required: true,
|
||
},
|
||
})
|
||
|
||
// 检查data是否为null或空对象
|
||
const defaultData = {
|
||
swift_number: '999333_20181029143459_23453A4E0',
|
||
code: '00',
|
||
flag_specialList_c: '1',
|
||
sl_id_court_bad: '',
|
||
sl_id_court_executed: '',
|
||
sl_id_bank_bad: '',
|
||
sl_id_bank_overdue: '',
|
||
sl_id_bank_lost: '',
|
||
sl_id_nbank_bad: '',
|
||
sl_id_nbank_overdue: '',
|
||
sl_id_nbank_lost: '',
|
||
sl_id_nbank_nsloan_bad: '',
|
||
sl_id_nbank_nsloan_overdue: '',
|
||
sl_id_nbank_nsloan_lost: '',
|
||
sl_id_nbank_sloan_bad: '',
|
||
sl_id_nbank_sloan_overdue: '',
|
||
sl_id_nbank_sloan_lost: '',
|
||
sl_id_nbank_cons_bad: '',
|
||
sl_id_nbank_cons_overdue: '',
|
||
sl_id_nbank_cons_lost: '',
|
||
sl_id_nbank_finlea_bad: '',
|
||
sl_id_nbank_finlea_overdue: '',
|
||
sl_id_nbank_finlea_lost: '',
|
||
sl_id_nbank_autofin_bad: '',
|
||
sl_id_nbank_autofin_overdue: '',
|
||
sl_id_nbank_autofin_lost: '',
|
||
sl_id_nbank_other_bad: '',
|
||
sl_id_nbank_other_overdue: '',
|
||
sl_id_nbank_other_lost: '',
|
||
sl_id_court_bad_time: '0',
|
||
sl_id_court_executed_time: '0',
|
||
sl_id_bank_bad_time: '0',
|
||
sl_id_bank_overdue_time: '0',
|
||
sl_id_bank_lost_time: '0',
|
||
sl_id_nbank_bad_time: '0',
|
||
sl_id_nbank_overdue_time: '0',
|
||
sl_id_nbank_lost_time: '0',
|
||
sl_id_nbank_nsloan_bad_time: '0',
|
||
sl_id_nbank_nsloan_overdue_time: '0',
|
||
sl_id_nbank_nsloan_lost_time: '0',
|
||
sl_id_nbank_sloan_bad_time: '0',
|
||
sl_id_nbank_sloan_overdue_time: '0',
|
||
sl_id_nbank_sloan_lost_time: '0',
|
||
sl_id_nbank_cons_bad_time: '0',
|
||
sl_id_nbank_cons_overdue_time: '0',
|
||
sl_id_nbank_cons_lost_time: '0',
|
||
sl_id_nbank_finlea_bad_time: '0',
|
||
sl_id_nbank_finlea_overdue_time: '0',
|
||
sl_id_nbank_finlea_lost_time: '0',
|
||
sl_id_nbank_autofin_bad_time: '0',
|
||
sl_id_nbank_autofin_overdue_time: '0',
|
||
sl_id_nbank_autofin_lost_time: '0',
|
||
sl_id_nbank_other_bad_time: '0',
|
||
sl_id_nbank_other_overdue_time: '0',
|
||
sl_id_nbank_other_lost_time: '0',
|
||
sl_id_court_bad_allnum: '0',
|
||
sl_id_court_executed_allnum: '0',
|
||
sl_id_bank_bad_allnum: '0',
|
||
sl_id_bank_overdue_allnum: '0',
|
||
sl_id_bank_lost_allnum: '0',
|
||
sl_id_nbank_bad_allnum: '0',
|
||
sl_id_nbank_overdue_allnum: '0',
|
||
sl_id_nbank_lost_allnum: '0',
|
||
sl_id_nbank_nsloan_bad_allnum: '0',
|
||
sl_id_nbank_nsloan_overdue_allnum: '0',
|
||
sl_id_nbank_nsloan_lost_allnum: '0',
|
||
sl_id_nbank_sloan_bad_allnum: '0',
|
||
sl_id_nbank_sloan_overdue_allnum: '0',
|
||
sl_id_nbank_sloan_lost_allnum: '0',
|
||
sl_id_nbank_cons_bad_allnum: '0',
|
||
sl_id_nbank_cons_overdue_allnum: '0',
|
||
sl_id_nbank_cons_lost_allnum: '0',
|
||
sl_id_nbank_finlea_bad_allnum: '0',
|
||
sl_id_nbank_finlea_overdue_allnum: '0',
|
||
sl_id_nbank_finlea_lost_allnum: '0',
|
||
sl_id_nbank_autofin_bad_allnum: '0',
|
||
sl_id_nbank_autofin_overdue_allnum: '0',
|
||
sl_id_nbank_autofin_lost_allnum: '0',
|
||
sl_id_nbank_other_bad_allnum: '0',
|
||
sl_id_nbank_other_overdue_allnum: '0',
|
||
sl_id_nbank_other_lost_allnum: '0',
|
||
sl_cell_bank_bad: '',
|
||
sl_cell_bank_overdue: '',
|
||
sl_cell_bank_lost: '',
|
||
sl_cell_nbank_bad: '',
|
||
sl_cell_nbank_overdue: '',
|
||
sl_cell_nbank_lost: '',
|
||
sl_cell_nbank_nsloan_bad: '',
|
||
sl_cell_nbank_nsloan_overdue: '',
|
||
sl_cell_nbank_nsloan_lost: '',
|
||
sl_cell_nbank_sloan_bad: '',
|
||
sl_cell_nbank_sloan_overdue: '',
|
||
sl_cell_nbank_sloan_lost: '',
|
||
sl_cell_nbank_cons_bad: '',
|
||
sl_cell_nbank_cons_overdue: '',
|
||
sl_cell_nbank_cons_lost: '',
|
||
sl_cell_nbank_finlea_bad: '',
|
||
sl_cell_nbank_finlea_overdue: '',
|
||
sl_cell_nbank_finlea_lost: '',
|
||
sl_cell_nbank_autofin_bad: '',
|
||
sl_cell_nbank_autofin_overdue: '',
|
||
sl_cell_nbank_autofin_lost: '',
|
||
sl_cell_nbank_other_bad: '',
|
||
sl_cell_nbank_other_overdue: '',
|
||
sl_cell_nbank_other_lost: '',
|
||
sl_cell_bank_bad_time: '0',
|
||
sl_cell_bank_overdue_time: '0',
|
||
sl_cell_bank_lost_time: '0',
|
||
sl_cell_nbank_bad_time: '0',
|
||
sl_cell_nbank_overdue_time: '0',
|
||
sl_cell_nbank_lost_time: '0',
|
||
sl_cell_nbank_nsloan_bad_time: '0',
|
||
sl_cell_nbank_nsloan_overdue_time: '0',
|
||
sl_cell_nbank_nsloan_lost_time: '0',
|
||
sl_cell_nbank_sloan_bad_time: '0',
|
||
sl_cell_nbank_sloan_overdue_time: '0',
|
||
sl_cell_nbank_sloan_lost_time: '0',
|
||
sl_cell_nbank_cons_bad_time: '0',
|
||
sl_cell_nbank_cons_overdue_time: '0',
|
||
sl_cell_nbank_cons_lost_time: '0',
|
||
sl_cell_nbank_finlea_bad_time: '0',
|
||
sl_cell_nbank_finlea_overdue_time: '0',
|
||
sl_cell_nbank_finlea_lost_time: '0',
|
||
sl_cell_nbank_autofin_bad_time: '0',
|
||
sl_cell_nbank_autofin_overdue_time: '0',
|
||
sl_cell_nbank_autofin_lost_time: '0',
|
||
sl_cell_nbank_other_bad_time: '0',
|
||
sl_cell_nbank_other_overdue_time: '0',
|
||
sl_cell_nbank_other_lost_time: '0',
|
||
sl_cell_bank_bad_allnum: '0',
|
||
sl_cell_bank_overdue_allnum: '0',
|
||
sl_cell_bank_lost_allnum: '0',
|
||
sl_cell_nbank_bad_allnum: '0',
|
||
sl_cell_nbank_overdue_allnum: '0',
|
||
sl_cell_nbank_lost_allnum: '0',
|
||
sl_cell_nbank_nsloan_bad_allnum: '0',
|
||
sl_cell_nbank_nsloan_overdue_allnum: '0',
|
||
sl_cell_nbank_nsloan_lost_allnum: '0',
|
||
sl_cell_nbank_sloan_bad_allnum: '0',
|
||
sl_cell_nbank_sloan_overdue_allnum: '0',
|
||
sl_cell_nbank_sloan_lost_allnum: '0',
|
||
sl_cell_nbank_cons_bad_allnum: '0',
|
||
sl_cell_nbank_cons_overdue_allnum: '0',
|
||
sl_cell_nbank_cons_lost_allnum: '0',
|
||
sl_cell_nbank_finlea_bad_allnum: '0',
|
||
sl_cell_nbank_finlea_overdue_allnum: '0',
|
||
sl_cell_nbank_finlea_lost_allnum: '0',
|
||
sl_cell_nbank_autofin_bad_allnum: '0',
|
||
sl_cell_nbank_autofin_overdue_allnum: '0',
|
||
sl_cell_nbank_autofin_lost_allnum: '0',
|
||
sl_cell_nbank_other_bad_allnum: '0',
|
||
sl_cell_nbank_other_overdue_allnum: '0',
|
||
sl_cell_nbank_other_lost_allnum: '0',
|
||
}
|
||
|
||
// 当前激活的标签页
|
||
const activeTab = ref('summary')
|
||
|
||
// 由于所有值都需要是"0",我们可以直接使用这个defaultData
|
||
const dataValue = ref(!props.data || Object.keys(props.data).length === 0 ? defaultData : props.data)
|
||
|
||
// 风险类型定义
|
||
const riskTypes = {
|
||
idCard: {
|
||
label: '身份证风险',
|
||
prefix: 'sl_id_',
|
||
},
|
||
mobile: {
|
||
label: '手机号风险',
|
||
prefix: 'sl_cell_',
|
||
},
|
||
}
|
||
|
||
// 金融机构定义
|
||
const institutions = {
|
||
court: {
|
||
label: '法院失信人',
|
||
levels: ['bad'], // 法院失信人只有bad一种级别
|
||
},
|
||
court_executed: {
|
||
label: '法院被执行人',
|
||
levels: [''], // 特殊情况,没有风险级别后缀
|
||
},
|
||
bank: {
|
||
label: '银行(含信用卡)',
|
||
levels: ['bad', 'overdue', 'lost'],
|
||
},
|
||
nbank: {
|
||
label: '非银机构',
|
||
levels: ['bad', 'overdue', 'lost'],
|
||
},
|
||
nbank_nsloan: {
|
||
label: '持牌网络小贷',
|
||
levels: ['bad', 'overdue', 'lost'],
|
||
},
|
||
nbank_sloan: {
|
||
label: '持牌小贷',
|
||
levels: ['bad', 'overdue', 'lost'],
|
||
},
|
||
nbank_cons: {
|
||
label: '持牌消费金融',
|
||
levels: ['bad', 'overdue', 'lost'],
|
||
},
|
||
nbank_finlea: {
|
||
label: '持牌融资租赁',
|
||
levels: ['bad', 'overdue', 'lost'],
|
||
},
|
||
nbank_autofin: {
|
||
label: '持牌汽车金融',
|
||
levels: ['bad', 'overdue', 'lost'],
|
||
},
|
||
nbank_other: {
|
||
label: '其他',
|
||
levels: ['bad', 'overdue', 'lost'],
|
||
},
|
||
}
|
||
|
||
// 风险级别定义
|
||
const riskLevels = {
|
||
'': { label: '被执行人', type: 'medium', color: 'orange' }, // 特殊情况:法院被执行人
|
||
overdue: { label: '', type: 'low', color: 'blue' },
|
||
bad: { label: '', type: 'medium', color: 'orange' },
|
||
lost: { label: '', type: 'high', color: 'red' },
|
||
}
|
||
|
||
// 风险级别分组
|
||
const riskGroups = {
|
||
low: { label: '短期逾期', color: 'blue' },
|
||
medium: { label: '严重逾期', color: 'orange' },
|
||
high: { label: '无法收回', color: 'red' },
|
||
}
|
||
|
||
// 标签页配置
|
||
const tabConfigs = {
|
||
summary: {
|
||
title: '风险汇总',
|
||
description: '展示各类风险汇总信息',
|
||
},
|
||
low: {
|
||
title: '短期逾期详情',
|
||
description: '展示所有短期逾期相关记录,包括短期未按时还款(1-90天)产生罚息,征信记录暂未恶化的项目',
|
||
},
|
||
medium: {
|
||
title: '严重逾期详情',
|
||
description: '展示所有严重逾期相关记录,包括持续未还款(90-360天),征信标记"不良",限制贷款/信用卡使用的项目',
|
||
},
|
||
high: {
|
||
title: '无法收回详情',
|
||
description:
|
||
'展示所有无法收回相关记录,包括超360天未还且催收无效,带有法律诉讼、资产冻结、终身征信污点等严重后果的项目',
|
||
},
|
||
}
|
||
|
||
// 风险类型配置
|
||
const riskTypeConfigs = {
|
||
idCard: {
|
||
title: '身份证风险信息',
|
||
emptyText: '暂无身份证风险记录',
|
||
},
|
||
mobile: {
|
||
title: '手机号风险信息',
|
||
emptyText: '暂无手机号风险记录',
|
||
},
|
||
}
|
||
|
||
// 处理数据并分类
|
||
function processData(data) {
|
||
const result = {
|
||
low: [],
|
||
medium: [],
|
||
high: [],
|
||
}
|
||
|
||
// 遍历风险类型(身份证/手机号)
|
||
Object.entries(riskTypes).forEach(([riskTypeKey, riskType]) => {
|
||
// 遍历金融机构
|
||
Object.entries(institutions).forEach(([institutionKey, institution]) => {
|
||
// 遍历当前机构支持的风险级别
|
||
institution.levels.forEach(levelKey => {
|
||
// 构建字段名 - 特殊处理court_executed
|
||
let fieldBase
|
||
if (institutionKey === 'court_executed') {
|
||
fieldBase = `${riskType.prefix}${institutionKey}`
|
||
} else {
|
||
fieldBase = `${riskType.prefix}${institutionKey}_${levelKey}`
|
||
}
|
||
|
||
const valueField = fieldBase
|
||
const timeField = `${fieldBase}_time`
|
||
const countField = `${fieldBase}_allnum`
|
||
|
||
// 创建记录对象
|
||
const record = {
|
||
id: `${riskTypeKey}_${institutionKey}_${levelKey || 'executed'}`,
|
||
riskType: riskTypeKey,
|
||
riskTypeLabel: riskType.label,
|
||
institution: institutionKey,
|
||
institutionLabel: institution.label,
|
||
level: levelKey || 'executed', // 为空时使用'executed'作为标识
|
||
levelLabel: institutionKey === 'court_executed' ? '被执行人' : riskLevels[levelKey].label,
|
||
levelType: institutionKey === 'court_executed' ? 'medium' : riskLevels[levelKey].type,
|
||
levelColor: institutionKey === 'court_executed' ? 'orange' : riskLevels[levelKey].color,
|
||
value: data[valueField] || '',
|
||
time: data[timeField] || '0',
|
||
count: data[countField] || '0',
|
||
// 根据1.txt文档,0表示命中,空表示未命中
|
||
isTriggered: data[valueField] === '0',
|
||
fieldName: valueField,
|
||
}
|
||
|
||
// 根据风险级别分类
|
||
if (institutionKey === 'court_executed') {
|
||
result['medium'].push(record)
|
||
} else {
|
||
result[riskLevels[levelKey].type].push(record)
|
||
}
|
||
})
|
||
})
|
||
})
|
||
|
||
return result
|
||
}
|
||
|
||
// 生成汇总统计数据
|
||
function generateSummary(processedData) {
|
||
const summary = {
|
||
// 按风险级别统计
|
||
byRiskLevel: Object.keys(riskGroups).map(levelKey => {
|
||
const items = processedData[levelKey]
|
||
const triggeredItems = items.filter(item => item.isTriggered)
|
||
return {
|
||
id: levelKey,
|
||
label: riskGroups[levelKey].label,
|
||
color: riskGroups[levelKey].color,
|
||
total: items.length,
|
||
triggered: triggeredItems.length,
|
||
percentage: items.length > 0 ? ((triggeredItems.length / items.length) * 100).toFixed(1) : 0,
|
||
items: triggeredItems,
|
||
}
|
||
}),
|
||
|
||
// 按风险类型统计
|
||
byRiskType: Object.keys(riskTypes).map(typeKey => {
|
||
const allItems = [...processedData.low, ...processedData.medium, ...processedData.high].filter(
|
||
item => item.riskType === typeKey
|
||
)
|
||
|
||
const triggeredItems = allItems.filter(item => item.isTriggered)
|
||
|
||
return {
|
||
id: typeKey,
|
||
label: riskTypes[typeKey].label,
|
||
total: allItems.length,
|
||
triggered: triggeredItems.length,
|
||
percentage: allItems.length > 0 ? ((triggeredItems.length / allItems.length) * 100).toFixed(1) : 0,
|
||
items: triggeredItems,
|
||
}
|
||
}),
|
||
|
||
// 按机构类型统计
|
||
byInstitution: Object.keys(institutions).map(institutionKey => {
|
||
const allItems = [...processedData.low, ...processedData.medium, ...processedData.high].filter(
|
||
item => item.institution === institutionKey
|
||
)
|
||
|
||
const triggeredItems = allItems.filter(item => item.isTriggered)
|
||
|
||
return {
|
||
id: institutionKey,
|
||
label: institutions[institutionKey].label,
|
||
total: allItems.length,
|
||
triggered: triggeredItems.length,
|
||
percentage: allItems.length > 0 ? ((triggeredItems.length / allItems.length) * 100).toFixed(1) : 0,
|
||
items: triggeredItems,
|
||
}
|
||
}),
|
||
}
|
||
|
||
return summary
|
||
}
|
||
|
||
// 持有处理后的数据
|
||
const processedData = ref({})
|
||
const summaryData = ref({})
|
||
|
||
// 跳转到指定标签页
|
||
function navigateToTab(tabName) {
|
||
activeTab.value = tabName
|
||
}
|
||
function handlerTab(tabName) {
|
||
activeTab.value = tabName
|
||
}
|
||
// 点击风险级别时跳转到对应标签页
|
||
function handleRiskLevelClick(levelType) {
|
||
navigateToTab(levelType)
|
||
}
|
||
|
||
// 计算整体风险严重程度
|
||
const riskSeverity = computed(() => {
|
||
if (!summaryData.value || !summaryData.value.byRiskLevel) return { level: 'low', label: '暂无风险', color: 'green' }
|
||
|
||
// 获取各风险级别的命中项数量
|
||
const highRiskCount = summaryData.value.byRiskLevel.find(item => item.id === 'high')?.triggered || 0
|
||
const mediumRiskCount = summaryData.value.byRiskLevel.find(item => item.id === 'medium')?.triggered || 0
|
||
const lowRiskCount = summaryData.value.byRiskLevel.find(item => item.id === 'low')?.triggered || 0
|
||
|
||
// 计算总命中项数量
|
||
const totalTriggered = highRiskCount + mediumRiskCount + lowRiskCount
|
||
|
||
// 根据风险命中情况确定严重程度
|
||
if (highRiskCount > 0) {
|
||
return { level: 'critical', label: '严重风险', color: 'red', count: totalTriggered }
|
||
} else if (mediumRiskCount > 0) {
|
||
return { level: 'warning', label: '中度风险', color: 'orange', count: totalTriggered }
|
||
} else if (lowRiskCount > 0) {
|
||
return { level: 'notice', label: '轻微风险', color: 'blue', count: totalTriggered }
|
||
} else {
|
||
return { level: 'safe', label: '安全状态', color: 'green', count: 0 }
|
||
}
|
||
})
|
||
|
||
// 根据命中率确定颜色
|
||
function getRateColor(triggered, total) {
|
||
if (total === 0) return 'gray'
|
||
const rate = triggered / total
|
||
if (rate === 0) return 'gray'
|
||
if (rate < 0.3) return 'blue'
|
||
if (rate < 0.6) return 'orange'
|
||
return 'red'
|
||
}
|
||
|
||
// 初始化
|
||
onMounted(() => {
|
||
const processed = processData(dataValue.value)
|
||
processedData.value = processed
|
||
summaryData.value = generateSummary(processed)
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div class="card">
|
||
<div class="flex flex-col">
|
||
<!-- 风险总结头部 -->
|
||
<div class="mb-5">
|
||
<div class="flex items-center justify-between">
|
||
<div class="text-lg font-bold text-gray-800">借贷违约失信风险查询结果</div>
|
||
<div
|
||
:class="[
|
||
'px-3 py-1 rounded-full text-white text-sm font-semibold shadow-sm transform transition-all duration-300 hover:scale-105',
|
||
riskSeverity.level === 'critical'
|
||
? 'bg-gradient-to-r from-red-500 to-red-600'
|
||
: riskSeverity.level === 'warning'
|
||
? 'bg-gradient-to-r from-orange-400 to-orange-500'
|
||
: riskSeverity.level === 'notice'
|
||
? 'bg-gradient-to-r from-blue-400 to-blue-500'
|
||
: 'bg-gradient-to-r from-green-400 to-green-500',
|
||
]"
|
||
>
|
||
{{ riskSeverity.label }}
|
||
</div>
|
||
</div>
|
||
<div
|
||
v-if="riskSeverity.count > 0"
|
||
class="mt-3 p-4 rounded-lg shadow-md border-l-4 transform transition-all duration-300"
|
||
:class="[
|
||
riskSeverity.level === 'critical'
|
||
? 'border-red-500 bg-gradient-to-br from-white to-red-50'
|
||
: riskSeverity.level === 'warning'
|
||
? 'border-orange-500 bg-gradient-to-br from-white to-orange-50'
|
||
: 'border-blue-500 bg-gradient-to-br from-white to-blue-50',
|
||
]"
|
||
>
|
||
<div class="flex items-center">
|
||
<div
|
||
class="w-12 h-12 flex-shrink-0 mr-4 flex items-center justify-center rounded-full shadow-inner transform transition-all duration-300 hover:scale-110"
|
||
:class="[
|
||
riskSeverity.level === 'critical'
|
||
? 'bg-red-100 text-red-700'
|
||
: riskSeverity.level === 'warning'
|
||
? 'bg-orange-100 text-orange-700'
|
||
: 'bg-blue-100 text-blue-700',
|
||
]"
|
||
>
|
||
<span class="text-xl font-bold">{{ riskSeverity.count }}</span>
|
||
</div>
|
||
<div>
|
||
<div
|
||
class="text-base font-medium"
|
||
:class="[
|
||
riskSeverity.level === 'critical'
|
||
? 'text-red-700'
|
||
: riskSeverity.level === 'warning'
|
||
? 'text-orange-700'
|
||
: 'text-blue-700',
|
||
]"
|
||
>
|
||
检测到 {{ riskSeverity.count }} 项风险
|
||
</div>
|
||
<p class="text-sm text-gray-600 mt-1">
|
||
{{
|
||
riskSeverity.level === 'critical'
|
||
? '存在无法收回风险,请立即处理'
|
||
: riskSeverity.level === 'warning'
|
||
? '存在严重逾期风险,建议尽快处理'
|
||
: '存在短期逾期风险,请注意处理'
|
||
}}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div
|
||
v-else
|
||
class="mt-3 p-4 rounded-lg shadow-md border-l-4 border-green-500 bg-gradient-to-br from-white to-green-50 transform transition-all duration-300"
|
||
>
|
||
<div class="flex items-center">
|
||
<div
|
||
class="w-12 h-12 flex-shrink-0 mr-4 bg-green-100 flex items-center justify-center rounded-full shadow-inner transform transition-all duration-300 hover:scale-110"
|
||
>
|
||
<svg
|
||
class="w-7 h-7 text-green-600"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
>
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||
</svg>
|
||
</div>
|
||
<div>
|
||
<div class="text-base font-medium text-green-700">未检测到风险</div>
|
||
<p class="text-sm text-gray-600 mt-1">信用状况良好,无逾期或违约记录</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 标签页导航 -->
|
||
<div class="grid grid-cols-4 w-full border-b mb-5">
|
||
<button
|
||
v-for="(tab, key) in {
|
||
summary: '汇总',
|
||
low: '短期逾期',
|
||
medium: '严重逾期',
|
||
high: '无法收回',
|
||
}"
|
||
:key="key"
|
||
class="px-2 py-3 text-center cursor-pointer transition-all duration-300 font-medium text-xs sm:text-sm relative border-b-2"
|
||
:class="[
|
||
key === 'summary'
|
||
? activeTab === key
|
||
? 'border-blue-500 text-blue-600'
|
||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||
: key === 'low'
|
||
? activeTab === key
|
||
? 'border-blue-500 text-blue-600'
|
||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||
: key === 'medium'
|
||
? activeTab === key
|
||
? 'border-orange-500 text-orange-600'
|
||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||
: activeTab === key
|
||
? 'border-red-500 text-red-600'
|
||
: 'border-transparent text-gray-500 hover:text-gray-700',
|
||
]"
|
||
@click="handlerTab(key)"
|
||
>
|
||
{{ tab }}
|
||
<span
|
||
v-if="
|
||
key !== 'summary' &&
|
||
summaryData.byRiskLevel &&
|
||
summaryData.byRiskLevel.find(level => level.id === key && level.triggered > 0)
|
||
"
|
||
:class="[
|
||
'absolute -top-1 -right-1 inline-flex items-center justify-center w-4 h-4 text-xs font-bold leading-none text-white rounded-full shadow-sm transform transition-all duration-300 hover:scale-110',
|
||
key === 'low' ? 'bg-blue-500' : key === 'medium' ? 'bg-orange-500' : 'bg-red-500',
|
||
]"
|
||
>
|
||
{{ summaryData.byRiskLevel.find(level => level.id === key).triggered }}
|
||
</span>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 标签页内容 -->
|
||
<div class="mt-2">
|
||
<!-- 汇总页 -->
|
||
<div v-if="activeTab === 'summary'" class="space-y-6">
|
||
<!-- 风险级别汇总卡片 -->
|
||
<div class="grid grid-cols-1 gap-4">
|
||
<div
|
||
v-for="levelSummary in summaryData.byRiskLevel"
|
||
:key="levelSummary.id"
|
||
:class="[
|
||
'p-4 rounded-lg shadow-md border-l-4 transition-all duration-300 hover:shadow-lg transform hover:-translate-y-1 cursor-pointer',
|
||
levelSummary.id === 'low'
|
||
? 'border-blue-500 bg-gradient-to-br from-white to-blue-50'
|
||
: levelSummary.id === 'medium'
|
||
? 'border-orange-500 bg-gradient-to-br from-white to-orange-50'
|
||
: 'border-red-500 bg-gradient-to-br from-white to-red-50',
|
||
]"
|
||
@click="handleRiskLevelClick(levelSummary.id)"
|
||
>
|
||
<div class="flex justify-between items-start">
|
||
<h3
|
||
class="text-base font-semibold"
|
||
:class="
|
||
levelSummary.id === 'low'
|
||
? 'text-blue-700'
|
||
: levelSummary.id === 'medium'
|
||
? 'text-orange-700'
|
||
: 'text-red-700'
|
||
"
|
||
>
|
||
{{ levelSummary.label }}
|
||
</h3>
|
||
<span
|
||
:class="[
|
||
'text-xs px-2 py-1 rounded-full font-medium shadow-sm',
|
||
levelSummary.triggered > 0
|
||
? levelSummary.id === 'low'
|
||
? 'bg-blue-100 text-blue-800'
|
||
: levelSummary.id === 'medium'
|
||
? 'bg-orange-100 text-orange-800'
|
||
: 'bg-red-100 text-red-800'
|
||
: 'bg-gray-100 text-gray-600',
|
||
]"
|
||
>
|
||
{{ levelSummary.triggered > 0 ? '已命中' : '未命中' }}
|
||
</span>
|
||
</div>
|
||
<div class="mt-3 flex items-end justify-between">
|
||
<div>
|
||
<p class="text-xs text-gray-600">命中项</p>
|
||
<p
|
||
class="text-xl font-bold"
|
||
:class="
|
||
levelSummary.triggered > 0
|
||
? levelSummary.id === 'low'
|
||
? 'text-blue-600'
|
||
: levelSummary.id === 'medium'
|
||
? 'text-orange-600'
|
||
: 'text-red-600'
|
||
: 'text-gray-500'
|
||
"
|
||
>
|
||
{{ levelSummary.triggered }} / {{ levelSummary.total }}
|
||
</p>
|
||
</div>
|
||
<button
|
||
class="text-xs px-3 py-1.5 rounded-full focus:outline-none shadow-sm transition-all duration-300 hover:shadow-md"
|
||
:class="
|
||
levelSummary.id === 'low'
|
||
? 'bg-blue-100 text-blue-600 hover:bg-blue-200'
|
||
: levelSummary.id === 'medium'
|
||
? 'bg-orange-100 text-orange-600 hover:bg-orange-200'
|
||
: 'bg-red-100 text-red-600 hover:bg-red-200'
|
||
"
|
||
@click.stop="handleRiskLevelClick(levelSummary.id)"
|
||
>
|
||
查看详情
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 风险类型汇总 -->
|
||
<div class="">
|
||
<h3 class="text-base font-semibold mb-3 text-gray-700 border-l-4 border-gray-400 pl-2">风险主体分布</h3>
|
||
<!-- 移动端卡片布局替代表格 -->
|
||
<div class="space-y-3">
|
||
<div
|
||
v-for="typeSummary in summaryData.byRiskType"
|
||
:key="typeSummary.id"
|
||
class="p-3 bg-white rounded-lg border border-gray-200 shadow-sm relative overflow-hidden transition-all duration-300 hover:shadow-md"
|
||
>
|
||
<div class="flex justify-between items-center">
|
||
<div class="text-sm font-medium text-gray-900">
|
||
{{ typeSummary.label }}
|
||
</div>
|
||
<div class="flex items-center space-x-1">
|
||
<span class="text-xs text-gray-500">命中项</span>
|
||
<span
|
||
class="text-xs font-medium px-1.5 py-0.5 rounded-full"
|
||
:class="[
|
||
typeSummary.triggered > 0
|
||
? getRateColor(typeSummary.triggered, typeSummary.total) === 'red'
|
||
? 'bg-red-100 text-red-700'
|
||
: getRateColor(typeSummary.triggered, typeSummary.total) === 'orange'
|
||
? 'bg-orange-100 text-orange-700'
|
||
: 'bg-blue-100 text-blue-700'
|
||
: 'bg-gray-100 text-gray-500',
|
||
]"
|
||
>
|
||
{{ typeSummary.triggered }}
|
||
</span>
|
||
<span class="text-xs text-gray-500">/</span>
|
||
<span class="text-xs text-gray-500">{{ typeSummary.total }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="w-full h-2 bg-gray-100 rounded-full mt-2 overflow-hidden">
|
||
<div
|
||
class="h-full rounded-full transition-all duration-500"
|
||
:class="[
|
||
typeSummary.triggered > 0
|
||
? getRateColor(typeSummary.triggered, typeSummary.total) === 'red'
|
||
? 'bg-gradient-to-r from-red-400 to-red-500'
|
||
: getRateColor(typeSummary.triggered, typeSummary.total) === 'orange'
|
||
? 'bg-gradient-to-r from-orange-400 to-orange-500'
|
||
: 'bg-gradient-to-r from-blue-400 to-blue-500'
|
||
: 'bg-gray-200',
|
||
]"
|
||
:style="{
|
||
width: `${(typeSummary.triggered / Math.max(1, typeSummary.total)) * 100}%`,
|
||
}"
|
||
></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 金融机构汇总 -->
|
||
<div class="">
|
||
<h3 class="text-base font-semibold mb-3 text-gray-700 border-l-4 border-gray-400 pl-2">机构风险分布</h3>
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<div
|
||
v-for="institutionSummary in summaryData.byInstitution"
|
||
:key="institutionSummary.id"
|
||
class="flex flex-col p-3 rounded-lg border border-gray-200 bg-white shadow-sm transition-all duration-300 hover:shadow-md"
|
||
>
|
||
<div class="flex items-center mb-2">
|
||
<div
|
||
class="mr-2 flex-shrink-0 w-8 h-8 flex items-center justify-center rounded-full shadow-inner"
|
||
:class="[
|
||
institutionSummary.triggered > 0
|
||
? getRateColor(institutionSummary.triggered, institutionSummary.total) === 'red'
|
||
? 'bg-red-100'
|
||
: getRateColor(institutionSummary.triggered, institutionSummary.total) === 'orange'
|
||
? 'bg-orange-100'
|
||
: 'bg-blue-100'
|
||
: 'bg-gray-100',
|
||
]"
|
||
>
|
||
<span
|
||
class="text-sm font-bold"
|
||
:class="[
|
||
institutionSummary.triggered > 0
|
||
? getRateColor(institutionSummary.triggered, institutionSummary.total) === 'red'
|
||
? 'text-red-600'
|
||
: getRateColor(institutionSummary.triggered, institutionSummary.total) === 'orange'
|
||
? 'text-orange-600'
|
||
: 'text-blue-600'
|
||
: 'text-gray-500',
|
||
]"
|
||
>
|
||
{{ institutionSummary.triggered }}
|
||
</span>
|
||
</div>
|
||
<div class="flex flex-col">
|
||
<h4 class="text-xs font-medium text-gray-900 truncate max-w-[100px]">
|
||
{{ institutionSummary.label }}
|
||
</h4>
|
||
<div class="flex items-center mt-1 text-xs text-gray-500">
|
||
<span>命中项:{{ institutionSummary.triggered }}/{{ institutionSummary.total }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="w-full h-1.5 bg-gray-100 rounded-full">
|
||
<div
|
||
class="h-full rounded-full transition-all duration-500"
|
||
:class="[
|
||
institutionSummary.triggered > 0
|
||
? getRateColor(institutionSummary.triggered, institutionSummary.total) === 'red'
|
||
? 'bg-gradient-to-r from-red-400 to-red-500'
|
||
: getRateColor(institutionSummary.triggered, institutionSummary.total) === 'orange'
|
||
? 'bg-gradient-to-r from-orange-400 to-orange-500'
|
||
: 'bg-gradient-to-r from-blue-400 to-blue-500'
|
||
: 'bg-gray-200',
|
||
]"
|
||
:style="{
|
||
width: `${(institutionSummary.triggered / Math.max(1, institutionSummary.total)) * 100}%`,
|
||
}"
|
||
></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 风险详情页 -->
|
||
<div v-else>
|
||
<div class="mb-3">
|
||
<h3
|
||
class="text-lg font-semibold"
|
||
:class="
|
||
activeTab === 'low' ? 'text-blue-700' : activeTab === 'medium' ? 'text-orange-700' : 'text-red-700'
|
||
"
|
||
>
|
||
{{ tabConfigs[activeTab].title }}
|
||
</h3>
|
||
|
||
<p class="text-xs text-gray-600 mt-1">
|
||
{{ tabConfigs[activeTab].description }}
|
||
</p>
|
||
</div>
|
||
|
||
<!-- 风险详情 - 身份证风险 -->
|
||
<div class="mb-4">
|
||
<h4 class="text-base font-medium text-gray-700 mb-2">
|
||
{{ riskTypeConfigs.idCard.title }}
|
||
</h4>
|
||
<!-- 移动端卡片列表 -->
|
||
<div class="space-y-2">
|
||
<div
|
||
v-for="item in processedData[activeTab].filter(i => i.riskType === 'idCard')"
|
||
:key="item.id"
|
||
:class="[
|
||
'p-3 rounded-lg border shadow-sm',
|
||
item.isTriggered ? 'bg-red-50 border-red-200' : 'bg-white border-gray-200',
|
||
]"
|
||
>
|
||
<div class="flex justify-between items-start mb-2">
|
||
<div>
|
||
<div class="text-sm font-medium text-gray-900">
|
||
{{ item.institutionLabel }}{{ item.levelLabel }}
|
||
</div>
|
||
<!-- <div class="text-xs text-gray-500">{{ item.fieldName }}</div> -->
|
||
</div>
|
||
<span
|
||
:class="[
|
||
'px-2 py-1 text-xs leading-5 font-semibold rounded-full',
|
||
item.isTriggered
|
||
? item.levelType === 'low'
|
||
? 'bg-blue-100 text-blue-800'
|
||
: item.levelType === 'medium'
|
||
? 'bg-orange-100 text-orange-800'
|
||
: 'bg-red-100 text-red-800'
|
||
: 'bg-gray-100 text-gray-800',
|
||
]"
|
||
>
|
||
{{ item.isTriggered ? '命中' : '无' }}
|
||
</span>
|
||
</div>
|
||
<div class="grid grid-cols-2 gap-2 text-xs">
|
||
<div>
|
||
<span class="text-gray-500">发生次数:</span>
|
||
<span :class="item.isTriggered ? 'text-red-600 font-medium' : 'text-gray-500'">
|
||
{{ item.count === '0' ? '-' : item.count }}
|
||
</span>
|
||
</div>
|
||
<div>
|
||
<span class="text-gray-500">最近发生:</span>
|
||
<span :class="item.isTriggered ? 'text-red-600 font-medium' : 'text-gray-500'">
|
||
{{ item.time !== '0' ? `近${item.time}年内` : '-' }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div
|
||
v-if="processedData[activeTab].filter(i => i.riskType === 'idCard').length === 0"
|
||
class="p-3 text-center text-sm text-gray-500 bg-gray-50 rounded-lg border border-gray-200"
|
||
>
|
||
{{ riskTypeConfigs.idCard.emptyText }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 风险详情 - 手机号风险 -->
|
||
<div>
|
||
<h4 class="text-base font-medium text-gray-700 mb-2">
|
||
{{ riskTypeConfigs.mobile.title }}
|
||
</h4>
|
||
<!-- 移动端卡片列表 -->
|
||
<div class="space-y-2">
|
||
<div
|
||
v-for="item in processedData[activeTab].filter(i => i.riskType === 'mobile')"
|
||
:key="item.id"
|
||
:class="[
|
||
'p-3 rounded-lg border shadow-sm',
|
||
item.isTriggered ? 'bg-red-50 border-red-200' : 'bg-white border-gray-200',
|
||
]"
|
||
>
|
||
<div class="flex justify-between items-start mb-2">
|
||
<div>
|
||
<div class="text-sm font-medium text-gray-900">
|
||
{{ item.institutionLabel }}{{ item.levelLabel }}
|
||
</div>
|
||
<!-- <div class="text-xs text-gray-500">{{ item.fieldName }}</div> -->
|
||
</div>
|
||
<span
|
||
:class="[
|
||
'px-2 py-1 text-xs leading-5 font-semibold rounded-full',
|
||
item.isTriggered
|
||
? item.levelType === 'low'
|
||
? 'bg-blue-100 text-blue-800'
|
||
: item.levelType === 'medium'
|
||
? 'bg-orange-100 text-orange-800'
|
||
: 'bg-red-100 text-red-800'
|
||
: 'bg-gray-100 text-gray-800',
|
||
]"
|
||
>
|
||
{{ item.isTriggered ? '命中' : '无' }}
|
||
</span>
|
||
</div>
|
||
<div class="grid grid-cols-2 gap-2 text-xs">
|
||
<div>
|
||
<span class="text-gray-500">发生次数:</span>
|
||
<span :class="item.isTriggered ? 'text-red-600 font-medium' : 'text-gray-500'">
|
||
{{ item.count === '0' ? '-' : item.count }}
|
||
</span>
|
||
</div>
|
||
<div>
|
||
<span class="text-gray-500">最近发生:</span>
|
||
<span :class="item.isTriggered ? 'text-red-600 font-medium' : 'text-gray-500'">
|
||
{{ item.time !== '0' ? `近${item.time}年内` : '-' }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div
|
||
v-if="processedData[activeTab].filter(i => i.riskType === 'mobile').length === 0"
|
||
class="p-3 text-center text-sm text-gray-500 bg-gray-50 rounded-lg border border-gray-200"
|
||
>
|
||
{{ riskTypeConfigs.mobile.emptyText }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style lang="scss" scoped>
|
||
.card {
|
||
background-color: white;
|
||
border-radius: 0.75rem;
|
||
box-shadow:
|
||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||
padding: 1.25rem;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
/* 风险颜色定义 */
|
||
.text-blue-600 {
|
||
color: #2563eb;
|
||
}
|
||
|
||
.text-orange-600 {
|
||
color: #ea580c;
|
||
}
|
||
|
||
.text-red-600 {
|
||
color: #dc2626;
|
||
}
|
||
|
||
.bg-blue-50 {
|
||
background-color: #eff6ff;
|
||
}
|
||
|
||
.bg-orange-50 {
|
||
background-color: #fff7ed;
|
||
}
|
||
|
||
.bg-red-50 {
|
||
background-color: #fef2f2;
|
||
}
|
||
|
||
.bg-blue-100 {
|
||
background-color: #dbeafe;
|
||
}
|
||
|
||
.bg-orange-100 {
|
||
background-color: #ffedd5;
|
||
}
|
||
|
||
.bg-red-100 {
|
||
background-color: #fee2e2;
|
||
}
|
||
|
||
.border-blue-500 {
|
||
border-color: #3b82f6;
|
||
}
|
||
|
||
.border-orange-500 {
|
||
border-color: #f97316;
|
||
}
|
||
|
||
.border-red-500 {
|
||
border-color: #ef4444;
|
||
}
|
||
|
||
.text-blue-700 {
|
||
color: #1d4ed8;
|
||
}
|
||
|
||
.text-orange-700 {
|
||
color: #c2410c;
|
||
}
|
||
|
||
.text-red-700 {
|
||
color: #b91c1c;
|
||
}
|
||
|
||
.text-blue-800 {
|
||
color: #1e40af;
|
||
}
|
||
|
||
.text-orange-800 {
|
||
color: #9a3412;
|
||
}
|
||
|
||
.text-red-800 {
|
||
color: #991b1b;
|
||
}
|
||
|
||
.border-red-200 {
|
||
border-color: #fecaca;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0%,
|
||
100% {
|
||
opacity: 1;
|
||
}
|
||
|
||
50% {
|
||
opacity: 0.8;
|
||
}
|
||
}
|
||
|
||
.animate-pulse {
|
||
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||
}
|
||
</style>
|