Files
report_viewer/CSpecialList.vue
2025-11-17 12:49:59 +08:00

1049 lines
37 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>