This commit is contained in:
Mrx
2026-06-10 12:22:43 +08:00
parent 70cfe458be
commit d682a13af9
19 changed files with 4570 additions and 0 deletions

1072
public/DWBG9FB3.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -590,6 +590,11 @@ const featureMap = {
component: defineAsyncComponent(() => import("@/ui/DWBG5SAM/index.vue")),
remark: '天远指谜报告综合展示身份核验、信用等级、风险画像与名单、公安不良、逾期与司法案件等维度,数据来源于合作机构,仅供参考。',
},
DWBG9FB3: {
name: "个人大数据风险档案",
component: defineAsyncComponent(() => import("@/ui/DWBG9FB3/index.vue")),
remark: '个人大数据风险档案综合展示风险评估、基本信息、借贷画像、逾期黑名单、欺诈黑名单、投诉风险、逾期勘测、借贷意向与司法案件等维度,数据来源于合作机构,仅供参考。',
},
};
@@ -685,6 +690,7 @@ const featureRiskLevels = {
'CFLX3A9B': 5, // 法院被执行人限高版
'IVYZ4Y27' :3 , //xueli
'DWBG5SAM': 10,
'DWBG9FB3': 10,
// 🟡 中风险类 - 权重 5
'QYGL3F8E': 5, // 人企关系加强版

View File

@@ -17,6 +17,11 @@ const router = createRouter({
return import("./views/Report.vue");
},
},
{
path: "/DWBG9FB3",
name: "DWBG9FB3",
component: () => import("./views/DWBG9FB3Report.vue"),
},
],
});

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>

172
src/ui/DWBG9FB3/index.vue Normal file
View File

@@ -0,0 +1,172 @@
<template>
<div class="gamma-report">
<div ref="reportRef" class="gamma-container">
<div class="report-header">
<div class="report-title">个人大数据风险档案</div>
<div class="report-time">报告输出时间: {{ reportTime }}</div>
</div>
<RiskAssessmentSection
:risk-level="root.riskLevel"
:risk-score="root.riskScore"
:amount-index="root.loanRiskTagV21?.xyp_cpl0082"
:count-index="root.loanRiskTagV21?.xyp_cpl0083"
/>
<RiskSummarySection :risks="root.risks" />
<BasicInfoSection
:params="params"
:real-name-auth="root.realNameAuth"
:mobile3-verify="root.mobile3Verify"
:mobile4-verify="root.mobile4Verify"
:mobile-duration="root.mobileDuration"
:court-risk="root.courtRisk"
:personal-lawsuit="root.personalLawsuit"
/>
<LoanProfileSection :loan-total="root.loanTotal" />
<OverdueBlacklistSection :data="root.blackListV110" />
<FraudBlacklistSection :data="root.blackListV121_3" />
<ComplaintRiskSection :data="root.mobileRiskV709" />
<OverdueSurveySection :data="root.loanRiskTagV10" />
<CreditPanoramaSection :data="root.loanRiskTagV21" />
<LoanIntentSection :data="root.loanRiskTagV11" />
<JudicialCaseSection :data="root.personalLawsuit" />
</div>
</div>
</template>
<script setup>
import { computed, ref } from 'vue';
import { parseRoot, extractReportUrl } from './reportHelper';
import { printReportAsPdf } from './reportExport';
import RiskAssessmentSection from './components/RiskAssessmentSection.vue';
import RiskSummarySection from './components/RiskSummarySection.vue';
import BasicInfoSection from './components/BasicInfoSection.vue';
import LoanProfileSection from './components/LoanProfileSection.vue';
import OverdueBlacklistSection from './components/OverdueBlacklistSection.vue';
import FraudBlacklistSection from './components/FraudBlacklistSection.vue';
import ComplaintRiskSection from './components/ComplaintRiskSection.vue';
import OverdueSurveySection from './components/OverdueSurveySection.vue';
import CreditPanoramaSection from './components/CreditPanoramaSection.vue';
import LoanIntentSection from './components/LoanIntentSection.vue';
import JudicialCaseSection from './components/JudicialCaseSection.vue';
const props = defineProps({
data: { type: Object, default: () => ({}) },
params: { type: Object, default: () => ({}) },
apiId: { type: String, default: 'DWBG9FB3' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => {} },
reportDateTime: { type: String, default: '' },
showToolbar: { type: Boolean, default: false },
});
const root = computed(() => parseRoot(props.data));
const reportUrl = computed(() => extractReportUrl(props.data));
const reportRef = ref(null);
const exporting = ref(false);
const reportTime = computed(
() => props.reportDateTime || new Date().toLocaleString('zh-CN'),
);
</script>
<style lang="scss">
@import './shared.scss';
</style>
<style lang="scss" scoped>
.report-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
background: #fff;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
}
.report-title {
font-size: 24px;
font-weight: 600;
color: #8b4513;
}
.report-time {
font-size: 14px;
color: #666;
}
.report-toolbar {
max-width: 1200px;
margin: 0 auto 16px;
padding: 12px 20px;
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.toolbar-title {
font-size: 15px;
font-weight: 600;
color: #8b4513;
}
.toolbar-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.toolbar-btn {
padding: 8px 18px;
border: none;
border-radius: 6px;
background: #8b4513;
color: #fff;
font-size: 14px;
cursor: pointer;
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
&--secondary {
background: #fff;
color: #8b4513;
border: 1px solid #d4af37;
}
}
@media (max-width: 768px) {
.report-header {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
}
@media print {
.no-print {
display: none !important;
}
.gamma-report {
padding: 0;
background: #fff;
}
.gamma-card,
.panorama-report,
.intent-report {
break-inside: avoid;
box-shadow: none;
}
}
</style>

View File

@@ -0,0 +1,19 @@
/**
* 通过浏览器打印对话框导出 PDF目标打印机选「另存为 PDF」
* 零依赖,适合当前纯前端报告查看器;复杂图表场景可改用服务端渲染 PDF。
*/
export async function printReportAsPdf(reportElement) {
if (!reportElement) return;
document.body.classList.add('dwbg9fb3-printing');
await new Promise((resolve) => requestAnimationFrame(resolve));
try {
window.print();
} finally {
window.setTimeout(() => {
document.body.classList.remove('dwbg9fb3-printing');
}, 500);
}
}

View File

@@ -0,0 +1,533 @@
/** DWBG9FB3 伽马报告工具函数 */
export const RISK_LEVEL_DESC = [
{ level: 'A', label: '优秀', desc: '未命中风险策略,建议通过' },
{ level: 'B', label: '未知', desc: '命中未知风险项,建议人工复核' },
{ level: 'C', label: '良好', desc: '命中中低风险策略,建议审核' },
{ level: 'D', label: '一般', desc: '命中中风险策略,建议严格审核' },
{ level: 'E', label: '较差', desc: '命中中高风险策略,建议拒绝' },
{ level: 'F', label: '极差', desc: '命中高风险策略,建议拒绝' },
];
export const RISK_SUMMARY_CATEGORIES = [
{ key: 'mobile4Verify', title: '基本信息', icon: '📛' },
{ key: 'personalLawsuit', title: '司法案件', icon: '📑' },
{ key: 'loanRiskTagV11', title: '借贷意向', icon: '📈' },
{ key: 'loanRiskTagV10', title: '逾期勘测V3', icon: '📑' },
];
export const BLACKLIST_TAGS = [
{ key: 'black_tag04', label: '疑似短期频繁还款失败' },
{ key: 'black_tag05', label: '疑似短期频繁借贷' },
{ key: 'black_tag06', label: '疑似短期新机构频繁借贷' },
{ key: 'black_tag07', label: '疑似短期还款失败比例高' },
{ key: 'black_tag08', label: '疑似短期多机构逾期' },
{ key: 'black_tag09', label: '疑似中期频繁还款失败' },
{ key: 'black_tag10', label: '疑似短期新机构频繁还款失败' },
{ key: 'black_tag11', label: '疑似当前逾期较严重' },
{ key: 'black_tag12', label: '疑似高风险客户' },
];
export const FRAUD_DIST_LABELS = ['银行', '保险', '消金', '汽车金融', '小贷', '特殊金融', '混合金融', '其他金融'];
export const FRAUD_DIST_KEYS = [
{ d30: 'h1_30d', d90: 'h1_90d', d180: 'h1_180d' },
{ d30: 'h2_30d', d90: 'h2_90d', d180: 'h2_180d' },
{ d30: 'h3_30d', d90: 'h3_90d', d180: 'h3_180d' },
{ d30: 'h4_30d', d90: 'h4_90d', d180: 'h4_180d' },
{ d30: 'h5_30d', d90: 'h5_90d', d180: 'h5_180d' },
{ d30: 'h6_30d', d90: 'h6_90d', d180: 'h6_180d' },
{ d30: 'h7_30d', d90: 'h7_90d', d180: 'h7_180d' },
{ d30: 'h8_30d', d90: 'h8_90d', d180: 'h8_180d' },
];
export const OVERDUE_SURVEY_FIELDS = [
{ key: 'result_code', label: '探查结果', map: { 0: '正常', 1: '逾期未还款', 2: '逾期又还款' } },
{ key: 'max_overdue_amt', label: '最大逾期金额' },
{ key: 'max_overdue_days', label: '最长逾期天数' },
{ key: 'latest_overdue_time', label: '最近逾期时间' },
{ key: 'currently_performance', label: '最大履约金额' },
{ key: 'latest_performance_time', label: '最近履约时间' },
{ key: 'performance_count', label: '履约笔数' },
{ key: 'currently_overdue', label: '当前逾期机构数' },
{ key: 'currently_performance_org', label: '当前履约机构数' },
{ key: 'acc_exc', label: '异常还款机构数' },
{ key: 'acc_sleep', label: '睡眠机构数' },
];
export const LOAN_INTENT_PERIOD_PREFIXES = [
{ label: '近7天', prefix: 'd7' },
{ label: '近15天', prefix: 'd15' },
{ label: '近1个月', prefix: 'm1' },
{ label: '近3个月', prefix: 'm3' },
{ label: '近6个月', prefix: 'm6' },
{ label: '近12个月', prefix: 'm12' },
];
/** 构建 als 字段名 */
export function alsField(prefix, dim, suffix, metric) {
return `als_${prefix}_${dim}_${suffix}_${metric}`;
}
/** 借贷意向 id/cell 成对展示,空值显示 -/- */
export function formatAlsPair(data, idKey, cellKey) {
if (!data || !idKey || !cellKey) return '-/-';
const hasId = data[idKey] !== null && data[idKey] !== undefined && data[idKey] !== '';
const hasCell = data[cellKey] !== null && data[cellKey] !== undefined && data[cellKey] !== '';
if (!hasId && !hasCell) return '-/-';
const id = hasId ? String(data[idKey]) : '-';
const cell = hasCell ? String(data[cellKey]) : '-';
return `${id}/${cell}`;
}
export function formatAlsOrg(data, prefix, suffix) {
return formatAlsPair(
data,
alsField(prefix, 'id', suffix, 'orgnum'),
alsField(prefix, 'cell', suffix, 'orgnum'),
);
}
export function formatAlsCount(data, prefix, suffix) {
return formatAlsPair(
data,
alsField(prefix, 'id', suffix, 'allnum'),
alsField(prefix, 'cell', suffix, 'allnum'),
);
}
export const LOAN_INTENT_INST_ROWS = [
{ label: '银行', suffix: 'bank' },
{ label: '非银', suffix: 'nbank' },
];
export const LOAN_INTENT_CUSTOMER_ROWS = [
{ label: '银行汇总', suffix: 'bank', highlight: true },
{ label: '传统银行', suffix: 'bank_tra' },
{ label: '网络零售银行', suffix: 'bank_ret' },
{ label: '非银汇总', suffix: 'nbank', highlight: true },
{ label: '持牌小贷', suffix: 'nbank_sloan' },
{ label: '持牌网络小贷', suffix: 'nbank_nsloan' },
{ label: '持牌消费金融', suffix: 'nbank_cons' },
{ label: '其他', suffix: 'nbank_oth' },
];
export const LOAN_INTENT_BUSINESS_ROWS = [
{ label: '信用卡(类信用卡)', suffix: 'rel' },
{ label: '线上现金分期', suffix: 'caon' },
{ label: '线下现金分期', suffix: 'caoff' },
{ label: '线上小额现金贷', suffix: 'pdl' },
{ label: '汽车金融', suffix: 'af' },
{ label: '线上消费分期', suffix: 'coon' },
{ label: '线下消费分期', suffix: 'cooff' },
];
export const LOAN_INTENT_ABNORMAL_ROWS = [
{ label: '夜间-银行', suffix: 'bank_night' },
{ label: '夜间-非银', suffix: 'nbank_nnight' },
{ label: '周末-银行', suffix: 'bank_week' },
{ label: '周末-非银', suffix: 'nbank_week' },
];
export const LOAN_INTENT_NOTE =
'注:<br>1. 取值结果展示:按身份证号查询命中次数/按手机号查询命中次数。如:"1/2" 表示按身份证号查询命中1次按手机号查询命中2次。<br>2. 取值为 "-"、"0":无申请记录;';
export function cellText(v) {
if (v === null || v === undefined || v === '') return '—';
return String(v);
}
export function isHit(v) {
return v === 1 || v === '1' || v === true;
}
export function formatPair(data, idKey, cellKey) {
if (!data) return '—';
const id = cellText(data[idKey]);
const cell = cellText(data[cellKey]);
if (id === '—' && cell === '—') return '—';
return `${id}/${cell}`;
}
export function maskName(name) {
if (!name) return '—';
if (name.length <= 1) return '*';
if (name.length === 2) return `${name[0]}*`;
return `${name[0]}${'*'.repeat(name.length - 2)}${name[name.length - 1]}`;
}
export function maskIdCard(id) {
if (!id) return '—';
if (id.length <= 10) return id;
return `${id.slice(0, 6)}***********`;
}
export function maskMobile(mobile) {
if (!mobile || mobile.length !== 11) return cellText(mobile);
return `${mobile.slice(0, 3)}****${mobile.slice(7)}`;
}
export function mobileStatusText(status) {
const map = { 1: '正常', 2: '停机', 3: '销号', 4: '空号' };
return map[status] ?? cellText(status);
}
export function fraudGradeText(grade) {
const map = { 1: '低风险', 2: '较低风险', 3: '中风险', 4: '较高风险', 5: '高风险' };
return map[grade] ?? '未命中';
}
export function overdueResultText(code) {
const map = { 0: '正常', 1: '逾期未还款', 2: '逾期又还款' };
return map[code] ?? cellText(code);
}
export function riskLevelColor(level) {
const map = { A: '#2ecc71', B: '#3498db', C: '#f39c12', D: '#e67e22', E: '#e74c3c', F: '#ff3333' };
return map[level] ?? '#999';
}
export function parseRoot(data) {
if (!data || typeof data !== 'object') return {};
if (data.result && typeof data.result === 'object') return data.result;
if (data.data?.result) return data.data.result;
return data;
}
/** 从接口响应中提取查询人参数 */
export function extractReportParams(data) {
if (!data || typeof data !== 'object') return {};
if (data.reportParams && typeof data.reportParams === 'object') return data.reportParams;
if (data.params && typeof data.params === 'object') return data.params;
if (data.data?.reportParams) return data.data.reportParams;
return {};
}
/** 从接口响应中提取报告输出时间 */
export function extractReportTime(data) {
if (!data || typeof data !== 'object') return '';
return data.reportDateTime || data.timestamp || data.data?.timestamp || '';
}
/** 从接口响应中提取官方报告链接(若后端提供 PDF/在线报告) */
export function extractReportUrl(data) {
const root = parseRoot(data);
return root.reportUrl || data?.reportUrl || '';
}
/** 履约类指数0-1转为 0-1000 分展示 */
export function formatComplianceIndex(value) {
if (isEmptyXypValue(value)) {
return { value: '未命中', unit: '未命中' };
}
const n = parseFloat(value);
if (Number.isNaN(n)) {
return { value: '未命中', unit: '未命中' };
}
return { value: String(Math.round(n * 1000)), unit: '分' };
}
/** 根据风险等级 F→A 渲染星级(越高风险越少亮星) */
export function riskLevelStars(level) {
const map = { A: 5, B: 4, C: 3, D: 2, E: 1, F: 0 };
const lit = map[level] ?? 0;
return '★'.repeat(lit) + '☆'.repeat(5 - lit);
}
/** 司法风险项:优先 courtRisk 字段,被告未结案从案件统计推导 */
export function buildCourtRiskItems(courtRisk, personalLawsuit) {
const risk = courtRisk || {};
const count = personalLawsuit?.count || {};
const beigaoWei = risk.beigaoWei ?? (Number(count.count_wei_beigao) > 0);
return [
{ key: 'zhixing', label: '是否被执行人员', hit: !!risk.zhixing },
{ key: 'xiangao', label: '是否限制高消费人员', hit: !!risk.xiangao },
{ key: 'shean', label: '是否涉案人员', hit: !!risk.shean },
{ key: 'beigao', label: '是否被告人员', hit: !!risk.beigao },
{ key: 'beigaoWei', label: '是否被告未结案人员', hit: !!beigaoWei },
{ key: 'xingshi', label: '是否命中刑事案件', hit: !!risk.xingshi },
];
}
/** 投诉意愿登记文案 */
export function dncRegisterText(dnc) {
if (dnc === null || dnc === undefined || dnc === '') return '未查得';
if (dnc === 0 || dnc === '0') return '未查得';
return String(dnc);
}
/** 解析 "判决(2),维持(1)" 类分布字符串 */
export function parseStatDistribution(stat) {
if (!stat) return [];
return String(stat)
.split(',')
.map((part) => {
const m = part.trim().match(/^(.+)\((\d+)\)$/);
if (m) return { label: m[1].trim(), count: Number(m[2]) };
return { label: part.trim(), count: 1 };
})
.filter((item) => item.label);
}
const PIE_COLORS = ['#4E74F6', '#89d079', '#f6c358', '#ff7c7c', '#9b59b6', '#5dade2'];
/** 根据分布数据生成环形图 conic-gradient */
export function buildConicGradient(items) {
if (!items.length) return '#e8eef7';
const total = items.reduce((s, i) => s + i.count, 0) || 1;
let acc = 0;
const stops = items.map((item, i) => {
const pct = (item.count / total) * 100;
const start = acc;
acc += pct;
return `${PIE_COLORS[i % PIE_COLORS.length]} ${start}% ${acc}%`;
});
return `conic-gradient(${stops.join(', ')})`;
}
export function toCaseArray(v) {
if (Array.isArray(v)) return v;
if (v && typeof v === 'object' && Object.keys(v).length) return [v];
return [];
}
/** 从司法数据中提取列表(兼容多种字段名) */
export function extractJudicialList(data, keys) {
if (!data) return [];
for (const key of keys) {
const list = toCaseArray(data[key]);
if (list.length) return list;
}
return [];
}
export const REPORT_USAGE_NOTICE = [
'客户使用本报告,需经过被查询人授权,客户承担因授权不充分引起的任何法律责任。',
'本报告仅限客户内部使用,请妥善保管本报告,不得向任何第三方泄露或允许任何第三方使用本报告。',
'本报告仅供客户参考,不作为客户决策的依据。',
'未经我司书面许可,任何人不得擅自复制、摘录、编辑、转载、披露和发表。',
'请确保在安全的物理及网络环境操作并确保导出内容的保密、安全以及合规应用。',
];
export function caseListText(c) {
if (!c) return '—';
return `${c.c_ah || '—'}${c.n_ssdw || '—'}${c.n_ajjzjd || '—'}`;
}
export function limitHighRow(item) {
return {
ah: item.c_ah || item.ah || item.caseCode || '—',
legalPerson: item.legalPerson || item.qyfr || item.businessentity || '—',
entName: item.entName || item.qymc || item.companyName || '—',
court: item.n_jbfy || item.court || item.zxfy || '—',
publishDate: item.publishDate || item.fbrq || item.fb_date || '—',
filingDate: item.d_larq || item.larq || item.regDate || '—',
};
}
/** 信用全景扫描 — 模型指数 */
export const CREDIT_PANORAMA_SCORES = [
{ key: 'xyp_model_score_high', label: '小额网贷指数' },
{ key: 'xyp_model_score_mid', label: '小额分期指数' },
{ key: 'xyp_model_score_low', label: '中大额分期指数' },
];
/** 信用全景扫描 — 机构借贷情况 */
export const CREDIT_PANORAMA_INSTITUTIONS = [
{ key: 'xyp_cpl0001', label: '贷款总机构数', unit: '家' },
{ key: 'xyp_cpl0002', label: '贷款已结清机构数', unit: '家' },
{ key: 'xyp_cpl0007', label: '消费金融类机构数', unit: '家' },
{ key: 'xyp_cpl0008', label: '网络贷款类机构数', unit: '家' },
];
/** 信用全景扫描 — 近期贷款申请机构数 */
export const CREDIT_PANORAMA_RECENT_LOAN = [
{ key: 'xyp_cpl0070', label: '最近1天贷款机构数', unit: '家' },
{ key: 'xyp_cpl0009', label: '最近7天贷款机构数', unit: '家' },
{ key: 'xyp_cpl0010', label: '最近14贷款机构数', unit: '家' },
{ key: 'xyp_cpl0050', label: '最近21天贷款机构数', unit: '家' },
{ key: 'xyp_cpl0011', label: '最近30天贷款机构数', unit: '家' },
{ key: 'xyp_cpl0012', label: '最近90天贷款机构数', unit: '家' },
{ key: 'xyp_cpl0013', label: '最近180天贷款机构数', unit: '家' },
];
/** 信用全景扫描 — 交易失败笔数 */
export const CREDIT_PANORAMA_FAIL_COUNTS = [
{ key: 'xyp_cpl0016', label: '最近1天交易失败笔数', unit: '笔' },
{ key: 'xyp_cpl0018', label: '最近7天交易失败笔数', unit: '笔' },
{ key: 'xyp_cpl0020', label: '最近14天交易失败笔数', unit: '笔' },
{ key: 'xyp_cpl0065', label: '最近21天交易失败笔数', unit: '笔' },
{ key: 'xyp_cpl0022', label: '最近30天交易失败笔数', unit: '笔' },
{ key: 'xyp_cpl0024', label: '最近90天交易失败笔数', unit: '笔' },
{ key: 'xyp_cpl0026', label: '最近180天交易失败笔数', unit: '笔' },
{ key: 'xyp_t03td148', label: '最近一次交易为交易失败机构数', unit: '家' },
];
/** 信用全景扫描 — 交易失败后还款 */
export const CREDIT_PANORAMA_FAIL_RECOVERY = [
{ key: 'xyp_cpl0052', label: '消费金融类最后一次交易失败后还款次数', unit: '次' },
{ key: 'xyp_cpl0053', label: '小贷担保类最后一次交易失败后还款次数', unit: '次' },
{ key: 'xyp_cpl0069', label: '最后一次交易失败后还款次数', unit: '次' },
{ key: 'xyp_cpl0056', label: '交易失败向后距离下一次还款成功的天数最大值', unit: '天' },
{ key: 'xyp_cpl0062', label: '交易失败向后距离下一次还款成功的天数平均值', unit: '天' },
];
/** 信用全景扫描 — 逾期标识 */
export const CREDIT_PANORAMA_OVERDUE_FLAGS = [
{ key: 'xyp_cpl0044', label: '当前是否存在逾期未结清', isFlag: true },
{ key: 'xyp_cpl0028', label: '最近1天是否发生过逾期', isFlag: true },
{ key: 'xyp_cpl0029', label: '最近7天是否发生过逾期', isFlag: true },
{ key: 'xyp_cpl0030', label: '最近14天是否发生过逾期', isFlag: true },
{ key: 'xyp_cpl0031', label: '最近30天是否发生过逾期', isFlag: true },
];
/** 信用全景扫描 — 贷款整体情况 */
export const CREDIT_PANORAMA_LOAN_OVERVIEW = [
{ key: 'xyp_cpl0045', label: '信用贷款时长', unit: '天', type: 'interval' },
{ key: 'xyp_cpl0046', label: '最近一次交易距离当前时间', unit: '天', type: 'interval' },
{ key: 'xyp_t01aczzzz', label: '累计交易金额', unit: '元', type: 'amount' },
{ key: 'xyp_cpl0058', label: '因交易能力不足导致失败的交易金额(最小值)', unit: '元', type: 'amount' },
{ key: 'xyp_cpl0014', label: '历史贷款机构成功还款笔数', unit: '笔', type: 'interval' },
{ key: 'xyp_cpl0015', label: '历史贷款机构交易失败笔数', unit: '笔', type: 'interval' },
];
/** 信用全景扫描 — 分周期汇总表 */
export const CREDIT_PANORAMA_PERIOD_ROWS = [
{
label: '近1天',
instKey: 'xyp_cpl0070',
successKey: 'xyp_cpl0017',
successAmtKey: 'xyp_cpl0033',
failKey: 'xyp_cpl0016',
failAmtKey: 'xyp_cpl0032',
},
{
label: '近7天',
instKey: 'xyp_cpl0009',
successKey: 'xyp_cpl0019',
successAmtKey: 'xyp_cpl0035',
failKey: 'xyp_cpl0018',
failAmtKey: 'xyp_cpl0034',
},
{
label: '近14天',
instKey: 'xyp_cpl0010',
successKey: 'xyp_cpl0021',
successAmtKey: 'xyp_cpl0037',
failKey: 'xyp_cpl0020',
failAmtKey: 'xyp_cpl0036',
},
{
label: '近21天',
instKey: 'xyp_cpl0050',
successKey: 'xyp_cpl0064',
successAmtKey: 'xyp_cpl0067',
failKey: 'xyp_cpl0065',
failAmtKey: 'xyp_cpl0066',
},
{
label: '近30天',
instKey: 'xyp_cpl0011',
successKey: 'xyp_cpl0023',
successAmtKey: 'xyp_cpl0039',
failKey: 'xyp_cpl0022',
failAmtKey: 'xyp_cpl0038',
},
{
label: '近90天',
instKey: 'xyp_cpl0012',
successKey: 'xyp_cpl0025',
successAmtKey: 'xyp_cpl0041',
failKey: 'xyp_cpl0024',
failAmtKey: 'xyp_cpl0040',
},
{
label: '近180天',
instKey: 'xyp_cpl0013',
successKey: 'xyp_cpl0027',
successAmtKey: 'xyp_cpl0043',
failKey: 'xyp_cpl0026',
failAmtKey: 'xyp_cpl0042',
},
];
export function isEmptyXypValue(v) {
return v === null || v === undefined || v === '';
}
/** 解析 xyp 区间码为展示数值,空值返回 null */
export function parseXypInterval(value) {
if (isEmptyXypValue(value)) return null;
const num = parseInt(value, 10);
if (Number.isNaN(num)) return null;
switch (num) {
case 1: return 1;
case 2: return 3;
case 3: return 7;
case 4: return 15;
case 5: return 25;
default: return num;
}
}
export function formatPanoramaCount(value, unit = '') {
if (isEmptyXypValue(value)) return '-';
const n = parseXypInterval(value);
if (n === null) return '-';
if (n === 0) return `0${unit}`;
if (n < 5) return `${n}${unit}`;
return `${n}+${unit}`;
}
export function formatPanoramaAmount(value) {
if (isEmptyXypValue(value)) return '-';
const n = parseXypInterval(value);
if (n === null) return '-';
if (n === 0) return '0元';
if (n < 1000) return `${n}`;
if (n < 10000) return `${(n / 1000).toFixed(1)}千元`;
return `${(n / 10000).toFixed(1)}万元`;
}
export function formatPanoramaRatio(value) {
if (isEmptyXypValue(value)) return '-';
const n = parseFloat(value);
if (Number.isNaN(n)) return '-';
return `${(n * 100).toFixed(1)}%`;
}
export function formatPanoramaYesNo(value) {
if (isEmptyXypValue(value)) return '-';
return value === '1' || value === 1 ? '是' : '否';
}
export function formatModelScore(value) {
if (isEmptyXypValue(value)) return '未命中';
const n = parseInt(value, 10);
if (Number.isNaN(n)) return '未命中';
return String(n);
}
export function getPanoramaRiskTag(value, { isFlag = false } = {}) {
if (isEmptyXypValue(value)) {
return { label: '低风险', danger: false, warn: false };
}
if (isFlag) {
const hit = value === '1' || value === 1;
return hit
? { label: '高风险', danger: true, warn: false }
: { label: '低风险', danger: false, warn: false };
}
const n = parseXypInterval(value) ?? 0;
if (n >= 15) return { label: '高风险', danger: true, warn: false };
if (n >= 7) return { label: '中风险', danger: false, warn: true };
return { label: '低风险', danger: false, warn: false };
}
export function panoramaTableCell(data, key, type = 'count') {
if (!data || !key) return '-';
const v = data[key];
if (type === 'amount') return formatPanoramaAmount(v);
return formatPanoramaCount(v, type === 'inst' ? '' : '');
}

133
src/ui/DWBG9FB3/shared.scss Normal file
View File

@@ -0,0 +1,133 @@
.gamma-report {
--gamma-brown: #8b4513;
--gamma-danger: #d93025;
--gamma-success: #2ecc71;
--gamma-bg: #f5f7fa;
background: var(--gamma-bg);
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 14px;
color: #333;
}
.gamma-container {
max-width: 1200px;
margin: 0 auto;
}
.gamma-card {
background: #fff;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.gamma-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
color: #333;
}
.gamma-subtitle {
display: flex;
align-items: center;
gap: 8px;
font-size: 15px;
font-weight: 600;
color: #333;
margin: 16px 0 12px;
padding-bottom: 4px;
border-bottom: 2px solid #f0e6d6;
}
.gamma-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 12px;
}
.gamma-grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.gamma-grid-3 {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
}
.gamma-grid-4 {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
.gamma-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 10px;
background: #fafbfc;
border-radius: 4px;
}
.gamma-item label {
color: #666;
}
.gamma-tag {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
background: #e6f7e6;
color: #3cb371;
}
.gamma-tag--danger {
background: #fef0f0;
color: var(--gamma-danger);
}
.gamma-tag--warn {
background: #fff7e6;
color: #d89614;
}
.gamma-table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
th {
background: #fff8e6;
padding: 10px;
text-align: left;
font-weight: 500;
border: 1px solid #eee;
}
td {
padding: 10px;
border: 1px solid #eee;
}
}
.gamma-text-danger { color: var(--gamma-danger); }
.gamma-text-success { color: var(--gamma-success); }
.gamma-small { font-size: 12px; color: #999; }
@media (max-width: 768px) {
.gamma-grid-2,
.gamma-grid-3,
.gamma-grid-4 {
grid-template-columns: 1fr;
}
}

View File

@@ -0,0 +1,95 @@
<template>
<DWBG9FB3Report
v-if="isDone"
:data="reportData"
:params="reportParams"
:report-date-time="reportDateTime"
:show-toolbar="true"
/>
<div v-else class="loading-container">
<div class="loading-spinner" />
<p>加载中请稍候...</p>
</div>
</template>
<script setup>
import DWBG9FB3Report from '@/ui/DWBG9FB3/index.vue';
import { extractReportParams, extractReportTime } from '@/ui/DWBG9FB3/reportHelper';
const reportData = ref({});
const reportParams = ref({});
const reportDateTime = ref('');
const isDone = ref(false);
onMounted(async () => {
try {
const response = await fetch('/DWBG9FB3.json');
const json = await response.json();
reportData.value = json;
reportParams.value = extractReportParams(json);
reportDateTime.value =
extractReportTime(json) || new Date().toLocaleString('zh-CN');
isDone.value = true;
} catch (error) {
console.error('[DWBG9FB3Report] 加载示例数据失败:', error);
isDone.value = true;
}
});
</script>
<style lang="scss" scoped>
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
.loading-spinner {
width: 50px;
height: 50px;
border: 4px solid #f3f3f3;
border-top: 4px solid #8b4513;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 20px;
}
p { color: #666; font-size: 16px; }
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
<style lang="scss">
@media print {
@page {
size: A4;
margin: 10mm;
}
body.dwbg9fb3-printing {
background: #fff !important;
#app {
min-height: auto;
}
.gamma-report {
padding: 0 !important;
background: #fff !important;
}
.gamma-container {
max-width: none;
}
.no-print {
display: none !important;
}
}
}
</style>