Files
report_viewer/src/ui/JRZQ8A2D.vue
2025-12-18 15:39:43 +08:00

823 lines
36 KiB
Vue
Raw 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.

<template>
<div class="card special-list-verification">
<!-- 头部标题和最终决策 -->
<div class="mb-4 relative">
<div class="flex items-center mb-3">
<div class="w-8 h-8 flex items-center justify-center mr-3">
<img src="@/assets/images/report/gazdryhycp.png" alt="特殊名单验证" class="w-8 h-8 object-contain" />
</div>
<span class="font-bold text-gray-800">特殊名单验证</span>
</div>
<!-- 最终评分 -->
<div v-if="finalWeight" class="bg-blue-50 rounded-lg p-4 border border-[#2B79EE8F] mb-3">
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600">最终规则评分</span>
<span class="text-xl font-bold text-gray-800">{{ finalWeight }}</span>
</div>
</div>
<!-- 命中规则列表 -->
<div v-if="hitRules.length > 0">
<LTitle title="命中项目" />
<div class="mt-3 space-y-2">
<div v-for="rule in hitRules" :key="rule.ruleId"
class="bg-white rounded-xl p-4 border border-gray-200 relative">
<!-- <div class="absolute top-0 right-0">
<div class="px-2 py-1 text-sm text-white rounded-bl-xl rounded-tr-xl bg-orange-500">
权重: {{ rule.weight }}
</div>
</div> -->
<div class="flex items-center">
<div class="w-10 h-10 mr-4">
<img :src="getRuleIcon()" alt="规则" class="w-10 h-10 object-contain" />
</div>
<div class="flex-1">
<div class="font-bold text-gray-800">{{ rule.name }}</div>
</div>
</div>
</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-sm 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-sm font-bold leading-none text-white rounded-full',
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="[
'bg-white rounded-xl border border-gray-200 p-4 transition-all duration-300 hover:shadow-lg cursor-pointer relative',
levelSummary.id === 'low'
? 'border-l-4 border-l-blue-500'
: levelSummary.id === 'medium'
? 'border-l-4 border-l-orange-500'
: 'border-l-4 border-l-red-500',
]" @click="handleRiskLevelClick(levelSummary.id)">
<div class="absolute top-0 right-0">
<div :class="[
'px-2 py-1 text-sm text-white rounded-bl-xl rounded-tr-xl',
levelSummary.triggered > 0
? levelSummary.id === 'low'
? 'bg-blue-500'
: levelSummary.id === 'medium'
? 'bg-orange-500'
: 'bg-red-500'
: 'bg-gray-400',
]">
{{ levelSummary.triggered > 0 ? '已命中' : '未命中' }}
</div>
</div>
<div class="flex items-center mb-3">
<div class="w-8 h-8 flex items-center justify-center mr-3">
<img :src="getRiskLevelIcon(levelSummary.id)" :alt="levelSummary.label"
class="w-8 h-8 object-contain" />
</div>
<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>
</div>
<div class="mt-3 flex items-end justify-between">
<div>
<p class="text-sm 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-sm px-3 py-1.5 rounded-full focus:outline-none transition-all duration-300"
: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="">
<LTitle title="风险主体分布" />
<div class="space-y-3 mt-3">
<div v-for="typeSummary in summaryData.byRiskType" :key="typeSummary.id"
class="p-4 bg-white rounded-xl border border-gray-200 shadow-sm">
<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-sm text-gray-500">命中项</span>
<span class="text-sm 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-sm text-gray-500">/</span>
<span class="text-sm 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-red-500'
: getRateColor(typeSummary.triggered, typeSummary.total) === 'orange'
? 'bg-orange-500'
: 'bg-blue-500'
: 'bg-gray-200',
]" :style="{
width: `${(typeSummary.triggered / Math.max(1, typeSummary.total)) * 100}%`,
}"></div>
</div>
</div>
</div>
</div>
<!-- 金融机构汇总 -->
<div class="">
<LTitle title="机构风险分布" />
<div class="grid grid-cols-2 gap-3 mt-3">
<div v-for="institutionSummary in summaryData.byInstitution" :key="institutionSummary.id"
class="flex flex-col p-4 rounded-xl border border-gray-200 bg-white shadow-sm">
<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"
: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-sm font-medium text-gray-900 truncate max-w-[100px]">
{{ institutionSummary.label }}
</h4>
<div class="flex items-center mt-1 text-sm 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-red-500'
: getRateColor(institutionSummary.triggered, institutionSummary.total) === 'orange'
? 'bg-orange-500'
: 'bg-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-sm 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="[
'rounded-xl p-4 relative',
item.isTriggered
? item.levelType === 'low'
? 'bg-[#E3F2FD] border border-blue-200'
: item.levelType === 'medium'
? 'bg-[#FFF3E0] border border-orange-200'
: 'bg-[#FFF0F0] border border-red-200'
: 'bg-[#F0FFF0] border border-green-200',
]">
<div class="absolute top-0 right-0">
<div :class="[
'px-2 py-1 text-sm text-white rounded-bl-xl rounded-tr-xl',
item.isTriggered
? item.levelType === 'low'
? 'bg-blue-500'
: item.levelType === 'medium'
? 'bg-orange-500'
: 'bg-red-500'
: 'bg-[#4CAF50]',
]">
{{ item.isTriggered ? '命中' : '无' }}
</div>
</div>
<div class="flex items-center">
<div class="w-10 h-10 mr-4">
<img :src="getItemIcon(item.isTriggered, item.levelType)"
:alt="item.institutionLabel" class="w-10 h-10 object-contain" />
</div>
<div class="flex-1">
<div class="text-sm font-bold text-gray-800">
{{ item.institutionLabel }}{{ item.levelLabel }}
</div>
</div>
</div>
<div class="grid grid-cols-2 gap-2 text-sm mt-3">
<div>
<span class="text-gray-500">发生次数:</span>
<span
:class="item.isTriggered ? (item.levelType === 'low' ? 'text-blue-600' : item.levelType === 'medium' ? 'text-orange-600' : 'text-red-600') + ' font-medium' : 'text-gray-500'">
{{ item.count === '0' || !item.count ? '-' : item.count }}
</span>
</div>
<div>
<span class="text-gray-500">最近发生:</span>
<span
:class="item.isTriggered ? (item.levelType === 'low' ? 'text-blue-600' : item.levelType === 'medium' ? 'text-orange-600' : 'text-red-600') + ' font-medium' : 'text-gray-500'">
{{ item.time && 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-xl 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="[
'rounded-xl p-4 relative',
item.isTriggered
? item.levelType === 'low'
? 'bg-[#E3F2FD] border border-blue-200'
: item.levelType === 'medium'
? 'bg-[#FFF3E0] border border-orange-200'
: 'bg-[#FFF0F0] border border-red-200'
: 'bg-[#F0FFF0] border border-green-200',
]">
<div class="absolute top-0 right-0">
<div :class="[
'px-2 py-1 text-sm text-white rounded-bl-xl rounded-tr-xl',
item.isTriggered
? item.levelType === 'low'
? 'bg-blue-500'
: item.levelType === 'medium'
? 'bg-orange-500'
: 'bg-red-500'
: 'bg-[#4CAF50]',
]">
{{ item.isTriggered ? '命中' : '无' }}
</div>
</div>
<div class="flex items-center">
<div class="w-10 h-10 mr-4">
<img :src="getItemIcon(item.isTriggered, item.levelType)"
:alt="item.institutionLabel" class="w-10 h-10 object-contain" />
</div>
<div class="flex-1">
<div class="text-sm font-bold text-gray-800">
{{ item.institutionLabel }}{{ item.levelLabel }}
</div>
</div>
</div>
<div class="grid grid-cols-2 gap-2 text-sm mt-3">
<div>
<span class="text-gray-500">发生次数:</span>
<span
:class="item.isTriggered ? (item.levelType === 'low' ? 'text-blue-600' : item.levelType === 'medium' ? 'text-orange-600' : 'text-red-600') + ' font-medium' : 'text-gray-500'">
{{ item.count === '0' || !item.count ? '-' : item.count }}
</span>
</div>
<div>
<span class="text-gray-500">最近发生:</span>
<span
:class="item.isTriggered ? (item.levelType === 'low' ? 'text-blue-600' : item.levelType === 'medium' ? 'text-orange-600' : 'text-red-600') + ' font-medium' : 'text-gray-500'">
{{ item.time && 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-xl border border-gray-200">
{{ riskTypeConfigs.mobile.emptyText }}
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import LTitle from '@/components/LTitle.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: () => { },
},
})
// 当前激活的标签页
const activeTab = ref('summary')
// 风险类型定义
const riskTypes = {
idCard: {
label: '身份证风险',
prefix: 'id_',
},
mobile: {
label: '手机号风险',
prefix: 'cell_',
},
}
// 金融机构定义
const institutions = {
court_bad: {
label: '法院失信人',
levels: ['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: '展示所有短期逾期相关记录',
},
medium: {
title: '严重逾期详情',
description: '展示所有严重逾期相关记录',
},
high: {
title: '无法收回详情',
description: '展示所有无法收回相关记录',
},
}
// 风险类型配置
const riskTypeConfigs = {
idCard: {
title: '身份证风险信息',
emptyText: '暂无身份证风险记录',
},
mobile: {
title: '手机号风险信息',
emptyText: '暂无手机号风险记录',
},
}
// 处理数据并分类
function processData(data) {
const result = {
low: [],
medium: [],
high: [],
}
// 获取 id 和 cell 数据
const idData = data.id || {}
const cellData = data.cell || {}
// 遍历风险类型(身份证/手机号)
Object.entries(riskTypes).forEach(([riskTypeKey, riskType]) => {
const sourceData = riskTypeKey === 'idCard' ? idData : cellData
// 遍历金融机构
Object.entries(institutions).forEach(([institutionKey, institution]) => {
// 遍历当前机构支持的风险级别
institution.levels.forEach(levelKey => {
// 构建字段名 - 特殊处理court_bad和court_executed
let fieldBase
if (institutionKey === 'court_bad') {
fieldBase = `court_bad`
} else if (institutionKey === 'court_executed') {
fieldBase = `court_executed`
} else {
fieldBase = `${institutionKey}_${levelKey}`
}
const valueField = fieldBase
const timeField = `${fieldBase}_time`
const countField = `${fieldBase}_allnum`
// 获取值0表示命中空表示未命中
const value = sourceData[valueField] || ''
const time = sourceData[timeField] || '0'
const count = sourceData[countField] || '0'
// 创建记录对象
const record = {
id: `${riskTypeKey}_${institutionKey}_${levelKey || 'executed'}`,
riskType: riskTypeKey,
riskTypeLabel: riskType.label,
institution: institutionKey,
institutionLabel: institution.label,
level: levelKey || '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: value,
time: time,
count: count,
// 根据文档0表示命中空表示未命中
isTriggered: value === '0',
fieldName: valueField,
}
// 根据风险级别分类
if (institutionKey === 'court_executed' || institutionKey === 'court_bad') {
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({})
// 获取原始数据
const rawData = computed(() => {
return props.data?.data?.data || props.data?.data || props.data || {}
})
// 最终决策和评分
const finalDecision = computed(() => {
return rawData.value.Rule_final_decision || ''
})
const finalWeight = computed(() => {
return rawData.value.Rule_final_weight || ''
})
// 解析命中规则
const hitRules = computed(() => {
const rules = []
const data = rawData.value
// 遍历所有可能的规则字段
// Rule_name_odr* 和 Rule_weight_odr* 是动态的
const ruleNamePattern = /^Rule_name_(odr\d+)$/
const ruleWeightPattern = /^Rule_weight_(odr\d+)$/
// 收集所有规则名称
const ruleMap = {}
Object.keys(data).forEach(key => {
const nameMatch = key.match(ruleNamePattern)
if (nameMatch) {
const ruleId = nameMatch[1]
if (!ruleMap[ruleId]) {
ruleMap[ruleId] = { ruleId, name: '', weight: '' }
}
ruleMap[ruleId].name = data[key] || ''
}
const weightMatch = key.match(ruleWeightPattern)
if (weightMatch) {
const ruleId = weightMatch[1]
if (!ruleMap[ruleId]) {
ruleMap[ruleId] = { ruleId, name: '', weight: '' }
}
ruleMap[ruleId].weight = data[key] || ''
}
})
// 转换为数组,只包含有名称的规则
Object.values(ruleMap).forEach(rule => {
if (rule.name) {
rules.push(rule)
}
})
return rules
})
// 跳转到指定标签页
function handlerTab(tabName) {
activeTab.value = tabName
}
// 点击风险级别时跳转到对应标签页
function handleRiskLevelClick(levelType) {
handlerTab(levelType)
}
// 根据命中率确定颜色
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'
}
function getDecisionClass(decision) {
const map = {
'Accept': 'bg-[#4CAF50]',
'Reject': 'bg-[#E53935]',
'Review': 'bg-orange-500',
}
return map[decision] || 'bg-gray-500'
}
// 获取规则图标
function getRuleIcon() {
return new URL('@/assets/images/report/zfx.png', import.meta.url).href
}
// 获取风险级别图标
function getRiskLevelIcon(levelId) {
const iconMap = {
'low': () => new URL('@/assets/images/report/zq.png', import.meta.url).href,
'medium': () => new URL('@/assets/images/report/zfx.png', import.meta.url).href,
'high': () => new URL('@/assets/images/report/gfx.png', import.meta.url).href,
}
return iconMap[levelId] ? iconMap[levelId]() : iconMap['medium']()
}
// 获取项目图标
function getItemIcon(isTriggered, levelType) {
if (!isTriggered) {
return new URL('@/assets/images/report/zq.png', import.meta.url).href
}
if (levelType === 'low') {
return new URL('@/assets/images/report/zfx.png', import.meta.url).href
}
if (levelType === 'high') {
return new URL('@/assets/images/report/gfx.png', import.meta.url).href
}
return new URL('@/assets/images/report/zfx.png', import.meta.url).href
}
// 计算风险评分0-100分分数越高越安全
const riskScore = computed(() => {
if (!summaryData.value || !summaryData.value.byRiskLevel) return 100
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 (totalTriggered === 0) {
return 100 // 无风险
}
// 根据风险级别计算分数,高风险权重更高
const score = Math.max(10, 100 - (highRiskCount * 30 + mediumRiskCount * 15 + lowRiskCount * 5))
return score
})
// 使用 composable 通知父组件风险评分
useRiskNotifier(props, riskScore)
// 初始化
onMounted(() => {
// 数据可能在不同层级,尝试多种路径
const data = props.data?.data?.data || props.data?.data || props.data || {}
const processed = processData(data)
processedData.value = processed
summaryData.value = generateSummary(processed)
})
// 暴露给父组件
defineExpose({
riskScore
})
</script>
<style lang="scss" scoped>
.special-list-verification {
@apply space-y-4;
}
</style>