This commit is contained in:
Mrx
2026-06-10 21:03:31 +08:00
parent 907d277302
commit f069d93d84
18 changed files with 3438 additions and 66 deletions

View File

@@ -0,0 +1,96 @@
<template>
<div class="gamma-grid-3">
<div class="gamma-card info-card">
<div class="gamma-title"><span>👤</span> 用户信息</div>
<div class="info-item">
<span class="info-label">姓名:</span>
<span class="info-value">
{{ maskedName }}
<span v-if="realNameAuth.coincide" class="gamma-tag">身份证姓名一致</span>
</span>
</div>
<div class="info-item"><span class="info-label">性别</span><span>{{ params.sex || '—' }}</span></div>
<div class="info-item"><span class="info-label">年龄</span><span>{{ params.age || '—' }}</span></div>
<div class="info-item"><span class="info-label">身份证号</span><span>{{ maskedIdCard }}</span></div>
<div class="info-item"><span class="info-label">户籍地</span><span>{{ params.location || '—' }}</span></div>
</div>
<div class="gamma-card info-card">
<div class="gamma-title"><span>📱</span> 手机信息</div>
<div class="info-item">
<span class="info-label">手机号:</span>
<span>
{{ maskedMobile }}
<span v-if="mobile3Verify.status === 1" class="gamma-tag">身份证姓名手机号一致</span>
</span>
</div>
<div class="info-item">
<span class="info-label">手机号在网时长:</span>
<span>{{ durationText }} </span>
</div>
<div class="info-item">
<span class="info-label">手机号在网状态:</span>
<span :class="mobile4Verify.status === 2 ? 'gamma-text-danger' : ''">
{{ mobileStatusText(mobile4Verify.status) }}
<span v-if="mobile4Verify.status === 2"> </span>
</span>
</div>
<div class="info-item"><span class="info-label">手机号运营商:</span><span>{{ params.carrier || '—' }}</span></div>
<div class="info-item"><span class="info-label">手机号归属地:</span><span>{{ params.phonePlace || '—' }}</span></div>
</div>
<div class="gamma-card info-card">
<div class="gamma-title"><span></span> 司法风险</div>
<div v-for="item in courtItems" :key="item.key" class="info-item">
<span class="info-label">
<span :class="item.hit ? 'gamma-text-danger' : 'gamma-text-success'">{{ item.hit ? '❗' : '✅' }}</span>
{{ item.label }}
</span>
<span :class="item.hit ? 'gamma-text-danger' : ''">{{ item.hit ? '是' : '否' }}</span>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { maskName, maskIdCard, maskMobile, mobileStatusText, buildCourtRiskItems } from '../reportHelper';
const props = defineProps({
params: { type: Object, default: () => ({}) },
realNameAuth: { type: Object, default: () => ({}) },
mobile3Verify: { type: Object, default: () => ({}) },
mobile4Verify: { type: Object, default: () => ({}) },
mobileDuration: { type: Object, default: () => ({}) },
courtRisk: { type: Object, default: () => ({}) },
personalLawsuit: { type: Object, default: () => ({}) },
});
const maskedName = computed(() => maskName(props.params.name));
const maskedIdCard = computed(() => maskIdCard(props.params.id_card));
const maskedMobile = computed(() => maskMobile(props.params.mobile));
const durationText = computed(() => {
const range = props.mobileDuration.range || '';
if (range.includes('[24')) return '24~36月';
return range || '—';
});
const courtItems = computed(() =>
buildCourtRiskItems(props.courtRisk, props.personalLawsuit),
);
</script>
<style lang="scss" scoped>
.info-card { margin-bottom: 0; }
.info-item {
display: flex;
justify-content: space-between;
margin: 10px 0;
font-size: 14px;
gap: 8px;
}
.info-label { color: #666; flex-shrink: 0; }
</style>

View File

@@ -0,0 +1,87 @@
<template>
<div class="gamma-card">
<div class="gamma-title"><span>📞</span> 投诉风险筛查</div>
<p class="gamma-small" style="margin-bottom: 16px;">风险分: 0-100分数越高投诉风险越高</p>
<div class="complaint-content">
<div class="complaint-score-card">
<div class="gamma-subtitle"><span></span> 筛查结果</div>
<div class="complaint-risk-circle">
<div class="circle" />
<div class="complaint-risk-text">
<div class="score">{{ data.score ?? '—' }}</div>
<div class="gamma-small">风险分</div>
</div>
</div>
</div>
<div>
<table class="gamma-table">
<thead>
<tr><th>规则名称</th><th>权重</th></tr>
</thead>
<tbody>
<tr><td>是否建议拨打电话</td><td>{{ data.is_call ? '是' : '否' }}</td></tr>
<tr><td>是否高频</td><td>{{ data.is_gp ? '是' : '否' }}</td></tr>
<tr><td>是否靓号</td><td>{{ data.is_lh ? '是' : '否' }}</td></tr>
<tr><td>近14天其他类来电次数</td><td>{{ data.other_times?.day_14 ?? '—' }}</td></tr>
<tr><td>近14天金融类来电次数</td><td>{{ data.finance_times?.day_14 ?? '—' }}</td></tr>
<tr><td>用户接受意愿登记</td><td>{{ dncText }}</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { dncRegisterText } from '../reportHelper';
const props = defineProps({
data: { type: Object, default: () => ({}) },
});
const dncText = computed(() => dncRegisterText(props.data.dnc));
</script>
<style lang="scss" scoped>
.complaint-content {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 24px;
}
.complaint-score-card {
display: flex;
flex-direction: column;
align-items: center;
}
.complaint-risk-circle {
width: 120px;
height: 120px;
position: relative;
}
.complaint-risk-circle .circle {
width: 100px;
height: 100px;
border: 6px solid #eee;
border-top-color: #66cc99;
border-radius: 50%;
margin: 0 auto;
}
.complaint-risk-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
}
.score { font-size: 28px; font-weight: 600; }
@media (max-width: 768px) {
.complaint-content { grid-template-columns: 1fr; }
}
</style>

View File

@@ -0,0 +1,462 @@
<template>
<div class="panorama-report">
<div class="panorama-main-title"><span>🛡</span> 信用全景扫描</div>
<!-- 第一行指数 + 机构借贷情况 -->
<div class="panorama-row">
<div class="panorama-card">
<div class="score-grid">
<div v-for="item in CREDIT_PANORAMA_SCORES" :key="item.key" class="score-item">
<div class="score-label">{{ item.label }}</div>
<div class="circle-index">
<span :class="['score-value', { muted: formatModelScore(data[item.key]) === '未命中' }]">
{{ formatModelScore(data[item.key]) }}
</span>
</div>
<div class="score-hint">350-950指数越大逾期率越低</div>
</div>
</div>
</div>
<div class="panorama-card">
<div class="block-title"><span>🏦</span> 机构借贷情况</div>
<div class="mini-grid">
<div v-for="item in CREDIT_PANORAMA_INSTITUTIONS" :key="item.key" class="mini-box">
<div class="mini-box-head">
<span>{{ item.label }}</span>
<span
class="gamma-tag"
:class="{
'gamma-tag--danger': getPanoramaRiskTag(data[item.key]).danger,
'gamma-tag--warn': getPanoramaRiskTag(data[item.key]).warn,
}"
>
{{ getPanoramaRiskTag(data[item.key]).label }}
</span>
</div>
<div class="mini-box-value">{{ formatPanoramaCount(data[item.key], item.unit) }}</div>
</div>
</div>
</div>
</div>
<!-- 第二行近期贷款申请 + 还款历史 -->
<div class="panorama-row">
<div class="panorama-card">
<div class="block-title"><span></span> 近期贷款申请</div>
<div class="list-rows">
<div v-for="item in CREDIT_PANORAMA_RECENT_LOAN" :key="item.key" class="list-row">
<div class="list-row-left">
<span
class="gamma-tag tag-inline"
:class="{
'gamma-tag--danger': getPanoramaRiskTag(data[item.key]).danger,
'gamma-tag--warn': getPanoramaRiskTag(data[item.key]).warn,
}"
>
{{ getPanoramaRiskTag(data[item.key]).label }}
</span>
<span>{{ item.label }}</span>
</div>
<span class="list-row-value">{{ formatPanoramaCount(data[item.key], item.unit) }}</span>
</div>
</div>
</div>
<div class="panorama-card">
<div class="block-title"><span>📋</span> 还款历史</div>
<div class="stat-grid-2">
<div class="stat-box">
<div class="stat-label">历史贷款机构成功还款笔数</div>
<div class="stat-value">{{ formatPanoramaCount(data.xyp_cpl0014, ' 笔') }}</div>
</div>
<div class="stat-box">
<div class="stat-label">历史贷款机构交易失败笔数</div>
<div class="stat-value">{{ formatPanoramaCount(data.xyp_cpl0015, ' 笔') }}</div>
</div>
</div>
<div class="list-rows">
<div class="list-row">
<span>90天还款成功率</span>
<span class="list-row-value">{{ formatPanoramaRatio(data.xyp_cpl0080) }}</span>
</div>
<div class="list-row">
<span>近90天内还款中成功还款总金额比例</span>
<span class="list-row-value">{{ formatPanoramaRatio(data.xyp_cpl0079) }}</span>
</div>
<div class="list-row">
<span>近5次还款中成功还款总金额比例</span>
<span class="list-row-value">{{ formatPanoramaRatio(data.xyp_cpl0073) }}</span>
</div>
<div class="list-row">
<span>近5次还款中还款成功笔数比例</span>
<span class="list-row-value">{{ formatPanoramaRatio(data.xyp_cpl0074) }}</span>
</div>
</div>
</div>
</div>
<!-- 第三行交易失败 + 交易失败后还款 -->
<div class="panorama-row">
<div class="panorama-card">
<div class="block-title"><span></span> 交易失败情况</div>
<div class="list-rows">
<div v-for="item in CREDIT_PANORAMA_FAIL_COUNTS" :key="item.key" class="list-row">
<div class="list-row-left">
<span
class="gamma-tag tag-inline"
:class="{
'gamma-tag--danger': getPanoramaRiskTag(data[item.key]).danger,
'gamma-tag--warn': getPanoramaRiskTag(data[item.key]).warn,
}"
>
{{ getPanoramaRiskTag(data[item.key]).label }}
</span>
<span>{{ item.label }}</span>
</div>
<span class="list-row-value">{{ formatPanoramaCount(data[item.key], item.unit) }}</span>
</div>
</div>
</div>
<div class="panorama-card">
<div class="block-title"><span></span> 交易失败后还款情况</div>
<div class="list-rows">
<div v-for="item in CREDIT_PANORAMA_FAIL_RECOVERY" :key="item.key" class="list-row">
<div class="list-row-left">
<span
class="gamma-tag tag-inline"
:class="{
'gamma-tag--danger': getPanoramaRiskTag(data[item.key]).danger,
'gamma-tag--warn': getPanoramaRiskTag(data[item.key]).warn,
}"
>
{{ getPanoramaRiskTag(data[item.key]).label }}
</span>
<span>{{ item.label }}</span>
</div>
<span class="list-row-value">{{ formatPanoramaCount(data[item.key], item.unit) }}</span>
</div>
</div>
</div>
</div>
<!-- 第四行逾期情况 + 贷款整体情况 -->
<div class="panorama-row">
<div class="panorama-card">
<div class="block-title"><span>🚨</span> 逾期情况</div>
<div class="stat-grid-2">
<div class="stat-box">
<div class="stat-label">当前逾期机构数</div>
<div class="stat-value">{{ formatPanoramaCount(data.xyp_cpl0071, '家') }}</div>
</div>
<div class="stat-box">
<div class="stat-label">当前逾期金额</div>
<div class="stat-value">{{ formatPanoramaAmount(data.xyp_cpl0072) }}</div>
</div>
</div>
<div class="list-rows">
<div v-for="item in CREDIT_PANORAMA_OVERDUE_FLAGS" :key="item.key" class="list-row flag-row">
<div class="list-row-left">
<span class="success-check"></span>
<span>{{ item.label }}</span>
</div>
<span
class="list-row-value"
:class="{ 'gamma-text-danger': data[item.key] === '1' }"
>
{{ formatPanoramaYesNo(data[item.key]) }}
</span>
</div>
</div>
</div>
<div class="panorama-card">
<div class="block-title"><span>📈</span> 贷款整体情况</div>
<div class="list-rows">
<div v-for="item in CREDIT_PANORAMA_LOAN_OVERVIEW" :key="item.key" class="list-row flag-row">
<div class="list-row-left">
<span class="success-check"></span>
<span>{{ item.label }}</span>
</div>
<span class="list-row-value">
{{
item.type === 'amount'
? formatPanoramaAmount(data[item.key])
: formatPanoramaCount(data[item.key], item.unit)
}}
</span>
</div>
</div>
</div>
</div>
<!-- 第五行贷款分周期汇总 -->
<div class="panorama-card panorama-card--full">
<div class="block-title"><span>📊</span> 贷款分周期汇总</div>
<div class="table-wrap">
<table class="gamma-table panorama-table">
<thead>
<tr>
<th>周期</th>
<th>贷款机构数</th>
<th>还款成功笔数</th>
<th>还款成功总金额()</th>
<th>交易失败笔数</th>
<th>交易失败总金额()</th>
</tr>
</thead>
<tbody>
<tr v-for="row in CREDIT_PANORAMA_PERIOD_ROWS" :key="row.label">
<td>{{ row.label }}</td>
<td>{{ panoramaTableCell(data, row.instKey) }}</td>
<td>{{ panoramaTableCell(data, row.successKey) }}</td>
<td>{{ panoramaTableCell(data, row.successAmtKey, 'amount') }}</td>
<td>{{ panoramaTableCell(data, row.failKey) }}</td>
<td>{{ panoramaTableCell(data, row.failAmtKey, 'amount') }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script setup>
import {
CREDIT_PANORAMA_SCORES,
CREDIT_PANORAMA_INSTITUTIONS,
CREDIT_PANORAMA_RECENT_LOAN,
CREDIT_PANORAMA_FAIL_COUNTS,
CREDIT_PANORAMA_FAIL_RECOVERY,
CREDIT_PANORAMA_OVERDUE_FLAGS,
CREDIT_PANORAMA_LOAN_OVERVIEW,
CREDIT_PANORAMA_PERIOD_ROWS,
formatModelScore,
formatPanoramaCount,
formatPanoramaAmount,
formatPanoramaRatio,
formatPanoramaYesNo,
getPanoramaRiskTag,
panoramaTableCell,
} from '../reportHelper';
defineProps({
data: { type: Object, default: () => ({}) },
});
</script>
<style lang="scss" scoped>
.panorama-report {
background: #fff;
border-radius: 8px;
padding: 24px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.panorama-main-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 8px;
color: #333;
padding-left: 10px;
border-left: 4px solid #d4af37;
}
.panorama-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
.panorama-card {
background: #fff;
border: 1px solid #eee;
border-radius: 12px;
padding: 20px;
&--full {
margin-bottom: 0;
}
}
.block-title {
font-size: 15px;
font-weight: 600;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 6px;
color: #333;
}
.score-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.score-item {
text-align: center;
}
.score-label {
font-size: 14px;
font-weight: 500;
color: #444;
margin-bottom: 12px;
}
.circle-index {
width: 120px;
height: 120px;
border: 4px solid #f0f0f0;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 10px;
}
.score-value {
font-size: 20px;
font-weight: 600;
color: #2da44e;
&.muted {
font-size: 16px;
color: #999;
}
}
.score-hint {
font-size: 12px;
color: #999;
line-height: 1.4;
}
.mini-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.mini-box {
border: 1px solid #eee;
border-radius: 8px;
padding: 12px;
}
.mini-box-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
font-size: 13px;
color: #444;
}
.mini-box-value {
margin-top: 6px;
font-size: 13px;
color: #666;
}
.list-rows {
display: flex;
flex-direction: column;
gap: 12px;
}
.list-row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
font-size: 13px;
color: #444;
}
.list-row-left {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
min-width: 0;
}
.tag-inline {
flex-shrink: 0;
}
.list-row-value {
flex-shrink: 0;
color: #666;
}
.stat-grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-bottom: 16px;
}
.stat-box {
border: 1px solid #eee;
border-radius: 8px;
padding: 16px;
text-align: center;
}
.stat-label {
font-size: 12px;
color: #999;
margin-bottom: 6px;
}
.stat-value {
font-size: 16px;
font-weight: 500;
color: #444;
}
.success-check {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
background: #2da44e;
border-radius: 50%;
color: #fff;
font-size: 10px;
flex-shrink: 0;
}
.flag-row .list-row-left span:last-child {
line-height: 1.4;
}
.table-wrap {
overflow-x: auto;
}
.panorama-table {
min-width: 720px;
}
@media (max-width: 992px) {
.panorama-row {
grid-template-columns: 1fr;
}
.score-grid {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,219 @@
<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>

View File

@@ -0,0 +1,984 @@
<template>
<div class="gamma-card judicial-section">
<div class="gamma-title"><span>🗺</span> 司法案件</div>
<!-- 案件概览 -->
<div class="block-title">案件概览</div>
<div class="case-overview">
<div
v-for="item in caseTypeCards"
:key="item.key"
class="overview-card"
:class="{ criminal: item.count > 0 && item.key === 'criminal' }"
>
<div v-if="item.count > 0 && item.key === 'criminal'" class="badge">!</div>
<div class="card-title">{{ item.label }}</div>
<div class="card-num">{{ item.count }}</div>
</div>
</div>
<!-- 司法风险 -->
<div class="risk-section">
<div class="risk-item">
<div class="label">司法风险<br>被告案件数</div>
<div class="bar-wrap">
<div class="bar-fill" :style="{ width: defendantBarWidth }" />
</div>
<div class="legend">
<span class="unresolved">未结案{{ count.count_wei_beigao ?? 0 }}</span>
<span class="resolved">已经结案{{ count.count_jie_beigao ?? 0 }}</span>
</div>
<div class="total-hint">{{ count.count_beigao ?? 0 }}</div>
</div>
<div class="risk-item">
<div class="label">被告案件金额</div>
<div class="num-row">
<span class="icon-yen">¥</span>
<span>{{ count.money_beigao ?? 0 }}</span>
</div>
</div>
<div class="risk-item">
<div class="label">被执行案件数</div>
<div class="num-row red">
<span>📋</span>
<span>{{ implementCount }}</span>
</div>
</div>
<div class="risk-item">
<div class="label">最近案件年份</div>
<div class="num-row red">
<span>🗓</span>
<span>{{ recentYearText }}</span>
</div>
</div>
</div>
<!-- 案件统计 -->
<div class="block-title">案件统计</div>
<div class="case-stats">
<div class="chart-title">案件数量</div>
<div class="bar-chart">
<div class="chart-legend">
<span class="unresolved">
未结案统计
<em>{{ count.count_wei_total ?? 0 }}</em>
</span>
<span class="resolved">
已结案统计
<em>{{ count.count_jie_total ?? 0 }}</em>
</span>
</div>
<div class="bar-rows">
<div v-for="row in roleBarRows" :key="row.label" class="bar-row">
<div class="row-label">{{ row.label }}</div>
<div class="row-bar-track">
<div class="row-bar" :style="{ width: row.width }" />
</div>
<div class="row-value">{{ row.count }}</div>
</div>
</div>
<div class="axis">
<span v-for="tick in axisTicks" :key="tick">{{ tick }}</span>
</div>
</div>
<div class="chart-title">涉案金额</div>
<div class="amount-section">
<div v-for="card in amountCards" :key="card.title" class="amount-card">
<div class="card-header">
<span>{{ card.icon }}</span>
<span>{{ card.title }}</span>
</div>
<div class="row"><span>总案件</span><span class="value">{{ card.total }}</span></div>
<div class="row"><span>已结案</span><span class="value">{{ card.jie }}</span></div>
<div class="row"><span>未结案</span><span class="value">{{ card.wei }}</span></div>
</div>
</div>
<div class="pie-section">
<div v-for="pie in pieCharts" :key="pie.title" class="pie-card">
<div class="pie-container">
<div class="pie-circle" :class="{ simple: pie.items.length <= 1 }" :style="{ background: pie.gradient }">
<div class="inner">
{{ pie.title }}<br>占比图
</div>
</div>
</div>
<div v-if="pie.items.length" class="pie-legend">
<div v-for="(item, i) in pie.items" :key="i" class="pie-legend-item">
<span class="dot" :style="{ background: pieColors[i % pieColors.length] }" />
{{ item.label }}({{ item.count }})
</div>
</div>
</div>
</div>
</div>
<!-- 案件列表可展开详情 -->
<div v-if="allCases.length" class="case-list-card">
<div
v-for="(c, i) in allCases"
:key="`${c.c_ah}-${i}`"
class="case-list-group"
>
<div
class="case-item"
:class="{ active: expandedIndex === i }"
@click="toggleCase(i)"
>
<span class="case-text">{{ caseListText(c) }}</span>
<span class="case-arrow">{{ expandedIndex === i ? '' : '>' }}</span>
</div>
<div v-if="expandedIndex === i" class="case-block">
<div class="case-info">
<div><span>案件类型:</span><span>{{ c.sectionLabel }}</span></div>
<div><span>诉讼地位:</span><span>{{ c.n_ssdw }}</span></div>
<div><span>所属地域:</span><span>{{ c.c_ssdy }}</span></div>
<div><span>当事人:</span><span>{{ partiesText(c.c_dsrxx) }}</span></div>
<div><span>审理程序:</span><span>{{ c.n_slcx }}</span></div>
<div><span>立案时间:</span><span>{{ c.d_larq }}</span></div>
<div><span>结束时间:</span><span>{{ c.d_jarq }}</span></div>
<div><span>立案案由详情:</span><span>{{ c.n_laay_tree }}</span></div>
<div><span>结案方式:</span><span>{{ c.n_jafs }}</span></div>
<div><span>经办法院:</span><span>{{ c.n_jbfy }}</span></div>
</div>
<div v-if="c.c_gkws_pjjg" class="case-judgment gamma-small">
判决结果<br>{{ c.c_gkws_pjjg }}
</div>
</div>
</div>
</div>
<!-- 失信被执行人 -->
<div class="module-card">
<div class="module-title title-dishonest">失信被执行人</div>
<template v-if="dishonestList.length">
<div class="case-list-card inner-list">
<div v-for="(item, i) in dishonestList" :key="i" class="case-item static">
<span class="case-text">{{ dishonestText(item) }}</span>
</div>
</div>
</template>
<div v-else class="empty-data">
<svg class="empty-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 7C3 5.89543 3.89543 5 5 5H19C20.1046 5 21 5.89543 21 7V17C21 18.1046 20.1046 19 19 19H5C3.89543 19 3 18.1046 3 17V7Z" stroke="#999" stroke-width="1.5" />
<path d="M3 7L12 12L21 7" stroke="#999" stroke-width="1.5" stroke-linecap="round" />
</svg>
<span>暂无数据</span>
</div>
</div>
<!-- 限高被执行人 -->
<div class="module-card">
<div class="module-title title-high-limit">限高被执行人</div>
<table class="data-table">
<thead>
<tr>
<th>案号</th>
<th>企业法人</th>
<th>企业名称</th>
<th>执行法院</th>
<th>发布时间日期</th>
<th>立案时间日期</th>
</tr>
</thead>
<tbody>
<template v-if="limitHighList.length">
<tr v-for="(item, i) in limitHighRows" :key="i">
<td>{{ item.ah }}</td>
<td>{{ item.legalPerson }}</td>
<td>{{ item.entName }}</td>
<td>{{ item.court }}</td>
<td>{{ item.publishDate }}</td>
<td>{{ item.filingDate }}</td>
</tr>
</template>
<tr v-else>
<td colspan="6">
<div class="empty-data">
<svg class="empty-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 7C3 5.89543 3.89543 5 5 5H19C20.1046 5 21 5.89543 21 7V17C21 18.1046 20.1046 19 19 19H5C3.89543 19 3 18.1046 3 17V7Z" stroke="#999" stroke-width="1.5" />
<path d="M3 7L12 12L21 7" stroke="#999" stroke-width="1.5" stroke-linecap="round" />
</svg>
<span>暂无数据</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 报告使用须知 -->
<div class="notice-card">
<div class="notice-title">报告使用须知</div>
<ol class="notice-list">
<li v-for="(line, i) in REPORT_USAGE_NOTICE" :key="i">{{ line }}</li>
</ol>
</div>
</div>
</template>
<script setup>
import { computed, ref } from 'vue';
import {
parseStatDistribution,
buildConicGradient,
toCaseArray,
extractJudicialList,
REPORT_USAGE_NOTICE,
caseListText,
limitHighRow,
} from '../reportHelper';
const props = defineProps({
data: { type: Object, default: () => ({}) },
});
const PIE_COLORS = ['#4E74F6', '#89d079', '#f6c358', '#ff7c7c', '#9b59b6', '#5dade2'];
const pieColors = PIE_COLORS;
const count = computed(() => props.data.count || {});
const caseTypeCards = computed(() => [
{ key: 'administrative', label: '行政案件', count: toCaseArray(props.data.administrative).length },
{ key: 'civil', label: '民事案件', count: toCaseArray(props.data.civil).length },
{ key: 'criminal', label: '刑事案件', count: toCaseArray(props.data.criminal).length },
{ key: 'implement', label: '执行案件', count: toCaseArray(props.data.implement).length },
{ key: 'preservation', label: '非诉保全审查', count: toCaseArray(props.data.preservation).length },
{ key: 'bankrupt', label: '强制清算与破产案件', count: toCaseArray(props.data.bankrupt).length },
]);
const implementCount = computed(() => toCaseArray(props.data.implement).length);
const defendantBarWidth = computed(() => {
const total = Number(count.value.count_beigao) || 0;
const jie = Number(count.value.count_jie_beigao) || 0;
if (!total) return '0%';
return `${Math.round((jie / total) * 100)}%`;
});
const recentYearText = computed(() => {
const stat = count.value.larq_stat || '';
const items = parseStatDistribution(stat);
if (!items.length) return '—';
const latest = items.reduce((a, b) => (a.label > b.label ? a : b));
return `${latest.label}(${latest.count})`;
});
const roleBarRows = computed(() => {
const c = count.value;
const rows = [
{ label: '被告', count: Number(c.count_beigao) || 0 },
{ label: '原告', count: Number(c.count_yuangao) || 0 },
{ label: '第三人', count: Number(c.count_other) || 0 },
];
const max = Math.max(...rows.map((r) => r.count), 1);
return rows.map((r) => ({
...r,
width: `${Math.round((r.count / max) * 100)}%`,
}));
});
const axisTicks = computed(() => {
const max = Math.max(...roleBarRows.value.map((r) => r.count), 2);
const step = max <= 2 ? 0.5 : 1;
const ticks = [];
for (let i = 0; i <= max; i += step) {
ticks.push(Number.isInteger(i) ? i : i.toFixed(1));
}
return ticks.length > 1 ? ticks : [0, 0.5, 1, 1.5, 2, 2.5];
});
const amountCards = computed(() => {
const c = count.value;
return [
{
title: '汇总',
icon: '📊',
total: c.money_total ?? 0,
jie: c.money_jie_total ?? 0,
wei: c.money_wei_total ?? 0,
},
{
title: '被告',
icon: '👤',
total: c.money_beigao ?? 0,
jie: c.money_jie_beigao ?? 0,
wei: c.money_wei_beigao ?? 0,
},
{
title: '原告',
icon: '👤',
total: c.money_yuangao ?? 0,
jie: c.money_jie_yuangao ?? 0,
wei: c.money_wei_yuangao ?? 0,
},
{
title: '第三人',
icon: '👤',
total: c.money_other ?? 0,
jie: c.money_jie_other ?? 0,
wei: c.money_wei_other ?? 0,
},
];
});
const pieCharts = computed(() => {
const c = count.value;
return [
{ title: '地点分布', items: parseStatDistribution(c.area_stat), gradient: '' },
{ title: '案由分布', items: parseStatDistribution(c.ay_stat), gradient: '' },
{ title: '结案分布', items: parseStatDistribution(c.jafs_stat), gradient: '' },
{ title: '时间分布', items: parseStatDistribution(c.larq_stat), gradient: '' },
].map((pie) => ({
...pie,
gradient: buildConicGradient(pie.items),
}));
});
const caseSections = computed(() => {
const sections = [
{ key: 'criminal', label: '刑事案件', items: toCaseArray(props.data.criminal) },
{ key: 'civil', label: '民事案件', items: toCaseArray(props.data.civil) },
{ key: 'administrative', label: '行政案件', items: toCaseArray(props.data.administrative) },
{ key: 'implement', label: '执行案件', items: toCaseArray(props.data.implement) },
];
return sections.filter((s) => s.items.length > 0);
});
const allCases = computed(() => {
const list = [];
for (const section of caseSections.value) {
for (const c of section.items) {
list.push({ ...c, sectionLabel: section.label });
}
}
return list;
});
const expandedIndex = ref(null);
function toggleCase(index) {
expandedIndex.value = expandedIndex.value === index ? null : index;
}
const dishonestList = computed(() =>
extractJudicialList(props.data, [
'dishonest',
'sxbzxr',
'breachCase',
'disinCases',
'dishonestExecutor',
]),
);
const limitHighList = computed(() =>
extractJudicialList(props.data, [
'limitHigh',
'xgbzxr',
'consumptionRestriction',
'limitCases',
'limitExecutor',
]),
);
const limitHighRows = computed(() => limitHighList.value.map(limitHighRow));
function dishonestText(item) {
const ah = item.c_ah || item.ah || item.caseCode || '—';
const court = item.n_jbfy || item.court || item.zxfy || '';
return court ? `${ah}${court}` : ah;
}
function partiesText(dsrxx) {
if (!Array.isArray(dsrxx)) return '—';
return dsrxx.map((d) => `${d.c_mc}${d.n_dsrlx}`).join('; ');
}
</script>
<style lang="scss" scoped>
.judicial-section {
--blue: #4e74f6;
--red: #ff4d4f;
--border: #e8eef7;
}
.block-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 20px 0 16px;
display: flex;
align-items: center;
gap: 6px;
&::before {
content: '';
display: inline-block;
width: 16px;
height: 16px;
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%234E74F6"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/></svg>') no-repeat center;
background-size: contain;
}
}
.case-overview {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.overview-card {
background: #fff;
border-radius: 8px;
padding: 20px 12px;
text-align: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
position: relative;
border: 1px solid var(--border);
&.criminal {
background: #fff5f5;
border-color: #ffd0d0;
.card-num { color: var(--red); }
}
}
.badge {
position: absolute;
top: 8px;
right: 8px;
width: 16px;
height: 16px;
background: var(--red);
color: #fff;
border-radius: 50%;
font-size: 10px;
display: flex;
align-items: center;
justify-content: center;
}
.card-title {
font-size: 14px;
color: #666;
margin-bottom: 12px;
}
.card-num {
font-size: 28px;
font-weight: 700;
color: var(--blue);
}
.risk-section {
background: #fff;
border-radius: 8px;
padding: 20px;
margin-bottom: 30px;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
border: 1px solid var(--border);
}
.risk-item .label {
font-size: 13px;
color: #666;
margin-bottom: 8px;
line-height: 1.5;
}
.bar-wrap {
width: 100%;
height: 8px;
background: var(--border);
border-radius: 4px;
overflow: hidden;
margin-bottom: 4px;
}
.bar-fill {
height: 100%;
background: var(--blue);
border-radius: 4px;
min-width: 0;
}
.legend {
display: flex;
gap: 12px;
font-size: 12px;
color: #888;
margin-top: 4px;
flex-wrap: wrap;
}
.legend span::before {
content: '';
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 4px;
}
.legend .unresolved::before { background: var(--red); }
.legend .resolved::before { background: var(--blue); }
.total-hint {
text-align: right;
font-size: 12px;
color: #666;
margin-top: 4px;
}
.num-row {
display: flex;
align-items: center;
gap: 6px;
font-size: 16px;
font-weight: 600;
color: #333;
&.red { color: var(--red); }
}
.icon-yen { color: var(--blue); font-size: 18px; font-style: normal; }
.chart-title {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
color: #333;
margin-bottom: 16px;
&::before {
content: '';
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--blue);
border: 2px solid var(--border);
}
}
.bar-chart {
background: #fff;
border-radius: 8px;
padding: 20px;
margin-bottom: 24px;
border: 1px solid var(--border);
position: relative;
}
.chart-legend {
display: flex;
gap: 16px;
flex-wrap: wrap;
margin-bottom: 24px;
}
.chart-legend > span {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
padding: 2px 8px;
border-radius: 4px;
}
.chart-legend .unresolved {
background: #ff4d4f20;
color: var(--red);
}
.chart-legend .resolved {
background: #4e74f620;
color: var(--blue);
}
.chart-legend em {
font-style: normal;
padding: 0 6px;
border-radius: 4px;
color: #fff;
}
.chart-legend .unresolved em { background: var(--red); }
.chart-legend .resolved em { background: var(--blue); }
.bar-rows {
display: flex;
flex-direction: column;
gap: 20px;
}
.bar-row {
display: flex;
align-items: center;
gap: 12px;
}
.row-label {
width: 60px;
font-size: 12px;
color: #666;
text-align: right;
flex-shrink: 0;
}
.row-bar-track {
flex: 1;
height: 8px;
background: var(--border);
border-radius: 4px;
overflow: hidden;
}
.row-bar {
height: 100%;
background: var(--blue);
border-radius: 4px;
min-width: 0;
position: relative;
&::after {
content: '';
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
width: 8px;
height: 8px;
background: var(--blue);
border-radius: 50%;
}
}
.row-value {
width: 24px;
font-size: 12px;
color: #666;
text-align: right;
}
.axis {
display: flex;
justify-content: space-between;
margin-top: 8px;
padding: 0 calc(60px + 36px) 0 calc(60px + 12px);
font-size: 11px;
color: #999;
}
.amount-section {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-bottom: 30px;
}
.amount-card {
background: #fff;
border-radius: 8px;
padding: 16px;
border: 1px solid var(--border);
}
.amount-card .card-header {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 12px;
}
.amount-card .row {
display: flex;
justify-content: space-between;
font-size: 13px;
color: #666;
margin-bottom: 8px;
}
.amount-card .value {
color: #333;
font-weight: 500;
}
.pie-section {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-bottom: 24px;
}
.pie-card {
background: #fff;
border-radius: 8px;
padding: 20px;
border: 1px solid var(--border);
display: flex;
flex-direction: column;
align-items: center;
}
.pie-container {
width: 120px;
height: 120px;
}
.pie-circle {
width: 100%;
height: 100%;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
&.simple { background: var(--blue) !important; }
}
.pie-circle .inner {
width: 80px;
height: 80px;
background: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 500;
color: #333;
text-align: center;
line-height: 1.4;
}
.pie-legend {
margin-top: 12px;
width: 100%;
}
.pie-legend-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: #666;
margin-bottom: 4px;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.case-list-card {
background: #fff;
border-radius: 8px;
margin: 24px 0 20px;
overflow: hidden;
border: 1px solid var(--border);
}
.case-list-card.inner-list {
margin: 0;
border: none;
}
.case-list-group {
border-bottom: 1px solid #e8edf5;
&:last-child { border-bottom: none; }
}
.case-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 20px;
background: #f0f7ff;
border-bottom: 1px solid #e8edf5;
cursor: pointer;
user-select: none;
&::before {
content: '';
display: inline-block;
width: 3px;
height: 16px;
background: #4080ff;
margin-right: 10px;
flex-shrink: 0;
}
&.static { cursor: default; }
&.active { background: #e6f0ff; }
}
.case-text {
flex: 1;
font-size: 14px;
color: #333;
}
.case-arrow {
color: #999;
font-size: 16px;
margin-left: 8px;
}
.case-block {
padding: 16px 20px 16px 33px;
background: #fafbfc;
border-top: 1px dashed #e8edf5;
}
.case-info {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.case-info div {
display: flex;
justify-content: space-between;
padding: 6px 0;
border-bottom: 1px dashed #eee;
gap: 8px;
font-size: 13px;
}
.case-judgment {
margin-top: 10px;
line-height: 1.6;
color: #666;
}
.module-card {
background: #fff;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
border: 1px solid var(--border);
}
.module-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 20px;
display: flex;
align-items: center;
&::before {
content: '';
display: inline-block;
width: 18px;
height: 18px;
margin-right: 8px;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
opacity: 0.7;
}
}
.title-dishonest::before {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23666"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>');
}
.title-high-limit::before {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23666"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-7 14h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>');
}
.empty-data {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 0;
color: #999;
font-size: 14px;
}
.empty-icon {
width: 48px;
height: 48px;
margin-bottom: 10px;
opacity: 0.5;
}
.data-table {
width: 100%;
border-collapse: collapse;
}
.data-table th {
background: #fff9e8;
padding: 12px 8px;
text-align: center;
font-size: 13px;
color: #666;
font-weight: 500;
}
.data-table td {
padding: 12px 8px;
text-align: center;
font-size: 13px;
color: #333;
border-top: 1px solid #f0f0f0;
}
.notice-card {
background: #fff;
border-radius: 8px;
padding: 20px;
border: 1px solid #e0e0e0;
margin-top: 4px;
}
.notice-title {
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 12px;
}
.notice-list {
list-style-type: decimal;
padding-left: 20px;
font-size: 13px;
color: #666;
line-height: 1.8;
}
@media (max-width: 1024px) {
.case-overview { grid-template-columns: repeat(3, 1fr); }
.risk-section { grid-template-columns: repeat(2, 1fr); }
.amount-section,
.pie-section { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 640px) {
.case-overview { grid-template-columns: repeat(2, 1fr); }
.risk-section,
.amount-section,
.pie-section { grid-template-columns: 1fr; }
.data-table {
display: block;
overflow-x: auto;
}
}
</style>

View File

@@ -0,0 +1,278 @@
<template>
<div class="intent-report">
<div class="section-title">借贷意向</div>
<p class="desc-text">
借贷意向数据覆盖大部分的金融机构机构类型包括银行改制机构小贷消费类分期现金类分期代偿类分期和非银其它
</p>
<div class="section-title">最终决策结果</div>
<div class="tab-box">
<div
class="tab-item"
:class="{ active: activeTab === 'decision' }"
@click="activeTab = 'decision'"
>
{{ data.Rule_final_decision || '最终决策结果' }}
</div>
<div
class="tab-item"
:class="{ active: activeTab === 'review' }"
@click="activeTab = 'review'"
>
复议
</div>
</div>
<p v-if="data.Rule_name_QJF045" class="rule-hint">
命中规则{{ data.Rule_name_QJF045 }}
<span v-if="data.Rule_final_weight">权重 {{ data.Rule_final_weight }}</span>
</p>
<!-- 本人在本机构借贷意向表现 -->
<div class="section-title">本人在本机构借贷意向表现</div>
<table class="intent-table">
<thead>
<tr>
<th class="left-align">申请次数</th>
<th v-for="p in LOAN_INTENT_PERIOD_PREFIXES" :key="p.prefix">{{ p.label }}</th>
</tr>
</thead>
<tbody>
<tr v-for="row in LOAN_INTENT_INST_ROWS" :key="row.label">
<td class="left-align">{{ row.label }}</td>
<td v-for="p in LOAN_INTENT_PERIOD_PREFIXES" :key="`${row.label}-${p.prefix}`">
{{ formatAlsCount(data, p.prefix, row.suffix) }}
</td>
</tr>
</tbody>
</table>
<div class="note-text" v-html="LOAN_INTENT_NOTE" />
<!-- 本人在各个客户类型借贷意向表现 -->
<div class="section-title">本人在各个客户类型借贷意向表现</div>
<table class="intent-table">
<thead>
<tr>
<th class="left-align" rowspan="2">客户类型</th>
<th v-for="p in LOAN_INTENT_PERIOD_PREFIXES" :key="'h1-' + p.prefix" colspan="2">{{ p.label }}</th>
</tr>
<tr>
<template v-for="p in LOAN_INTENT_PERIOD_PREFIXES" :key="'h2-' + p.prefix">
<th>机构数</th>
<th>次数</th>
</template>
</tr>
</thead>
<tbody>
<tr
v-for="row in LOAN_INTENT_CUSTOMER_ROWS"
:key="row.label"
:class="{ 'light-bg': row.highlight }"
>
<td class="left-align">{{ row.label }}</td>
<template v-for="p in LOAN_INTENT_PERIOD_PREFIXES" :key="`${row.label}-${p.prefix}`">
<td>{{ formatAlsOrg(data, p.prefix, row.suffix) }}</td>
<td>{{ formatAlsCount(data, p.prefix, row.suffix) }}</td>
</template>
</tr>
</tbody>
</table>
<div class="note-text" v-html="LOAN_INTENT_NOTE" />
<!-- 本人在各个业务类型借贷意向表现 -->
<div class="section-title">本人在各个业务类型借贷意向表现</div>
<table class="intent-table">
<thead>
<tr>
<th class="left-align" rowspan="2">业务类型</th>
<th v-for="p in LOAN_INTENT_PERIOD_PREFIXES" :key="'b1-' + p.prefix" colspan="2">{{ p.label }}</th>
</tr>
<tr>
<template v-for="p in LOAN_INTENT_PERIOD_PREFIXES" :key="'b2-' + p.prefix">
<th>机构数</th>
<th>次数</th>
</template>
</tr>
</thead>
<tbody>
<tr v-for="row in LOAN_INTENT_BUSINESS_ROWS" :key="row.label">
<td class="left-align">{{ row.label }}</td>
<template v-for="p in LOAN_INTENT_PERIOD_PREFIXES" :key="`${row.label}-${p.prefix}`">
<td>{{ formatAlsOrg(data, p.prefix, row.suffix) }}</td>
<td>{{ formatAlsCount(data, p.prefix, row.suffix) }}</td>
</template>
</tr>
</tbody>
</table>
<div class="note-text" v-html="LOAN_INTENT_NOTE" />
<!-- 本人在异常时间段借贷意向表现 -->
<div class="section-title">本人在异常时间段借贷意向表现</div>
<table class="intent-table">
<thead>
<tr>
<th class="left-align" rowspan="2">时间-机构</th>
<th v-for="p in LOAN_INTENT_PERIOD_PREFIXES" :key="'a1-' + p.prefix" colspan="2">{{ p.label }}</th>
</tr>
<tr>
<template v-for="p in LOAN_INTENT_PERIOD_PREFIXES" :key="'a2-' + p.prefix">
<th>机构数</th>
<th>次数</th>
</template>
</tr>
</thead>
<tbody>
<tr v-for="row in LOAN_INTENT_ABNORMAL_ROWS" :key="row.label">
<td class="left-align">{{ row.label }}</td>
<template v-for="p in LOAN_INTENT_PERIOD_PREFIXES" :key="`${row.label}-${p.prefix}`">
<td>{{ formatAlsOrg(data, p.prefix, row.suffix) }}</td>
<td>{{ formatAlsCount(data, p.prefix, row.suffix) }}</td>
</template>
</tr>
</tbody>
</table>
<div class="note-text" v-html="LOAN_INTENT_NOTE" />
</div>
</template>
<script setup>
import { ref } from 'vue';
import {
LOAN_INTENT_PERIOD_PREFIXES,
LOAN_INTENT_INST_ROWS,
LOAN_INTENT_CUSTOMER_ROWS,
LOAN_INTENT_BUSINESS_ROWS,
LOAN_INTENT_ABNORMAL_ROWS,
LOAN_INTENT_NOTE,
formatAlsOrg,
formatAlsCount,
} from '../reportHelper';
defineProps({
data: { type: Object, default: () => ({}) },
});
const activeTab = ref('review');
</script>
<style lang="scss" scoped>
.intent-report {
background: #fff;
border-radius: 8px;
padding: 24px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.section-title {
font-size: 18px;
font-weight: bold;
margin: 30px 0 16px;
padding-left: 10px;
border-left: 4px solid #d4af37;
display: flex;
align-items: center;
gap: 8px;
&:first-child {
margin-top: 0;
}
&::before {
content: '';
display: inline-block;
width: 20px;
height: 16px;
background: linear-gradient(135deg, #d4af37 30%, transparent 30%, transparent 60%, #d4af37 60%);
}
}
.desc-text {
color: #666;
font-size: 15px;
margin: 8px 0 16px;
line-height: 1.6;
padding-left: 14px;
border-left: 2px solid #d4af37;
}
.tab-box {
display: inline-flex;
margin: 10px 0;
}
.tab-item {
padding: 8px 24px;
border: 1px solid #eee;
border-right: none;
background: #f5f7fa;
font-size: 15px;
cursor: pointer;
user-select: none;
&.active {
background: #fff;
font-weight: bold;
}
&:last-child {
border-right: 1px solid #eee;
border-radius: 0 4px 4px 0;
}
&:first-child {
border-radius: 4px 0 0 4px;
}
}
.rule-hint {
font-size: 13px;
color: #d89614;
margin: 4px 0 12px;
padding-left: 14px;
}
.intent-table {
width: 100%;
border-collapse: collapse;
margin: 16px 0;
font-size: 14px;
th,
td {
border: 1px solid #eee;
padding: 12px 8px;
text-align: center;
}
th {
background-color: #fdf8e9;
font-weight: 500;
}
}
.left-align {
text-align: left !important;
}
.light-bg {
background-color: #f0f5ff;
}
.note-text {
font-size: 13px;
color: #666;
margin: 8px 0 16px;
line-height: 1.6;
}
@media (max-width: 768px) {
.intent-report {
padding: 16px;
overflow-x: auto;
}
.intent-table {
min-width: 900px;
}
}
</style>

View File

@@ -0,0 +1,80 @@
<template>
<div class="gamma-card">
<div class="gamma-title"><span>🏦</span> 借贷画像</div>
<div class="loan-profile-grid">
<div v-for="item in items" :key="item.label" class="loan-item">
<div class="loan-icon" :class="item.color">{{ item.icon }}</div>
<div>
<div class="loan-text">{{ item.label }}</div>
<div class="loan-value">{{ item.value }}</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { cellText } from '../reportHelper';
const props = defineProps({
loanTotal: { type: Object, default: () => ({}) },
});
const items = computed(() => {
const d = props.loanTotal || {};
return [
{ label: '机构查询总次数', value: cellText(d.queryCount), icon: '📑', color: 'purple' },
{ label: '借贷机构数2年内', value: cellText(d.orgCount), icon: '🏢', color: 'green' },
{ label: '正常还款订单占比', value: cellText(d.repayRate), icon: '🤲', color: 'orange' },
{ label: '预估网贷授信额度', value: cellText(d.creditLimit), icon: '📄', color: 'pink' },
{ label: '网贷产品数', value: cellText(d.productCount), icon: '📦', color: 'red' },
{ label: '已结清订单数', value: cellText(d.settledCount), icon: '👤', color: 'blue' },
{ label: '借款总金额2年内', value: d.loanAmount != null ? `${d.loanAmount}` : '— 元', icon: '💴', color: 'orange' },
{ label: '逾期总金额2年内', value: cellText(d.overdueAmount), icon: '📄', color: 'pink' },
];
});
</script>
<style lang="scss" scoped>
.loan-profile-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
.loan-item {
background: #f9fafc;
border-radius: 6px;
padding: 16px;
display: flex;
align-items: center;
gap: 10px;
}
.loan-icon {
width: 36px;
height: 36px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 18px;
flex-shrink: 0;
}
.loan-icon.purple { background: #6c5ce7; }
.loan-icon.green { background: #00b894; }
.loan-icon.orange { background: #fdcb6e; }
.loan-icon.pink { background: #e84393; }
.loan-icon.red { background: #e74c3c; }
.loan-icon.blue { background: #3498db; }
.loan-text { font-size: 14px; line-height: 1.4; }
.loan-value { font-size: 16px; font-weight: 600; margin-top: 4px; }
@media (max-width: 768px) {
.loan-profile-grid { grid-template-columns: repeat(2, 1fr); }
}
</style>

View File

@@ -0,0 +1,83 @@
<template>
<div class="gamma-card">
<div class="gamma-title"><span>🚫</span> 逾期黑名单</div>
<div class="gamma-subtitle"><span></span> 命中结果</div>
<div class="blacklist-row">
<div class="blacklist-label">是否命中黑名单</div>
<div class="blacklist-value">{{ isHit(data.black_list) ? '是' : '否' }}</div>
</div>
<div class="gamma-subtitle"><span></span> 疑似标签</div>
<div class="tags-grid">
<div v-for="tag in BLACKLIST_TAGS" :key="tag.key" class="tag-item">
<div class="tag-label">{{ tag.label }}</div>
<div class="tag-value">{{ isHit(data[tag.key]) ? '命中' : '未命中' }}</div>
</div>
</div>
</div>
</template>
<script setup>
import { BLACKLIST_TAGS, isHit } from '../reportHelper';
defineProps({
data: { type: Object, default: () => ({}) },
});
</script>
<style lang="scss" scoped>
.blacklist-row {
display: flex;
align-items: center;
max-width: 400px;
margin-bottom: 16px;
}
.blacklist-label {
width: 160px;
font-size: 14px;
color: #666;
background: #f5f7fa;
padding: 8px 12px;
border-radius: 4px 0 0 4px;
}
.blacklist-value {
flex: 1;
padding: 8px 12px;
border: 1px solid #eee;
border-left: none;
border-radius: 0 4px 4px 0;
text-align: center;
}
.tags-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.tag-item { display: flex; align-items: center; }
.tag-label {
width: 200px;
font-size: 14px;
color: #666;
background: #f9fafc;
padding: 8px 12px;
border-radius: 4px 0 0 4px;
}
.tag-value {
flex: 1;
padding: 8px 12px;
border: 1px solid #eee;
border-left: none;
border-radius: 0 4px 4px 0;
text-align: center;
}
@media (max-width: 768px) {
.tags-grid { grid-template-columns: 1fr; }
.tag-label { width: 140px; }
}
</style>

View File

@@ -0,0 +1,28 @@
<template>
<div class="gamma-card">
<div class="gamma-title"><span>🏠</span> 逾期勘测V3</div>
<div class="gamma-grid">
<div v-for="field in fields" :key="field.key" class="gamma-item">
<label>{{ field.label }}</label>
<span>{{ field.display }}</span>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { OVERDUE_SURVEY_FIELDS, cellText, overdueResultText } from '../reportHelper';
const props = defineProps({
data: { type: Object, default: () => ({}) },
});
const fields = computed(() =>
OVERDUE_SURVEY_FIELDS.map((f) => {
let display = cellText(props.data[f.key]);
if (f.key === 'result_code') display = overdueResultText(props.data[f.key]);
return { ...f, display };
}),
);
</script>

View File

@@ -0,0 +1,148 @@
<template>
<div class="gamma-card risk-assessment">
<div class="gamma-grid-2" style="grid-template-columns: 1fr 2fr; gap: 24px;">
<div class="risk-card">
<div class="gamma-title"><span>🛡</span> 风险评估</div>
<div class="risk-desc">
说明:<br>
<span v-for="item in RISK_LEVEL_DESC" :key="item.level">
{{ item.level }}{{ item.label }}: {{ item.desc }}<br>
</span>
</div>
<div class="risk-level-circle">
<div class="circle" :style="{ borderTopColor: levelColor }">
<div class="circle-text">{{ riskLevel }}</div>
</div>
<div class="risk-level-text">风险等级</div>
<div class="risk-stars">{{ riskStars }}</div>
</div>
</div>
<div class="risk-card index-group">
<div class="index-item">
<div class="index-title">信用风险指数</div>
<div class="index-circle">
<div class="index-status">{{ riskScoreDisplay }}</div>
<div class="gamma-small">{{ riskScoreUnit }}</div>
</div>
<div class="index-desc">信用风险评分0-1000分数越高用户信用越高</div>
</div>
<div class="index-item">
<div class="index-title">履约金额综合指数</div>
<div class="index-circle">
<div class="index-status">{{ amountIndexDisplay.value }}</div>
<div class="gamma-small">{{ amountIndexDisplay.unit }}</div>
</div>
<div class="index-desc">履约金额综合指数0-1000指数越大用户逾期可能性越低</div>
</div>
<div class="index-item">
<div class="index-title">履约笔数综合指数</div>
<div class="index-circle">
<div class="index-status">{{ countIndexDisplay.value }}</div>
<div class="gamma-small">{{ countIndexDisplay.unit }}</div>
</div>
<div class="index-desc">履约笔数综合指数0-1000指数越大用户逾期可能性越低</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { RISK_LEVEL_DESC, riskLevelColor, formatComplianceIndex, riskLevelStars } from '../reportHelper';
const props = defineProps({
riskLevel: { type: String, default: '—' },
riskScore: { type: [String, Number], default: '—' },
amountIndex: { type: [String, Number], default: '' },
countIndex: { type: [String, Number], default: '' },
});
const levelColor = computed(() => riskLevelColor(props.riskLevel));
const riskStars = computed(() => riskLevelStars(props.riskLevel));
const riskScoreDisplay = computed(() => {
if (props.riskScore === null || props.riskScore === undefined || props.riskScore === '') {
return '未命中';
}
return String(props.riskScore);
});
const riskScoreUnit = computed(() => (riskScoreDisplay.value === '未命中' ? '未命中' : '分'));
const amountIndexDisplay = computed(() => formatComplianceIndex(props.amountIndex));
const countIndexDisplay = computed(() => formatComplianceIndex(props.countIndex));
</script>
<style lang="scss" scoped>
.risk-assessment .risk-card {
background: #fff;
border-radius: 8px;
padding: 20px;
}
.risk-desc {
font-size: 13px;
color: #555;
line-height: 1.6;
}
.risk-level-circle {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 16px;
}
.circle {
width: 120px;
height: 120px;
border-radius: 50%;
border: 4px solid #eee;
border-top-color: #ff3333;
display: flex;
align-items: center;
justify-content: center;
}
.circle-text {
font-size: 48px;
font-weight: bold;
}
.risk-level-text {
font-size: 14px;
color: #666;
margin-top: 8px;
}
.risk-stars {
margin-top: 4px;
letter-spacing: 2px;
color: #d4af37;
}
.index-group {
display: flex;
justify-content: space-around;
align-items: center;
}
.index-item { text-align: center; }
.index-circle {
width: 100px;
height: 100px;
border-radius: 50%;
border: 3px solid #eee;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 0 auto 8px;
}
.index-title { font-size: 14px; margin-bottom: 4px; }
.index-status { font-size: 18px; font-weight: 600; }
.index-desc { font-size: 12px; color: #666; margin-top: 4px; line-height: 1.4; max-width: 180px; }
</style>

View File

@@ -0,0 +1,70 @@
<template>
<div class="gamma-card">
<div class="gamma-title"><span>🏮</span> 风险汇总</div>
<div class="summary-grid">
<div v-for="cat in RISK_SUMMARY_CATEGORIES" :key="cat.key" class="summary-card">
<div class="summary-card-title">
<span>{{ cat.icon }}</span>
<span>{{ cat.title }}</span>
</div>
<div
v-for="(item, i) in (risks[cat.key] || [])"
:key="i"
class="summary-item"
>
{{ item }}
</div>
<div v-if="!(risks[cat.key] || []).length" class="summary-empty">无风险项</div>
</div>
</div>
</div>
</template>
<script setup>
import { RISK_SUMMARY_CATEGORIES } from '../reportHelper';
defineProps({
risks: { type: Object, default: () => ({}) },
});
</script>
<style lang="scss" scoped>
.summary-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
.summary-card {
border: 1px solid #eee;
border-radius: 6px;
padding: 12px;
}
.summary-card-title {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
}
.summary-item {
font-size: 12px;
color: #d93025;
background: #fef0f0;
padding: 4px 8px;
border-radius: 4px;
margin: 4px 0;
}
.summary-empty {
font-size: 12px;
color: #999;
}
@media (max-width: 768px) {
.summary-grid { grid-template-columns: repeat(2, 1fr); }
}
</style>