Files
tydata-webview-v2/src/ui/CJRZQ5E9F/components/TimeTrendAnalysis.vue
2025-10-28 12:12:48 +08:00

1467 lines
36 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="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>