Files
report_viewer/src/ui/DWBG9FB3/components/FraudBlacklistSection.vue

220 lines
5.8 KiB
Vue
Raw Normal View History

2026-06-10 12:22:43 +08:00
<template>
<div class="gamma-card">
<div class="gamma-title"><span>👤</span> 欺诈黑名单</div>
<div class="gamma-subtitle"><span></span> 风险等级</div>
<div class="fraud-risk-circle">
<div class="circle" />
<div class="fraud-risk-text">
<div class="risk-level">{{ gradeText }}</div>
<div class="gamma-small">风险等级</div>
</div>
</div>
<div class="gamma-subtitle"><span></span> 命中统计</div>
<div class="fraud-stats-grid">
<div class="chart-card">
<div class="chart-title">命中总次数</div>
<div class="chart-bars">
<div v-for="bar in hitBars" :key="bar.label" class="bar-group">
<div class="bar-value">{{ bar.value }}</div>
<div class="bar" :style="{ height: bar.height + 'px' }" />
<div class="bar-label">{{ bar.label }}</div>
</div>
</div>
</div>
<div class="chart-card">
<div class="chart-title">命中机构数</div>
<div class="chart-bars">
<div v-for="bar in orgBars" :key="bar.label" class="bar-group">
<div class="bar-value">{{ bar.value }}</div>
<div class="bar" :style="{ height: bar.height + 'px' }" />
<div class="bar-label">{{ bar.label }}</div>
</div>
</div>
</div>
</div>
<div class="gamma-subtitle"><span></span> 命中分布</div>
<div class="chart-card">
<div class="distribution-bars">
<div v-for="(dist, i) in distributions" :key="i" class="distribution-bar-group">
<div class="distribution-bar blue" :style="{ height: dist.d30 + 'px' }" />
<div class="distribution-bar orange" :style="{ height: dist.d90 + 'px' }" />
<div class="distribution-bar yellow" :style="{ height: dist.d180 + 'px' }" />
</div>
</div>
<div class="distribution-labels">
<div v-for="label in FRAUD_DIST_LABELS" :key="label">{{ label }}</div>
</div>
<div class="chart-legend">
<div class="legend-item"><div class="legend-color blue" /><span>近30天</span></div>
<div class="legend-item"><div class="legend-color orange" /><span>近90天</span></div>
<div class="legend-item"><div class="legend-color yellow" /><span>近180天</span></div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { FRAUD_DIST_LABELS, FRAUD_DIST_KEYS, fraudGradeText } from '../reportHelper';
const props = defineProps({
data: { type: Object, default: () => ({}) },
});
const gradeText = computed(() => fraudGradeText(props.data.grade));
function barHeight(val, max) {
if (!max) return 0;
return Math.max(4, Math.round((Number(val) || 0) / max * 120));
}
const hitBars = computed(() => {
const d = props.data;
const values = [
{ label: '近30天', value: d.ha_30d_C ?? 0 },
{ label: '近90天', value: d.ha_90d_C ?? 0 },
{ label: '近180天', value: d.ha_180d_C ?? 0 },
];
const max = Math.max(...values.map((v) => Number(v.value)), 1);
return values.map((v) => ({ ...v, height: barHeight(v.value, max) }));
});
const orgBars = computed(() => {
const d = props.data;
const values = [
{ label: '近30天', value: d.ha_30d_J ?? 0 },
{ label: '近90天', value: d.ha_90d_J ?? 0 },
{ label: '近180天', value: d.ha_180d_J ?? 0 },
];
const max = Math.max(...values.map((v) => Number(v.value)), 1);
return values.map((v) => ({ ...v, height: barHeight(v.value, max) }));
});
const distributions = computed(() => {
const d = props.data;
const max = Math.max(
...FRAUD_DIST_KEYS.flatMap((k) => [d[k.d30], d[k.d90], d[k.d180]].map(Number)),
1,
);
return FRAUD_DIST_KEYS.map((k) => ({
d30: barHeight(d[k.d30], max),
d90: barHeight(d[k.d90], max),
d180: barHeight(d[k.d180], max),
}));
});
</script>
<style lang="scss" scoped>
.fraud-risk-circle {
width: 200px;
height: 200px;
margin: 0 auto 24px;
position: relative;
}
.fraud-risk-circle .circle {
width: 160px;
height: 160px;
border: 8px solid #eee;
border-top-color: #fdd860;
border-right-color: #fdd860;
border-radius: 50%;
margin: 0 auto;
}
.fraud-risk-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
}
.risk-level { font-size: 20px; font-weight: 600; color: #fdd860; }
.fraud-stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
margin-bottom: 16px;
}
.chart-card {
border: 1px solid #eee;
border-radius: 6px;
padding: 16px;
}
.chart-title { text-align: center; margin-bottom: 12px; font-size: 14px; }
.chart-bars {
display: flex;
align-items: flex-end;
justify-content: space-around;
height: 150px;
}
.bar-group { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.bar {
width: 30px;
background: #4a6fd4;
border-radius: 3px 3px 0 0;
}
.bar-label, .bar-value { font-size: 12px; color: #666; }
.distribution-bars {
display: flex;
align-items: flex-end;
justify-content: space-around;
height: 160px;
margin-top: 20px;
}
.distribution-bar-group {
display: flex;
gap: 4px;
align-items: flex-end;
}
.distribution-bar {
width: 20px;
border-radius: 2px 2px 0 0;
}
.distribution-bar.blue { background: #4a6fd4; }
.distribution-bar.orange { background: #f28534; }
.distribution-bar.yellow { background: #f7bc0c; }
.distribution-labels {
display: flex;
justify-content: space-around;
font-size: 10px;
color: #666;
margin-top: 8px;
}
.chart-legend {
display: flex;
justify-content: center;
gap: 16px;
font-size: 12px;
margin-top: 12px;
}
.legend-item { display: flex; align-items: center; gap: 4px; }
.legend-color {
width: 12px;
height: 8px;
border-radius: 2px;
}
.legend-color.blue { background: #4a6fd4; }
.legend-color.orange { background: #f28534; }
.legend-color.yellow { background: #f7bc0c; }
</style>