1467 lines
36 KiB
Vue
1467 lines
36 KiB
Vue
<template>
|
||
<div class="rounded-lg border border-[#99999933] mb-4">
|
||
<!-- 标题栏 -->
|
||
<div class="flex items-center mb-4 p-4">
|
||
<div class="w-8 h-8 flex items-center justify-center mr-2">
|
||
<img src="@/assets/images/report/zwsc.png" alt="贷款行为分析" class="w-8 h-8 object-contain" />
|
||
</div>
|
||
<span class="font-bold text-gray-800">贷款行为分析</span>
|
||
</div>
|
||
|
||
<!-- 交易金额趋势 -->
|
||
<div class="mb-6">
|
||
<LTitle title="交易金额趋势" class="mb-2" />
|
||
|
||
<!-- ECharts 图表 -->
|
||
<div class="mb-6">
|
||
<div ref="amountChartRef" :style="{ width: '100%', height: '300px' }"></div>
|
||
</div>
|
||
|
||
<!-- 数值统计 -->
|
||
<div class="bg-[#ECF2FD] rounded-lg border border-[#CADAF9] p-4 mx-4">
|
||
<div class="flex justify-between gap-4">
|
||
<div class="flex-1 text-center">
|
||
<div class="text-[#999999]">峰值</div>
|
||
<div class="text-[#2B79EE] font-bold">{{ formatAmount(maxAmountTrend) }}</div>
|
||
</div>
|
||
<div class="flex-1 text-center">
|
||
<div class="text-[#999999]">低值</div>
|
||
<div class="text-[#2B79EE] font-bold">{{ formatAmount(minAmountTrend) }}</div>
|
||
</div>
|
||
<div class="flex-1 text-center">
|
||
<div class="text-[#999999]">均值</div>
|
||
<div class="text-[#2B79EE] font-bold">{{ formatAmount(avgAmountTrend) }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 交易笔数趋势 -->
|
||
<div class="mb-6">
|
||
<LTitle title="交易笔数趋势" class="mb-2" />
|
||
|
||
<!-- ECharts 图表 -->
|
||
<div class="mb-6">
|
||
<div ref="countChartRef" :style="{ width: '100%', height: '300px' }"></div>
|
||
</div>
|
||
|
||
<!-- 统计摘要 -->
|
||
<div class="bg-[#ECF2FD] rounded-lg border border-[#CADAF9] p-4 mx-4">
|
||
<div class="flex justify-between gap-4">
|
||
<div class="flex-1 text-center">
|
||
<div class="text-[#999999]">峰值</div>
|
||
<div class="text-[#2B79EE] font-bold">{{ maxCountTrend }} 笔</div>
|
||
</div>
|
||
<div class="flex-1 text-center">
|
||
<div class="text-[#999999]">低值</div>
|
||
<div class="text-[#2B79EE] font-bold">{{ minCountTrend }} 笔</div>
|
||
</div>
|
||
<div class="flex-1 text-center">
|
||
<div class="text-[#999999]">均值</div>
|
||
<div class="text-[#2B79EE] font-bold">{{ avgCountTrend.toFixed(0) }} 笔</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 还款成功率趋势 -->
|
||
<div class="mb-6">
|
||
<LTitle title="还款成功率趋势" class="mb-2" />
|
||
|
||
<div class="space-y-3 px-4">
|
||
<div class="bg-[#ECF2FD] rounded-lg border border-[#CADAF9] p-4" v-for="rate in successRateTrend"
|
||
:key="rate.period">
|
||
<div class="flex justify-between items-center mb-3">
|
||
<span class="text-base font-bold text-[#333333]">{{ rate.period }}</span>
|
||
<span class="text-base font-bold text-[#333333]">
|
||
{{ (rate.rate * 100).toFixed(1) }}%
|
||
</span>
|
||
</div>
|
||
<div class="h-2 bg-[#DBE6FC] rounded-full overflow-hidden mb-3">
|
||
<div class="h-full rounded-full transition-all duration-300"
|
||
:style="`width: ${Math.max(rate.rate * 100, 2)}%; background-color: #5079EA;`"></div>
|
||
</div>
|
||
<div class="flex justify-between text-xs text-[#999999]">
|
||
<span>成功: {{ rate.success }}笔</span>
|
||
<span>失败: {{ rate.failure }}笔</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 机构数量变化趋势 -->
|
||
<div class="mb-6">
|
||
<LTitle title="机构数量变化" class="mb-2" />
|
||
|
||
<div class="space-y-3 px-4">
|
||
<div class="bg-[#ECF2FD] rounded-lg border border-[#CADAF9] p-4" v-for="item in institutionTrendData"
|
||
:key="item.period">
|
||
<div class="flex justify-between items-center mb-3">
|
||
<span class="text-base font-bold text-[#333333]">{{ item.period }}</span>
|
||
<span class="text-base font-bold text-[#333333]">总数: {{ item.total }}</span>
|
||
</div>
|
||
|
||
<div class="h-2 bg-[#DBE6FC] rounded-full overflow-hidden mb-3">
|
||
<div class="h-full rounded-full transition-all duration-300"
|
||
:style="`width: ${Math.max(item.totalPercentage, 2)}%; background-color: #5079EA;`"></div>
|
||
</div>
|
||
|
||
<div class="flex items-center gap-2 text-xs text-[#999999]">
|
||
<svg v-if="item.trendText === '增长趋势'" class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
||
<path fill-rule="evenodd"
|
||
d="M3.293 9.707a1 1 0 010-1.414l6-6a1 1 0 011.414 0l6 6a1 1 0 01-1.414 1.414L11 5.414V17a1 1 0 11-2 0V5.414L4.707 9.707a1 1 0 01-1.414 0z"
|
||
clip-rule="evenodd" />
|
||
</svg>
|
||
<svg v-else-if="item.trendText === '下降趋势'" class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
||
<path fill-rule="evenodd"
|
||
d="M16.707 10.293a1 1 0 010 1.414l-6 6a1 1 0 01-1.414 0l-6-6a1 1 0 111.414-1.414L9 14.586V3a1 1 0 012 0v11.586l4.293-4.293a1 1 0 011.414 0z"
|
||
clip-rule="evenodd" />
|
||
</svg>
|
||
<svg v-else class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
||
<path fill-rule="evenodd" d="M3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd" />
|
||
</svg>
|
||
<span>{{ item.trendText }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 新增机构金额分析 -->
|
||
<div class="mb-6">
|
||
<LTitle title="新增机构金额分析" class="mb-2" />
|
||
|
||
<!-- 标签页布局 -->
|
||
<div class="">
|
||
<div class="space-y-4">
|
||
<div class="performance-item">
|
||
<div class="loan-evaluation-wrap">
|
||
<!-- 标签页 -->
|
||
<div class="mb-3">
|
||
<van-tabs v-model:active="activeAmountPeriod" line-width="20" line-height="2"
|
||
color="var(--color-primary)" class="loan-evaluation-tabs">
|
||
<van-tab v-for="item in newInstitutionAmounts" :key="item.period" :name="item.period"
|
||
:title="item.period" />
|
||
</van-tabs>
|
||
</div>
|
||
|
||
<!-- 内容显示 -->
|
||
<div class="loan-evaluation-content">
|
||
<div class="space-y-3">
|
||
<!-- 新增金额 -->
|
||
<div class="space-y-2">
|
||
<div class="flex justify-between items-center">
|
||
<span class="text-sm text-gray-600">新增</span>
|
||
<span class="text-sm font-bold text-gray-800">{{ formatAmount(currentAmountData.totalAmount)
|
||
}}</span>
|
||
</div>
|
||
<div class="h-2 rounded-full bg-gray-200">
|
||
<div class="h-2 rounded-full transition-all duration-500"
|
||
:style="`width: ${Math.max(currentAmountData.percentage, 2)}%; background-color: #10b981;`">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 最大单笔 -->
|
||
<div class="flex justify-between items-center">
|
||
<span class="text-sm text-gray-600">最大单笔</span>
|
||
<span class="text-sm font-bold text-gray-800">{{ formatAmount(currentAmountData.maxAmount) }}</span>
|
||
</div>
|
||
|
||
<!-- 最小单笔 -->
|
||
<div class="flex justify-between items-center">
|
||
<span class="text-sm text-gray-600">最小单笔</span>
|
||
<span class="text-sm font-bold text-gray-800">{{ formatAmount(currentAmountData.minAmount) }}</span>
|
||
</div>
|
||
|
||
<!-- 平均金额 -->
|
||
<div class="flex justify-between items-center">
|
||
<span class="text-sm text-gray-600">平均金额</span>
|
||
<span class="text-sm font-bold text-gray-800">{{ formatAmount(currentAmountData.avgAmount) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 风险指标时间分布 -->
|
||
<div class="mb-6">
|
||
<LTitle title="风险指标时间分布" class="mb-2" />
|
||
|
||
<div class="px-4">
|
||
<!-- 风险事件列表 -->
|
||
<div class="space-y-3 mb-4" v-if="riskTimeline.length > 0">
|
||
<div class="flex items-center gap-3 p-3 rounded-lg" v-for="event in riskTimeline" :key="event.id"
|
||
:class="getRiskEventCardClass(event.riskLevel)">
|
||
<div class="w-10 h-10 flex-shrink-0">
|
||
<img :src="getRiskEventIcon(event.riskLevel)" :alt="event.title" class="w-10 h-10 object-contain" />
|
||
</div>
|
||
<div class="flex-1">
|
||
<div class="flex justify-between items-start">
|
||
<div>
|
||
<h4 class="text-sm font-bold text-[#333333]">{{ event.title }}</h4>
|
||
<p class="text-xs text-[#999999] mt-1">{{ event.description }}</p>
|
||
</div>
|
||
<span class="text-xs text-[#999999] whitespace-nowrap ml-2">{{ event.timeAgo }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 统计摘要 -->
|
||
<div class="bg-[#F9F9F9] rounded-lg border p-4 border-[#EEEEEE]">
|
||
<div class="flex justify-between text-center">
|
||
<div class="flex-1">
|
||
<div class="text-xs text-gray-600">高风险事件</div>
|
||
<div class="text-lg font-bold text-red-600">{{ riskEventCounts.high }}</div>
|
||
</div>
|
||
<div class="flex-1">
|
||
<div class="text-xs text-gray-600">中风险事件</div>
|
||
<div class="text-lg font-bold text-yellow-600">{{ riskEventCounts.medium }}</div>
|
||
</div>
|
||
<div class="flex-1">
|
||
<div class="text-xs text-gray-600">低风险事件</div>
|
||
<div class="text-lg font-bold text-green-600">{{ riskEventCounts.low }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 温馨提示 -->
|
||
<LRemark
|
||
content="时间趋势分析通过可视化图表展示申请人的交易金额趋势、申请频率趋势和风险变化趋势。图表数据基于历史申请记录生成,可以直观反映申请行为的时间规律性。建议重点关注异常波动时期,如短期内交易金额大幅增长或申请频率异常增高的情况。趋势分析有助于识别周期性风险模式和预测未来风险走向。数据统计周期通常为近12个月。" />
|
||
</template>
|
||
|
||
<script>
|
||
import LTitle from '@/components/LTitle.vue'
|
||
import LRemark from '@/components/LRemark.vue'
|
||
import * as echarts from 'echarts'
|
||
|
||
export default {
|
||
name: 'TimeTrendAnalysis',
|
||
components: {
|
||
LTitle,
|
||
LRemark
|
||
},
|
||
props: {
|
||
data: {
|
||
type: Object,
|
||
default: () => ({})
|
||
}
|
||
},
|
||
data() {
|
||
return {
|
||
amountChartInstance: null,
|
||
countChartInstance: null,
|
||
activeAmountPeriod: '最近90天新增'
|
||
}
|
||
},
|
||
computed: {
|
||
// 交易金额趋势点
|
||
amountTrendPoints() {
|
||
return [
|
||
{ label: '最近7天', value: this.parseIntervalValue(this.data.xyp_t01acbzzz) },
|
||
{ label: '最近14天', value: this.parseIntervalValue(this.data.xyp_t01acczzz) },
|
||
{ label: '最近21天', value: this.parseIntervalValue(this.data.xyp_t01acdzzz) },
|
||
{ label: '最近30天', value: this.parseIntervalValue(this.data.xyp_t01acezzz) },
|
||
{ label: '最近90天', value: this.parseIntervalValue(this.data.xyp_t01acfzzz) },
|
||
{ label: '最近180天', value: this.parseIntervalValue(this.data.xyp_t01acgzzz) }
|
||
]
|
||
},
|
||
|
||
maxAmountTrend() {
|
||
return Math.max(...this.amountTrendPoints.map(p => p.value))
|
||
},
|
||
|
||
minAmountTrend() {
|
||
return Math.min(...this.amountTrendPoints.map(p => p.value))
|
||
},
|
||
|
||
avgAmountTrend() {
|
||
const sum = this.amountTrendPoints.reduce((acc, p) => acc + p.value, 0)
|
||
return sum / this.amountTrendPoints.length
|
||
},
|
||
|
||
// 交易笔数趋势数据
|
||
countTrendData() {
|
||
return [
|
||
{ label: '近7日', value: this.parseIntervalValue(this.data.xyp_t01aabzzz) },
|
||
{ label: '近14日', value: this.parseIntervalValue(this.data.xyp_t01aaczzz) },
|
||
{ label: '近21日', value: this.parseIntervalValue(this.data.xyp_t01aadzzz) },
|
||
{ label: '近30日', value: this.parseIntervalValue(this.data.xyp_t01aaezzz) },
|
||
{ label: '近90日', value: this.parseIntervalValue(this.data.xyp_t01aafzzz) },
|
||
{ label: '近180日', value: this.parseIntervalValue(this.data.xyp_t01aagzzz) }
|
||
]
|
||
},
|
||
|
||
maxCountTrend() {
|
||
return Math.max(...this.countTrendData.map(d => d.value))
|
||
},
|
||
|
||
minCountTrend() {
|
||
return Math.min(...this.countTrendData.map(d => d.value))
|
||
},
|
||
|
||
avgCountTrend() {
|
||
const sum = this.countTrendData.reduce((acc, d) => acc + d.value, 0)
|
||
return sum / this.countTrendData.length
|
||
},
|
||
|
||
|
||
// 还款成功率趋势
|
||
successRateTrend() {
|
||
return [
|
||
{
|
||
period: '最近90天',
|
||
success: this.parseIntervalValue(this.data.xyp_cpl0025),
|
||
failure: this.parseIntervalValue(this.data.xyp_cpl0024)
|
||
},
|
||
{
|
||
period: '最近180天',
|
||
success: this.parseIntervalValue(this.data.xyp_cpl0027),
|
||
failure: this.parseIntervalValue(this.data.xyp_cpl0026)
|
||
}
|
||
].map(item => {
|
||
const total = item.success + item.failure || 1
|
||
return {
|
||
...item,
|
||
rate: item.success / total
|
||
}
|
||
})
|
||
},
|
||
|
||
// 机构数量变化趋势
|
||
institutionTrendData() {
|
||
const data = [
|
||
{
|
||
period: '最近30天',
|
||
total: this.parseIntervalValue(this.data.xyp_cpl0011),
|
||
new: this.parseNewInstitutionCount(this.data.xyp_t02dezezz_dezzzz, this.data.xyp_cpl0001)
|
||
},
|
||
{
|
||
period: '最近90天',
|
||
total: this.parseIntervalValue(this.data.xyp_cpl0012),
|
||
new: this.parseNewInstitutionCount(this.data.xyp_t02dezfzz_dezzzz, this.data.xyp_cpl0001)
|
||
},
|
||
{
|
||
period: '最近180天',
|
||
total: this.parseIntervalValue(this.data.xyp_cpl0013),
|
||
new: this.parseNewInstitutionCount(this.data.xyp_t02dezgzz_dezzzz, this.data.xyp_cpl0001)
|
||
}
|
||
]
|
||
|
||
const maxTotal = Math.max(...data.map(d => d.total)) || 1
|
||
const maxNew = Math.max(...data.map(d => d.new)) || 1
|
||
|
||
return data.map((item, index) => {
|
||
const prevItem = index > 0 ? data[index - 1] : null
|
||
let trendClass = 'trend-stable'
|
||
let trendIcon = 'fas fa-minus'
|
||
let trendText = '保持稳定'
|
||
|
||
if (prevItem) {
|
||
if (item.total > prevItem.total) {
|
||
trendClass = 'trend-up'
|
||
trendIcon = 'fas fa-arrow-up'
|
||
trendText = '增长趋势'
|
||
} else if (item.total < prevItem.total) {
|
||
trendClass = 'trend-down'
|
||
trendIcon = 'fas fa-arrow-down'
|
||
trendText = '下降趋势'
|
||
}
|
||
}
|
||
|
||
return {
|
||
...item,
|
||
totalPercentage: (item.total / maxTotal) * 100,
|
||
newPercentage: (item.new / maxNew) * 30,
|
||
trendClass,
|
||
trendIcon,
|
||
trendText
|
||
}
|
||
})
|
||
},
|
||
|
||
// 风险事件时间线
|
||
riskTimeline() {
|
||
const events = []
|
||
|
||
// 基于数据生成风险事件
|
||
if (this.data.xyp_cpl0028 === '1') {
|
||
events.push({
|
||
id: 'overdue_1day',
|
||
title: '最近1天逾期',
|
||
description: '检测到逾期行为',
|
||
timeAgo: '1天前',
|
||
position: 95,
|
||
riskLevel: 'high-risk'
|
||
})
|
||
}
|
||
|
||
if (this.data.xyp_cpl0029 === '1') {
|
||
events.push({
|
||
id: 'overdue_7day',
|
||
title: '最近7天逾期',
|
||
description: '7天内发生逾期',
|
||
timeAgo: '7天前',
|
||
position: 85,
|
||
riskLevel: 'high-risk'
|
||
})
|
||
}
|
||
|
||
if (this.parseIntervalValue(this.data.xyp_cpl0070) > 0) {
|
||
events.push({
|
||
id: 'new_loan_1day',
|
||
title: '最近1天新贷款',
|
||
description: '申请新的贷款机构',
|
||
timeAgo: '1天前',
|
||
position: 92,
|
||
riskLevel: 'medium-risk'
|
||
})
|
||
}
|
||
|
||
if (this.parseIntervalValue(this.data.xyp_cpl0009) > 5) {
|
||
events.push({
|
||
id: 'high_activity_7day',
|
||
title: '7天高频申请',
|
||
description: '短期内多次申请',
|
||
timeAgo: '7天内',
|
||
position: 80,
|
||
riskLevel: 'medium-risk'
|
||
})
|
||
}
|
||
|
||
const settledInstitutions = this.parseIntervalValue(this.data.xyp_cpl0002)
|
||
if (settledInstitutions > 0) {
|
||
events.push({
|
||
id: 'settled_loans',
|
||
title: '成功结清贷款',
|
||
description: `已结清${settledInstitutions}家机构`,
|
||
timeAgo: '历史记录',
|
||
position: 20,
|
||
riskLevel: 'low-risk'
|
||
})
|
||
}
|
||
|
||
return events.sort((a, b) => b.position - a.position)
|
||
},
|
||
|
||
// 新增机构金额分析
|
||
newInstitutionAmounts() {
|
||
const amounts = [
|
||
{
|
||
period: '最近90天',
|
||
totalAmount: this.parseIntervalValue(this.data.xyp_t01aczfzz),
|
||
maxAmount: this.parseIntervalValue(this.data.xyp_t01aazfzz),
|
||
minAmount: this.parseIntervalValue(this.data.xyp_t01abzfzz),
|
||
avgAmount: this.parseIntervalValue(this.data.xyp_t01adzfbz)
|
||
},
|
||
{
|
||
period: '最近180天',
|
||
totalAmount: this.parseIntervalValue(this.data.xyp_t01aczgzz),
|
||
maxAmount: this.parseIntervalValue(this.data.xyp_t01aazgzc),
|
||
minAmount: this.parseIntervalValue(this.data.xyp_t01abzgzc),
|
||
avgAmount: this.parseIntervalValue(this.data.xyp_t01adzgzc)
|
||
},
|
||
{
|
||
period: '最近360天',
|
||
totalAmount: this.parseIntervalValue(this.data.xyp_t01achzzc),
|
||
maxAmount: this.parseIntervalValue(this.data.xyp_t01aazhzz),
|
||
minAmount: 0, // 没有对应的最小值字段
|
||
avgAmount: this.parseIntervalValue(this.data.xyp_t01adhzbc)
|
||
}
|
||
]
|
||
|
||
const maxTotal = Math.max(...amounts.map(a => a.totalAmount)) || 1
|
||
return amounts.map(amount => ({
|
||
...amount,
|
||
percentage: (amount.totalAmount / maxTotal) * 100
|
||
}))
|
||
},
|
||
|
||
riskEventCounts() {
|
||
const counts = { high: 0, medium: 0, low: 0 }
|
||
this.riskTimeline.forEach(event => {
|
||
if (event.riskLevel === 'high-risk') counts.high++
|
||
else if (event.riskLevel === 'medium-risk') counts.medium++
|
||
else counts.low++
|
||
})
|
||
return counts
|
||
},
|
||
|
||
// 当前选中的金额数据
|
||
currentAmountData() {
|
||
return this.newInstitutionAmounts.find(item => item.period === this.activeAmountPeriod) || this.newInstitutionAmounts[0]
|
||
}
|
||
},
|
||
|
||
mounted() {
|
||
this.initAmountChart()
|
||
this.initCountChart()
|
||
window.addEventListener('resize', this.handleResize)
|
||
},
|
||
|
||
beforeUnmount() {
|
||
if (this.amountChartInstance) {
|
||
this.amountChartInstance.dispose()
|
||
this.amountChartInstance = null
|
||
}
|
||
if (this.countChartInstance) {
|
||
this.countChartInstance.dispose()
|
||
this.countChartInstance = null
|
||
}
|
||
window.removeEventListener('resize', this.handleResize)
|
||
},
|
||
|
||
watch: {
|
||
amountTrendPoints() {
|
||
this.updateAmountChart()
|
||
},
|
||
countTrendData() {
|
||
this.updateCountChart()
|
||
}
|
||
},
|
||
|
||
methods: {
|
||
initAmountChart() {
|
||
if (!this.$refs.amountChartRef) return
|
||
this.amountChartInstance = echarts.init(this.$refs.amountChartRef)
|
||
this.updateAmountChart()
|
||
},
|
||
|
||
updateAmountChart() {
|
||
if (!this.amountChartInstance) return
|
||
|
||
const option = {
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
axisPointer: {
|
||
type: 'shadow'
|
||
},
|
||
formatter: (params) => {
|
||
const data = params[0]
|
||
const point = this.amountTrendPoints[data.dataIndex]
|
||
return `${data.name}<br/>金额: ${this.formatAmount(point.value)}`
|
||
}
|
||
},
|
||
grid: {
|
||
left: '3%',
|
||
right: '4%',
|
||
bottom: '15%',
|
||
top: '5%',
|
||
containLabel: true
|
||
},
|
||
xAxis: {
|
||
type: 'category',
|
||
data: this.amountTrendPoints.map(p => p.label.replace('最近', '近').replace('天', '日')),
|
||
axisLabel: {
|
||
rotate: 45,
|
||
fontSize: 12,
|
||
color: '#666'
|
||
},
|
||
axisLine: {
|
||
lineStyle: {
|
||
color: '#e0e0e0'
|
||
}
|
||
}
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
axisLabel: {
|
||
formatter: (value) => {
|
||
return this.formatAmount(value)
|
||
},
|
||
fontSize: 12,
|
||
color: '#666'
|
||
},
|
||
axisLine: {
|
||
lineStyle: {
|
||
color: '#e0e0e0'
|
||
}
|
||
},
|
||
splitLine: {
|
||
lineStyle: {
|
||
color: '#f0f0f0'
|
||
}
|
||
}
|
||
},
|
||
series: [
|
||
{
|
||
name: '交易金额',
|
||
type: 'bar',
|
||
data: this.amountTrendPoints.map(p => p.value),
|
||
barWidth: '25%',
|
||
barMinHeight: 2,
|
||
itemStyle: {
|
||
color: '#10b981',
|
||
borderRadius: [4, 4, 0, 0]
|
||
},
|
||
label: {
|
||
show: true,
|
||
position: 'top',
|
||
formatter: (params) => {
|
||
return this.formatAmount(params.value)
|
||
},
|
||
fontSize: 11,
|
||
color: '#333'
|
||
}
|
||
}
|
||
]
|
||
}
|
||
|
||
this.amountChartInstance.setOption(option)
|
||
},
|
||
|
||
initCountChart() {
|
||
if (!this.$refs.countChartRef) return
|
||
this.countChartInstance = echarts.init(this.$refs.countChartRef)
|
||
this.updateCountChart()
|
||
},
|
||
|
||
updateCountChart() {
|
||
if (!this.countChartInstance) return
|
||
|
||
const option = {
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
axisPointer: {
|
||
type: 'shadow'
|
||
},
|
||
formatter: (params) => {
|
||
const data = params[0]
|
||
const item = this.countTrendData[data.dataIndex]
|
||
return `${data.name}<br/>笔数: ${item.value}笔`
|
||
}
|
||
},
|
||
grid: {
|
||
left: '3%',
|
||
right: '4%',
|
||
bottom: '15%',
|
||
top: '5%',
|
||
containLabel: true
|
||
},
|
||
xAxis: {
|
||
type: 'category',
|
||
data: this.countTrendData.map(d => d.label),
|
||
axisLabel: {
|
||
rotate: 45,
|
||
fontSize: 12,
|
||
color: '#666'
|
||
},
|
||
axisLine: {
|
||
lineStyle: {
|
||
color: '#e0e0e0'
|
||
}
|
||
}
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
axisLabel: {
|
||
formatter: '{value}笔',
|
||
fontSize: 12,
|
||
color: '#666'
|
||
},
|
||
axisLine: {
|
||
lineStyle: {
|
||
color: '#e0e0e0'
|
||
}
|
||
},
|
||
splitLine: {
|
||
lineStyle: {
|
||
color: '#f0f0f0'
|
||
}
|
||
}
|
||
},
|
||
series: [
|
||
{
|
||
name: '交易笔数',
|
||
type: 'bar',
|
||
data: this.countTrendData.map(d => d.value),
|
||
barWidth: '25%',
|
||
barMinHeight: 2,
|
||
itemStyle: {
|
||
color: '#10b981',
|
||
borderRadius: [4, 4, 0, 0]
|
||
},
|
||
label: {
|
||
show: true,
|
||
position: 'top',
|
||
formatter: (params) => {
|
||
return `${params.value}笔`
|
||
},
|
||
fontSize: 11,
|
||
color: '#333'
|
||
}
|
||
}
|
||
]
|
||
}
|
||
|
||
this.countChartInstance.setOption(option)
|
||
},
|
||
|
||
handleResize() {
|
||
if (this.amountChartInstance) {
|
||
this.amountChartInstance.resize()
|
||
}
|
||
if (this.countChartInstance) {
|
||
this.countChartInstance.resize()
|
||
}
|
||
},
|
||
|
||
parseIntervalValue(value) {
|
||
if (!value || value === '' || value === '-1') return 0
|
||
const num = parseInt(value)
|
||
if (isNaN(num)) return 0
|
||
|
||
// 根据区间映射返回大致范围的中值
|
||
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
|
||
}
|
||
},
|
||
|
||
parseNewInstitutionCount(ratioValue, totalValue) {
|
||
const ratio = parseFloat(ratioValue) || 0
|
||
const total = this.parseIntervalValue(totalValue)
|
||
return Math.round(ratio * total)
|
||
},
|
||
|
||
formatAmount(value) {
|
||
if (value === 0) return '0元'
|
||
if (value < 1000) return `${value}元`
|
||
if (value < 10000) return `${(value / 1000).toFixed(1)}千元`
|
||
return `${(value / 10000).toFixed(1)}万元`
|
||
},
|
||
|
||
|
||
getRateClass(rate) {
|
||
if (rate >= 0.8) return 'text-green-600'
|
||
if (rate >= 0.6) return 'text-yellow-600'
|
||
return 'text-red-600'
|
||
},
|
||
|
||
getRateBarClass(rate) {
|
||
if (rate >= 0.8) return 'bg-green-500'
|
||
if (rate >= 0.6) return 'bg-yellow-500'
|
||
return 'bg-red-500'
|
||
},
|
||
|
||
getRiskEventCardClass(riskLevel) {
|
||
if (riskLevel === 'high-risk') return 'bg-[#FFF0F0] border border-red-200'
|
||
if (riskLevel === 'medium-risk') return 'bg-[#FFF8E7] border border-[#F5D980]'
|
||
return 'bg-[#ECF9EF] border border-[#CAECD3]'
|
||
},
|
||
|
||
getRiskEventIcon(riskLevel) {
|
||
if (riskLevel === 'high-risk') return new URL('@/assets/images/report/gfx.png', import.meta.url).href
|
||
if (riskLevel === 'medium-risk') return new URL('@/assets/images/report/zfx.png', import.meta.url).href
|
||
return new URL('@/assets/images/report/zq.png', import.meta.url).href
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.time-trend-analysis {
|
||
background: white;
|
||
border-radius: 12px;
|
||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||
border: 1px solid #e5e7eb;
|
||
padding: 24px;
|
||
margin-bottom: 20px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.section-spacing {
|
||
height: 20px;
|
||
}
|
||
|
||
.time-trend-analysis:hover {
|
||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
/* 标签页样式 */
|
||
.loan-evaluation-tabs {}
|
||
|
||
.loan-evaluation-tabs :deep(.van-tabs__wrap) {
|
||
height: 32px !important;
|
||
background-color: transparent !important;
|
||
padding: 0 !important;
|
||
border-bottom: 1px solid #DDDDDD !important;
|
||
}
|
||
|
||
.loan-evaluation-tabs :deep(.van-tabs__nav) {
|
||
background-color: transparent !important;
|
||
gap: 0;
|
||
height: 32px !important;
|
||
}
|
||
|
||
.loan-evaluation-tabs :deep(.van-tab) {
|
||
color: #999999 !important;
|
||
font-size: 14px !important;
|
||
font-weight: 400 !important;
|
||
}
|
||
|
||
.loan-evaluation-tabs :deep(.van-tab--active) {
|
||
color: var(--van-theme-primary) !important;
|
||
background-color: unset !important;
|
||
}
|
||
|
||
.loan-evaluation-tabs :deep(.van-tabs__line) {
|
||
height: 2px !important;
|
||
border-radius: 1px !important;
|
||
}
|
||
|
||
/* 内容区域样式 */
|
||
.loan-evaluation-wrap {
|
||
@apply mx-4 my-1;
|
||
border: 1px solid #DDDDDD;
|
||
background-color: #F9F9F9;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.loan-evaluation-content {
|
||
padding: 8px 16px;
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@media (max-width: 768px) {
|
||
.time-trend-analysis {
|
||
padding: 16px;
|
||
}
|
||
|
||
.grid {
|
||
grid-template-columns: 1fr;
|
||
gap: 1rem;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
.time-trend-analysis {
|
||
padding: 12px;
|
||
}
|
||
}
|
||
|
||
.section-title {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.section-title h2 {
|
||
font-size: 1.125rem;
|
||
font-weight: 600;
|
||
color: #374151;
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
|
||
|
||
.card-header {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
/* 交易金额趋势图 */
|
||
.amount-trend-chart {
|
||
display: flex;
|
||
gap: 24px;
|
||
align-items: center;
|
||
}
|
||
|
||
.chart-container {
|
||
flex: 2;
|
||
height: 200px;
|
||
position: relative;
|
||
}
|
||
|
||
.trend-line-chart {
|
||
width: 100%;
|
||
height: 100%;
|
||
position: relative;
|
||
background: white;
|
||
border-radius: 8px;
|
||
border: 1px solid #e5e7eb;
|
||
}
|
||
|
||
.chart-grid {
|
||
position: absolute;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.grid-line {
|
||
position: absolute;
|
||
width: 100%;
|
||
height: 1px;
|
||
background: #f3f4f6;
|
||
}
|
||
|
||
.trend-points {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 100%;
|
||
padding: 20px;
|
||
}
|
||
|
||
.trend-point {
|
||
position: absolute;
|
||
transform: translate(-50%, 50%);
|
||
}
|
||
|
||
.point-dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
background: #3b82f6;
|
||
border-radius: 50%;
|
||
border: 2px solid white;
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.point-label {
|
||
position: absolute;
|
||
top: -25px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
font-size: 10px;
|
||
color: #6b7280;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.trend-line {
|
||
position: absolute;
|
||
top: 20px;
|
||
left: 20px;
|
||
right: 20px;
|
||
bottom: 20px;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.trend-legend {
|
||
flex: 1;
|
||
background: white;
|
||
border-radius: 8px;
|
||
padding: 16px;
|
||
border: 1px solid #e5e7eb;
|
||
}
|
||
|
||
.legend-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.legend-dot {
|
||
width: 12px;
|
||
height: 12px;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.amount-dot {
|
||
background: #3b82f6;
|
||
}
|
||
|
||
.legend-text {
|
||
font-size: 14px;
|
||
color: #374151;
|
||
}
|
||
|
||
.trend-stats>*+* {
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.stat-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 12px;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* 交易笔数趋势图 */
|
||
.count-trend-chart {
|
||
display: flex;
|
||
gap: 24px;
|
||
align-items: center;
|
||
}
|
||
|
||
.bar-chart {
|
||
flex: 2;
|
||
}
|
||
|
||
.chart-bars {
|
||
display: flex;
|
||
gap: 16px;
|
||
align-items: end;
|
||
height: 120px;
|
||
padding: 0 20px;
|
||
}
|
||
|
||
.trend-bar {
|
||
flex: 1;
|
||
border-radius: 4px 4px 0 0;
|
||
position: relative;
|
||
min-height: 8px;
|
||
transition: height 0.3s ease;
|
||
}
|
||
|
||
.bar-value {
|
||
position: absolute;
|
||
top: -20px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: #374151;
|
||
}
|
||
|
||
.chart-labels {
|
||
display: flex;
|
||
gap: 16px;
|
||
margin-top: 8px;
|
||
padding: 0 20px;
|
||
}
|
||
|
||
.chart-label {
|
||
flex: 1;
|
||
text-align: center;
|
||
font-size: 12px;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.count-analysis {
|
||
flex: 1;
|
||
background: white;
|
||
border-radius: 8px;
|
||
padding: 16px;
|
||
border: 1px solid #e5e7eb;
|
||
}
|
||
|
||
.analysis-item {
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.analysis-icon {
|
||
width: 32px;
|
||
height: 32px;
|
||
background: #dbeafe;
|
||
color: #2563eb;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 14px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.analysis-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.analysis-title {
|
||
font-weight: 600;
|
||
color: #374151;
|
||
}
|
||
|
||
.analysis-desc {
|
||
font-size: 12px;
|
||
color: #6b7280;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
/* 还款成功率趋势 */
|
||
.success-rate-trend {
|
||
background: white;
|
||
border-radius: 8px;
|
||
padding: 16px;
|
||
border: 1px solid #e5e7eb;
|
||
}
|
||
|
||
.rate-items>*+* {
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.rate-item {
|
||
padding: 16px;
|
||
background: #f9fafb;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.rate-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.rate-period {
|
||
font-weight: 600;
|
||
color: #374151;
|
||
}
|
||
|
||
.rate-percentage {
|
||
font-weight: 600;
|
||
}
|
||
|
||
.rate-bar {
|
||
height: 8px;
|
||
background: #e5e7eb;
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.rate-fill {
|
||
height: 100%;
|
||
border-radius: 4px;
|
||
transition: width 0.3s ease;
|
||
}
|
||
|
||
.rate-details {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
font-size: 12px;
|
||
color: #6b7280;
|
||
}
|
||
|
||
/* 机构数量变化趋势 */
|
||
.institution-trend {
|
||
background: white;
|
||
border-radius: 8px;
|
||
padding: 16px;
|
||
border: 1px solid #e5e7eb;
|
||
}
|
||
|
||
.trend-comparison>*+* {
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.comparison-item {
|
||
padding: 16px;
|
||
background: #f9fafb;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.item-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.item-period {
|
||
font-weight: 600;
|
||
color: #374151;
|
||
}
|
||
|
||
.item-indicators {
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
|
||
.indicator {
|
||
font-size: 12px;
|
||
padding: 2px 8px;
|
||
border-radius: 12px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.indicator.total {
|
||
background: #dbeafe;
|
||
color: #2563eb;
|
||
}
|
||
|
||
.indicator.new {
|
||
background: #dcfce7;
|
||
color: #16a34a;
|
||
}
|
||
|
||
.item-visual {
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.visual-bar {
|
||
height: 8px;
|
||
background: #e5e7eb;
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
display: flex;
|
||
}
|
||
|
||
.bar-segment {
|
||
height: 100%;
|
||
transition: width 0.3s ease;
|
||
}
|
||
|
||
.total-segment {
|
||
background: #3b82f6;
|
||
}
|
||
|
||
.new-segment {
|
||
background: #10b981;
|
||
}
|
||
|
||
.item-trend {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.trend-up {
|
||
color: #16a34a;
|
||
}
|
||
|
||
.trend-down {
|
||
color: #dc2626;
|
||
}
|
||
|
||
.trend-stable {
|
||
color: #6b7280;
|
||
}
|
||
|
||
/* 风险事件时间线 */
|
||
.risk-timeline {
|
||
background: white;
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
border: 1px solid #e5e7eb;
|
||
}
|
||
|
||
.timeline-container {
|
||
position: relative;
|
||
height: 200px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.timeline-axis {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 0;
|
||
right: 0;
|
||
height: 2px;
|
||
background: #e5e7eb;
|
||
transform: translateY(-50%);
|
||
}
|
||
|
||
.timeline-events {
|
||
position: relative;
|
||
height: 100%;
|
||
}
|
||
|
||
.timeline-event {
|
||
position: absolute;
|
||
top: 20px;
|
||
transform: translateX(-50%);
|
||
width: 120px;
|
||
}
|
||
|
||
.timeline-event.high-risk .event-marker {
|
||
background: #fee2e2;
|
||
color: #dc2626;
|
||
border-color: #dc2626;
|
||
}
|
||
|
||
.timeline-event.medium-risk .event-marker {
|
||
background: #fef3c7;
|
||
color: #d97706;
|
||
border-color: #d97706;
|
||
}
|
||
|
||
.timeline-event.low-risk .event-marker {
|
||
background: #dcfce7;
|
||
color: #16a34a;
|
||
border-color: #16a34a;
|
||
}
|
||
|
||
.event-marker {
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 50%;
|
||
border: 2px solid;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 12px;
|
||
margin: 0 auto 8px;
|
||
position: relative;
|
||
z-index: 2;
|
||
}
|
||
|
||
.event-content {
|
||
background: white;
|
||
border-radius: 6px;
|
||
padding: 8px;
|
||
border: 1px solid #e5e7eb;
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||
text-align: center;
|
||
}
|
||
|
||
.event-title {
|
||
font-weight: 600;
|
||
font-size: 12px;
|
||
color: #374151;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.event-desc {
|
||
font-size: 10px;
|
||
color: #6b7280;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.event-time {
|
||
font-size: 10px;
|
||
color: #9ca3af;
|
||
}
|
||
|
||
.timeline-summary {
|
||
border-top: 1px solid #e5e7eb;
|
||
padding-top: 16px;
|
||
}
|
||
|
||
.summary-stats {
|
||
display: flex;
|
||
gap: 24px;
|
||
justify-content: center;
|
||
}
|
||
|
||
.summary-stat {
|
||
text-align: center;
|
||
}
|
||
|
||
.stat-number {
|
||
display: block;
|
||
font-size: 20px;
|
||
font-weight: bold;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.summary-stat.high-risk .stat-number {
|
||
color: #dc2626;
|
||
}
|
||
|
||
.summary-stat.medium-risk .stat-number {
|
||
color: #d97706;
|
||
}
|
||
|
||
.summary-stat.low-risk .stat-number {
|
||
color: #16a34a;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 12px;
|
||
color: #6b7280;
|
||
}
|
||
|
||
/* 新增机构金额分析 */
|
||
.new-institution-amounts {
|
||
background: white;
|
||
border-radius: 8px;
|
||
padding: 16px;
|
||
border: 1px solid #e5e7eb;
|
||
}
|
||
|
||
.amount-comparison {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||
gap: 16px;
|
||
}
|
||
|
||
.amount-item {
|
||
padding: 16px;
|
||
background: #f9fafb;
|
||
border-radius: 8px;
|
||
border: 1px solid #e5e7eb;
|
||
}
|
||
|
||
.amount-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.amount-period {
|
||
font-weight: 600;
|
||
color: #374151;
|
||
}
|
||
|
||
.amount-total {
|
||
font-weight: 600;
|
||
color: #059669;
|
||
}
|
||
|
||
.amount-breakdown {
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.breakdown-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.breakdown-label {
|
||
font-size: 12px;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.breakdown-value {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: #374151;
|
||
}
|
||
|
||
.amount-progress {
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.progress-bar {
|
||
height: 6px;
|
||
background: #e5e7eb;
|
||
border-radius: 3px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.progress-fill {
|
||
height: 100%;
|
||
background: linear-gradient(90deg, #10b981, #059669);
|
||
border-radius: 3px;
|
||
transition: width 0.3s ease;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
|
||
.amount-trend-chart,
|
||
.count-trend-chart {
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
}
|
||
|
||
.chart-container,
|
||
.bar-chart {
|
||
width: 100%;
|
||
}
|
||
}
|
||
</style>
|