Files
report_viewer/src/ui/CJRZQ0A03.vue
2025-12-18 15:39:43 +08:00

1308 lines
45 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.

<script setup>
import * as echarts from 'echarts' // 引入 ECharts
import LTable from '@/components/LTable.vue'
import LTitle from '@/components/LTitle.vue'
import { useRiskNotifier } from '@/composables/useRiskNotifier'
const props = defineProps({
data: {
type: Object,
required: true,
},
apiId: {
type: String,
default: '',
},
index: {
type: Number,
default: 0,
},
notifyRiskStatus: {
type: Function,
default: () => { },
},
})
const { data } = props
// 图表相关
const chartInstance = ref(null) // ECharts 实例
const chartRef = ref(null) // 图表容器 DOM
const orgPieChartRef = ref(null) // 机构类型饼图容器
const orgPieChartInstance = ref(null) // 机构类型饼图实例
const chartData = ref({})
// 颜色配置
const opts = ref({
color: ['#4B96FF', '#3CA272', '#EE6666', '#FAC858', '#73C0DE', '#B280E9', '#FF8463'],
idColor: '#4B96FF', // 身份证数据颜色
cellColor: '#3CA272', // 手机号数据颜色
})
// 表格数据
const tableData = ref({
id: [],
cell: [],
})
const dateTableData = ref({
id: [],
cell: [],
})
const totalStatsData = ref({
id: {
totalApplyCount: [],
totalOrgCount: [],
},
cell: {
totalApplyCount: [],
totalOrgCount: [],
},
})
// 按钮选项
const timeOptions = ref([
{ label: '近7日', value: 0 },
{ label: '近1月', value: 1 },
{ label: '近3月', value: 2 },
{ label: '近6月', value: 3 },
{ label: '近1年', value: 4 },
])
const dataSourceOptions = ref([
{ label: '身份证匹配数据', value: 'id' },
{ label: '手机号匹配数据', value: 'cell' },
])
const selectedTimeOption = ref(0)
const dateSelectedOption = ref(0)
const selectedDataSource = ref('id')
const selectedStatType = ref('apply') // 'apply' 或 'org'
const statTabOptions = ref([
{ label: '申请次数', value: 'apply' },
{ label: '申请机构数', value: 'org' },
])
// 时间维度映射
const timeDimensions = {
d7: '近7日',
m1: '近1月',
m3: '近3月',
m6: '近6月',
m12: '近1年',
}
// 机构类型映射,更加用户友好的描述
const orgMappings = {
bank: '银行借贷',
mc: '小额贷款',
cf: '消费分期',
ca: '现金分期',
rel: '信用卡相关',
af: '汽车金融',
other: '其他借贷',
}
// 详细机构类型分组
const tableGroup = {
bank: [{ code: 'bank', name: '银行借贷机构' }],
mc: [
{ code: 'nbank_mc', name: '小贷机构' },
{ code: 'nbank_nsloan', name: '持牌网络小贷' },
{ code: 'nbank_sloan', name: '持牌小贷机构' },
{ code: 'pdl', name: '线上小额现金贷' },
],
cf: [
{ code: 'nbank_cf', name: '消费类分期机构' },
{ code: 'coon', name: '线上消费分期' },
{ code: 'cooff', name: '线下消费分期' },
],
ca: [
{ code: 'nbank_ca', name: '现金类分期机构' },
{ code: 'caon', name: '线上现金分期' },
{ code: 'caoff', name: '线下现金分期' },
{ code: 'nbank_com', name: '代偿类分期机构' },
],
rel: [{ code: 'rel', name: '信用卡(类信用卡)' }],
af: [
{ code: 'af', name: '汽车金融' },
{ code: 'nbank_autofin', name: '持牌汽车金融机构' },
],
other: [
{ code: 'nbank_p2p', name: '改制机构' },
{ code: 'nbank_cons', name: '持牌消费金融机构' },
{ code: 'nbank_finlea', name: '持牌融资租赁机构' },
{ code: 'nbank_oth', name: '其他非银借贷类型申请' },
{ code: 'nbank_else', name: '其他非银类型申请' },
{ code: 'oth', name: '其他银行借贷类型申请' },
{ code: 'else', name: '其他银行类型申请' },
],
}
// 特殊时段类型
const dateGroup = {
week: [
{ code: 'bank_week', name: '周末银行' },
{ code: 'nbank_week', name: '周末非银' },
],
night: [
{ code: 'bank_night', name: '夜间银行' },
{ code: 'nbank_night', name: '夜间非银' },
],
}
// 初始化图表
function initChart() {
if (!chartRef.value) return
if (!chartInstance.value) {
chartInstance.value = echarts.init(chartRef.value)
}
const option = {
color: opts.value.color,
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
shadowStyle: {
color: 'rgba(0,0,0,0.05)',
},
},
textStyle: {
fontSize: 12,
},
backgroundColor: 'rgba(50,50,50,0.9)',
borderRadius: 4,
shadowColor: 'rgba(0,0,0,0.3)',
shadowBlur: 10,
},
grid: {
left: '3%',
right: '4%',
bottom: '10%',
containLabel: true,
},
xAxis: {
type: 'category',
data: chartData.value.categories || [],
axisLabel: {
interval: 0,
rotate: 0,
fontSize: 12,
color: '#666',
},
axisLine: {
lineStyle: {
color: '#ddd',
},
},
},
yAxis: {
type: 'value',
name: '数量',
nameTextStyle: {
color: '#666',
fontSize: 12,
},
splitLine: {
lineStyle: {
type: 'dashed',
color: '#eee',
},
},
},
legend: {
data: chartData.value.series?.map(item => item.name) || [],
bottom: '0%',
textStyle: {
fontSize: 12,
},
selectedMode: true,
},
series: (chartData.value.series || []).map(item => ({
name: item.name,
type: 'bar',
data: item.data || [],
barMaxWidth: 50,
barGap: '30%',
barMinHeight: 3,
label: {
show: true,
position: 'top',
formatter: '{c}',
fontSize: 12,
fontWeight: 'bold',
},
itemStyle: {
borderRadius: [3, 3, 0, 0],
shadowColor: 'rgba(0,0,0,0.2)',
shadowBlur: 8,
shadowOffsetX: 2,
shadowOffsetY: 2,
},
})),
}
chartInstance.value.setOption(option)
}
// 初始化机构类型饼图
function initOrgPieChart() {
if (!orgPieChartRef.value) return
if (!orgPieChartInstance.value) {
orgPieChartInstance.value = echarts.init(orgPieChartRef.value)
}
// 从当前显示的tableData中获取数据
const currentData = transformedTableData.value?.[selectedDataSource.value]?.[selectedStatType.value] || []
if (!currentData || currentData.length === 0) {
// 如果没有数据,设置一个空的饼图
const option = {
title: {
text: '暂无数据',
left: 'center',
top: 'center',
},
series: [
{
type: 'pie',
radius: ['40%', '70%'],
data: [],
},
],
}
orgPieChartInstance.value.setOption(option)
return
}
const pieData = currentData.map(item => ({
name: item.label,
value: item.value,
}))
const option = {
color: opts.value.color,
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)',
},
legend: {
orient: 'vertical',
right: 0,
top: 'center',
data: pieData.map(item => item.name),
},
series: [
{
name: selectedStatType.value === 'apply' ? '申请次数' : '申请机构数',
type: 'pie',
radius: ['35%', '60%'], // 缩小饼图尺寸
avoidLabelOverlap: false,
label: {
show: false,
position: 'center',
},
emphasis: {
label: {
show: true,
fontSize: '14',
fontWeight: 'bold',
},
},
labelLine: {
show: false,
},
data: pieData,
center: ['32%', '50%'], // 将饼图位置向左移动
itemStyle: {
borderRadius: 8, // 添加圆角
borderWidth: 2, // 添加边框
borderColor: '#fff', // 边框颜色
shadowBlur: 10, // 添加阴影
shadowColor: 'rgba(0, 0, 0, 0.2)', // 阴影颜色
},
// 添加3D效果
// roseType: 'radius',
zlevel: 1,
},
],
}
orgPieChartInstance.value.setOption(option)
}
// 将表格数据转换为按时间维度展示的格式
const transformedTableData = computed(() => {
const result = {
id: {
// 申请次数数据
apply: [],
// 机构数数据
org: [],
},
cell: {
apply: [],
org: [],
},
}
// 遍历所有机构类型
Object.keys(orgMappings).forEach(orgType => {
// ID数据 - 申请次数
const idApplyItem = {
label: orgMappings[orgType],
d7: tableData.value?.id?.[0]?.find(i => i.name === orgType)?.totalApplyCount || 0,
m1: tableData.value?.id?.[1]?.find(i => i.name === orgType)?.totalApplyCount || 0,
m3: tableData.value?.id?.[2]?.find(i => i.name === orgType)?.totalApplyCount || 0,
m6: tableData.value?.id?.[3]?.find(i => i.name === orgType)?.totalApplyCount || 0,
m12: tableData.value?.id?.[4]?.find(i => i.name === orgType)?.totalApplyCount || 0,
value: tableData.value?.id?.[selectedTimeOption.value]?.find(i => i.name === orgType)?.totalApplyCount || 0,
}
result.id.apply.push(idApplyItem)
// ID数据 - 机构数
const idOrgItem = {
label: orgMappings[orgType],
d7: tableData.value?.id?.[0]?.find(i => i.name === orgType)?.totalOrgCount || 0,
m1: tableData.value?.id?.[1]?.find(i => i.name === orgType)?.totalOrgCount || 0,
m3: tableData.value?.id?.[2]?.find(i => i.name === orgType)?.totalOrgCount || 0,
m6: tableData.value?.id?.[3]?.find(i => i.name === orgType)?.totalOrgCount || 0,
m12: tableData.value?.id?.[4]?.find(i => i.name === orgType)?.totalOrgCount || 0,
value: tableData.value?.id?.[selectedTimeOption.value]?.find(i => i.name === orgType)?.totalOrgCount || 0,
}
result.id.org.push(idOrgItem)
// Cell数据 - 申请次数
const cellApplyItem = {
label: orgMappings[orgType],
d7: tableData.value?.cell?.[0]?.find(i => i.name === orgType)?.totalApplyCount || 0,
m1: tableData.value?.cell?.[1]?.find(i => i.name === orgType)?.totalApplyCount || 0,
m3: tableData.value?.cell?.[2]?.find(i => i.name === orgType)?.totalApplyCount || 0,
m6: tableData.value?.cell?.[3]?.find(i => i.name === orgType)?.totalApplyCount || 0,
m12: tableData.value?.cell?.[4]?.find(i => i.name === orgType)?.totalApplyCount || 0,
value: tableData.value?.cell?.[selectedTimeOption.value]?.find(i => i.name === orgType)?.totalApplyCount || 0,
}
result.cell.apply.push(cellApplyItem)
// Cell数据 - 机构数
const cellOrgItem = {
label: orgMappings[orgType],
d7: tableData.value?.cell?.[0]?.find(i => i.name === orgType)?.totalOrgCount || 0,
m1: tableData.value?.cell?.[1]?.find(i => i.name === orgType)?.totalOrgCount || 0,
m3: tableData.value?.cell?.[2]?.find(i => i.name === orgType)?.totalOrgCount || 0,
m6: tableData.value?.cell?.[3]?.find(i => i.name === orgType)?.totalOrgCount || 0,
m12: tableData.value?.cell?.[4]?.find(i => i.name === orgType)?.totalOrgCount || 0,
value: tableData.value?.cell?.[selectedTimeOption.value]?.find(i => i.name === orgType)?.totalOrgCount || 0,
}
result.cell.org.push(cellOrgItem)
})
return result
})
// 特殊时段申请数据转换
const transformedDateTableData = computed(() => {
const result = {
id: {
apply: [],
org: [],
},
cell: {
apply: [],
org: [],
},
}
// 特殊时段名称映射
const dateTypeMappings = {
week: '周末申请',
night: '夜间申请',
}
// 遍历所有特殊时段类型
Object.keys(dateTypeMappings).forEach(dateType => {
// ID数据 - 申请次数
const idApplyItem = {
label: dateTypeMappings[dateType],
d7: dateTableData.value?.id?.[0]?.find(i => i.name === dateType)?.totalApplyCount || 0,
m1: dateTableData.value?.id?.[1]?.find(i => i.name === dateType)?.totalApplyCount || 0,
m3: dateTableData.value?.id?.[2]?.find(i => i.name === dateType)?.totalApplyCount || 0,
m6: dateTableData.value?.id?.[3]?.find(i => i.name === dateType)?.totalApplyCount || 0,
m12: dateTableData.value?.id?.[4]?.find(i => i.name === dateType)?.totalApplyCount || 0,
}
result.id.apply.push(idApplyItem)
// ID数据 - 机构数
const idOrgItem = {
label: dateTypeMappings[dateType],
d7: dateTableData.value?.id?.[0]?.find(i => i.name === dateType)?.totalOrgCount || 0,
m1: dateTableData.value?.id?.[1]?.find(i => i.name === dateType)?.totalOrgCount || 0,
m3: dateTableData.value?.id?.[2]?.find(i => i.name === dateType)?.totalOrgCount || 0,
m6: dateTableData.value?.id?.[3]?.find(i => i.name === dateType)?.totalOrgCount || 0,
m12: dateTableData.value?.id?.[4]?.find(i => i.name === dateType)?.totalOrgCount || 0,
}
result.id.org.push(idOrgItem)
// Cell数据 - 申请次数
const cellApplyItem = {
label: dateTypeMappings[dateType],
d7: dateTableData.value?.cell?.[0]?.find(i => i.name === dateType)?.totalApplyCount || 0,
m1: dateTableData.value?.cell?.[1]?.find(i => i.name === dateType)?.totalApplyCount || 0,
m3: dateTableData.value?.cell?.[2]?.find(i => i.name === dateType)?.totalApplyCount || 0,
m6: dateTableData.value?.cell?.[3]?.find(i => i.name === dateType)?.totalApplyCount || 0,
m12: dateTableData.value?.cell?.[4]?.find(i => i.name === dateType)?.totalApplyCount || 0,
}
result.cell.apply.push(cellApplyItem)
// Cell数据 - 机构数
const cellOrgItem = {
label: dateTypeMappings[dateType],
d7: dateTableData.value?.cell?.[0]?.find(i => i.name === dateType)?.totalOrgCount || 0,
m1: dateTableData.value?.cell?.[1]?.find(i => i.name === dateType)?.totalOrgCount || 0,
m3: dateTableData.value?.cell?.[2]?.find(i => i.name === dateType)?.totalOrgCount || 0,
m6: dateTableData.value?.cell?.[3]?.find(i => i.name === dateType)?.totalOrgCount || 0,
m12: dateTableData.value?.cell?.[4]?.find(i => i.name === dateType)?.totalOrgCount || 0,
}
result.cell.org.push(cellOrgItem)
})
// 添加首次和最近申请数据
if (dateTableData.value?.id?.[4]?.some(i => i.name === 'first_apply')) {
const firstApply = dateTableData.value.id[4].find(i => i.name === 'first_apply')
const lastApply = dateTableData.value.id[4].find(i => i.name === 'last_apply')
result.id.apply.push({
label: '最早非银申请',
m12: firstApply?.totalApplyCount || '-',
specialDisplay: true,
})
result.id.apply.push({
label: '最近非银申请',
m12: lastApply?.totalApplyCount || '-',
specialDisplay: true,
})
result.id.org.push({
label: '最早非银申请',
m12: '-',
specialDisplay: true,
})
result.id.org.push({
label: '最近非银申请',
m12: '-',
specialDisplay: true,
})
}
if (dateTableData.value?.cell?.[4]?.some(i => i.name === 'first_apply')) {
const firstApply = dateTableData.value.cell[4].find(i => i.name === 'first_apply')
const lastApply = dateTableData.value.cell[4].find(i => i.name === 'last_apply')
result.cell.apply.push({
label: '最早非银申请',
m12: firstApply?.totalApplyCount || '-',
specialDisplay: true,
})
result.cell.apply.push({
label: '最近非银申请',
m12: lastApply?.totalApplyCount || '-',
specialDisplay: true,
})
result.cell.org.push({
label: '最早非银申请',
m12: '-',
specialDisplay: true,
})
result.cell.org.push({
label: '最近非银申请',
m12: '-',
specialDisplay: true,
})
}
return result
})
// 更新图表
function updateChart() {
if (chartInstance.value) {
initChart()
}
if (orgPieChartInstance.value) {
initOrgPieChart()
}
}
// 计算总体统计数据
function calculateTotalStats(data) {
// 获取时间维度列表
const timeKeys = Object.keys(timeDimensions)
// 初始化结果对象
const result = {
id: {
totalApplyCount: [],
totalOrgCount: [],
},
cell: {
totalApplyCount: [],
totalOrgCount: [],
},
}
// 机构类型
const orgTypes = ['bank', 'nbank']
// 遍历每个时间维度
for (const timeKey of timeKeys) {
// ID数据
let idTotalApplyCount = 0
let idTotalOrgCount = 0
// Cell数据
let cellTotalApplyCount = 0
let cellTotalOrgCount = 0
// 遍历每种机构类型并累加
orgTypes.forEach(orgType => {
// ID数据统计
const idApplyCountKey = `als_${timeKey}_id_${orgType}_allnum`
const idOrgCountKey = `als_${timeKey}_id_${orgType}_orgnum`
if (data?.[idApplyCountKey] !== undefined && data?.[idOrgCountKey] !== undefined) {
idTotalApplyCount += Number(data?.[idApplyCountKey] || 0)
idTotalOrgCount += Number(data?.[idOrgCountKey] || 0)
}
// Cell数据统计
const cellApplyCountKey = `als_${timeKey}_cell_${orgType}_allnum`
const cellOrgCountKey = `als_${timeKey}_cell_${orgType}_orgnum`
if (data?.[cellApplyCountKey] !== undefined && data?.[cellOrgCountKey] !== undefined) {
cellTotalApplyCount += Number(data?.[cellApplyCountKey] || 0)
cellTotalOrgCount += Number(data?.[cellOrgCountKey] || 0)
}
})
// 添加到结果对象
result.id.totalApplyCount.push(idTotalApplyCount)
result.id.totalOrgCount.push(idTotalOrgCount)
result.cell.totalApplyCount.push(cellTotalApplyCount)
result.cell.totalOrgCount.push(cellTotalOrgCount)
}
return result
}
// 计算各借贷类型统计数据
function typeTotalStats(data) {
// 时间维度列表
const timeKeys = Object.keys(timeDimensions)
// 初始化结果数组 - 按数据来源分组
const result = {
id: [],
cell: [],
}
// 遍历每个时间维度
for (const timeKey of timeKeys) {
const idTableDataEntry = []
const cellTableDataEntry = []
// 遍历每种借贷类型并累加
Object.keys(orgMappings).forEach(groupOrgType => {
const orgTypeArray = tableGroup[groupOrgType]
// ID数据统计
let idTotalApplyCount = 0
let idTotalOrgCount = 0
// Cell数据统计
let cellTotalApplyCount = 0
let cellTotalOrgCount = 0
for (const i of orgTypeArray) {
// ID数据统计
const idApplyCountKey = `als_${timeKey}_id_${i.code}_allnum`
const idOrgCountKey = `als_${timeKey}_id_${i.code}_orgnum`
idTotalApplyCount += Number(data?.[idApplyCountKey] || 0)
idTotalOrgCount += Number(data?.[idOrgCountKey] || 0)
// Cell数据统计
const cellApplyCountKey = `als_${timeKey}_cell_${i.code}_allnum`
const cellOrgCountKey = `als_${timeKey}_cell_${i.code}_orgnum`
cellTotalApplyCount += Number(data?.[cellApplyCountKey] || 0)
cellTotalOrgCount += Number(data?.[cellOrgCountKey] || 0)
}
// 添加到ID表格数据
idTableDataEntry.push({
label: orgMappings[groupOrgType],
name: groupOrgType,
totalApplyCount: idTotalApplyCount,
totalOrgCount: idTotalOrgCount,
})
// 添加到Cell表格数据
cellTableDataEntry.push({
label: orgMappings[groupOrgType],
name: groupOrgType,
totalApplyCount: cellTotalApplyCount,
totalOrgCount: cellTotalOrgCount,
})
})
// 添加到结果
result.id.push(idTableDataEntry)
result.cell.push(cellTableDataEntry)
}
return result
}
// 计算特殊时段统计数据
function dateTotalStats(data) {
// 时间维度列表
const timeKeys = Object.keys(timeDimensions)
// 特殊时段名称映射
const dateTypeMappings = {
week: '周末申请',
night: '夜间申请 (晚8点至次日早8点)',
}
// 初始化结果数组 - 按数据来源分组
const result = {
id: [],
cell: [],
}
// 遍历每个时间维度
for (const timeKey of timeKeys) {
const idTableDataEntry = []
const cellTableDataEntry = []
// 遍历每种特殊时段类型
Object.keys(dateTypeMappings).forEach(dateType => {
const typeArray = dateGroup[dateType]
// ID数据统计
let idTotalApplyCount = 0
let idTotalOrgCount = 0
// Cell数据统计
let cellTotalApplyCount = 0
let cellTotalOrgCount = 0
for (const i of typeArray) {
// ID数据统计
const idApplyCountKey = `als_${timeKey}_id_${i.code}_allnum`
const idOrgCountKey = `als_${timeKey}_id_${i.code}_orgnum`
idTotalApplyCount += Number(data?.[idApplyCountKey] || 0)
idTotalOrgCount += Number(data?.[idOrgCountKey] || 0)
// Cell数据统计
const cellApplyCountKey = `als_${timeKey}_cell_${i.code}_allnum`
const cellOrgCountKey = `als_${timeKey}_cell_${i.code}_orgnum`
cellTotalApplyCount += Number(data?.[cellApplyCountKey] || 0)
cellTotalOrgCount += Number(data?.[cellOrgCountKey] || 0)
}
// 添加到ID表格数据
idTableDataEntry.push({
label: dateTypeMappings[dateType],
name: dateType,
totalApplyCount: idTotalApplyCount,
totalOrgCount: idTotalOrgCount,
})
// 添加到Cell表格数据
cellTableDataEntry.push({
label: dateTypeMappings[dateType],
name: dateType,
totalApplyCount: cellTotalApplyCount,
totalOrgCount: cellTotalOrgCount,
})
})
// 添加最早和最近申请时间(如果存在数据)
if (timeKey === 'm12') {
// 最早申请日期(间隔天数)
const idFirstApplyKey = 'als_fst_id_nbank_inteday'
const cellFirstApplyKey = 'als_fst_cell_nbank_inteday'
// 最近申请日期(间隔天数)
const idLastApplyKey = 'als_lst_id_nbank_inteday'
const cellLastApplyKey = 'als_lst_cell_nbank_inteday'
if (data?.[idFirstApplyKey]) {
idTableDataEntry.push({
label: '最早非银借贷申请',
name: 'first_apply',
totalApplyCount: `${data[idFirstApplyKey]}天前`,
totalOrgCount: '-',
})
}
if (data?.[idLastApplyKey]) {
idTableDataEntry.push({
label: '最近非银借贷申请',
name: 'last_apply',
totalApplyCount: `${data[idLastApplyKey]}天前`,
totalOrgCount: '-',
})
}
if (data?.[cellFirstApplyKey]) {
cellTableDataEntry.push({
label: '最早非银借贷申请',
name: 'first_apply',
totalApplyCount: `${data[cellFirstApplyKey]}天前`,
totalOrgCount: '-',
})
}
if (data?.[cellLastApplyKey]) {
cellTableDataEntry.push({
label: '最近非银借贷申请',
name: 'last_apply',
totalApplyCount: `${data[cellLastApplyKey]}天前`,
totalOrgCount: '-',
})
}
}
// 添加到结果
result.id.push(idTableDataEntry)
result.cell.push(cellTableDataEntry)
}
return result
}
// 计算总结统计数据
function calculateSummaryStats(data) {
// 获取最大借贷次数和最大机构数
const maxData = {
id: {
maxApply: 0,
maxOrg: 0,
avgMonthlyApply: 0,
},
cell: {
maxApply: 0,
maxOrg: 0,
avgMonthlyApply: 0,
},
}
// 检查是否有最大月申请次数数据
if (data?.als_m12_id_max_monnum) {
maxData.id.maxApply = Number(data.als_m12_id_max_monnum)
}
if (data?.als_m12_cell_max_monnum) {
maxData.cell.maxApply = Number(data.als_m12_cell_max_monnum)
}
// 检查是否有平均月申请次数数据
if (data?.als_m12_id_avg_monnum) {
maxData.id.avgMonthlyApply = Number(data.als_m12_id_avg_monnum)
}
if (data?.als_m12_cell_avg_monnum) {
maxData.cell.avgMonthlyApply = Number(data.als_m12_cell_avg_monnum)
}
// 获取总申请月份数
if (data?.als_m12_id_nbank_tot_mons) {
maxData.id.totalMonths = Number(data.als_m12_id_nbank_tot_mons)
}
if (data?.als_m12_cell_nbank_tot_mons) {
maxData.cell.totalMonths = Number(data.als_m12_cell_nbank_tot_mons)
}
return maxData
}
// 检查某种数据源是否有数据
function hasDataSource(source) {
if (source === 'id') {
// 检查ID数据源
return totalStatsData.value?.id?.totalApplyCount?.some(count => count > 0) || false
} else if (source === 'cell') {
// 检查Cell数据源
return totalStatsData.value?.cell?.totalApplyCount?.some(count => count > 0) || false
}
return false
}
// 监听选项变化
watch(selectedTimeOption, () => {
// 更新饼图值
transformedTableData.value[selectedDataSource.value][selectedStatType.value].forEach(item => {
const timeKey = ['d7', 'm1', 'm3', 'm6', 'm12'][selectedTimeOption.value]
item.value = item[timeKey]
})
updateChart()
})
watch(selectedDataSource, () => {
updateChartData()
// 完全重新渲染图表
initChart()
})
watch(selectedStatType, updateChart)
// 窗口大小变化时重新调整图表大小
function resizeCharts() {
if (chartInstance.value) {
chartInstance.value.resize()
}
if (orgPieChartInstance.value) {
orgPieChartInstance.value.resize()
}
}
// 初始化组件
onMounted(() => {
// 计算总体统计数据
totalStatsData.value = calculateTotalStats(data)
// 计算各借贷类型统计数据
tableData.value = typeTotalStats(data)
// 计算特殊时段统计数据
dateTableData.value = dateTotalStats(data)
// 计算总结统计数据
const summaryData = calculateSummaryStats(data)
// 检查哪个数据源有数据,并自动选择
if (hasDataSource('id')) {
selectedDataSource.value = 'id'
} else if (hasDataSource('cell')) {
selectedDataSource.value = 'cell'
}
// 设置图表数据
updateChartData()
nextTick(() => {
// 初始化所有图表
initChart()
initOrgPieChart()
})
// 添加窗口大小变化监听
window.addEventListener('resize', resizeCharts)
})
// 更新图表数据
function updateChartData() {
chartData.value = {
categories: ['近7日', '近1月', '近3月', '近6月', '近1年'],
series: [
{
name: '申请次数',
data: totalStatsData.value?.[selectedDataSource.value]?.totalApplyCount || [],
},
{
name: '申请的机构数',
data: totalStatsData.value?.[selectedDataSource.value]?.totalOrgCount || [],
},
],
}
}
// 计算风险评分0-100分分数越高越安全
const riskScore = computed(() => {
// 获取近1年的申请次数
const idApplyCount = totalStatsData.value?.id?.totalApplyCount?.[4] || 0;
const cellApplyCount = totalStatsData.value?.cell?.totalApplyCount?.[4] || 0;
const totalApplyCount = Math.max(idApplyCount, cellApplyCount);
// 根据申请次数计算风险评分
// 0次100分最安全
// 1-3次80分较安全
// 4-10次60分中等风险
// 11-20次40分较高风险
// 20次以上20分高风险
if (totalApplyCount === 0) return 100;
if (totalApplyCount <= 3) return 80;
if (totalApplyCount <= 10) return 60;
if (totalApplyCount <= 20) return 40;
return 20;
});
// 使用 composable 通知父组件风险评分
useRiskNotifier(props, riskScore);
// 暴露给父组件
defineExpose({
riskScore
});
// 组件销毁时清理资源
onUnmounted(() => {
window.removeEventListener('resize', resizeCharts)
if (chartInstance.value) {
chartInstance.value.dispose()
}
if (orgPieChartInstance.value) {
orgPieChartInstance.value.dispose()
}
})
// 当数据源变化时,更新主图表数据
watch(selectedDataSource, newValue => {
updateChartData()
if (chartInstance.value) {
chartInstance.value.setOption({
xAxis: {
data: chartData.value.categories,
},
series: chartData.value.series.map(item => ({
name: item.name,
data: item.data,
})),
})
}
})
</script>
<template>
<div class="card">
<div class="flex flex-col gap-y-6">
<!-- 数据切换选项 -->
<div class="p-6 bg-white rounded-lg shadow-sm border border-gray-100 relative overflow-hidden">
<!-- 背景装饰元素 -->
<div class="absolute top-0 right-0 w-32 h-32 bg-blue-50 rounded-full -mr-8 -mt-8 opacity-60"></div>
<div class="absolute bottom-0 left-0 w-20 h-20 bg-green-50 rounded-full -ml-10 -mb-10 opacity-50"></div>
<div class="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 relative z-10">
<div class="space-y-2">
<h2 class="text-xl font-semibold text-gray-800 flex items-center">借贷申请分析报告</h2>
<p class="text-sm text-gray-600 ml-6">本报告统计借贷申请记录和机构情况帮助评估信贷风险</p>
</div>
<div class="flex flex-wrap gap-6 w-full">
<template v-if="hasDataSource('id') || hasDataSource('cell')">
<div class="flex-1 flex rounded-md shadow-sm relative">
<!-- 图标装饰 -->
<button v-if="hasDataSource('id')" type="button"
class="flex-1 py-2 px-4 text-sm font-medium rounded-l-md border transition-all duration-200 flex items-center flex-shrink-0"
:class="[
selectedDataSource === 'id'
? 'bg-gradient-to-r from-blue-500 to-blue-600 text-white border-blue-500 shadow-md'
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50',
]" @click="selectedDataSource = 'id'">
<svg v-if="selectedDataSource === 'id'" class="w-4 h-4 mr-1" fill="none" stroke="currentColor"
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<svg v-else class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10 6H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V8a2 2 0 00-2-2h-5m-4 0V5a2 2 0 114 0v1m-4 0a2 2 0 104 0m-5 8a2 2 0 100-4 2 2 0 000 4zm0 0c1.306 0 2.417.835 2.83 2M9 14a3.001 3.001 0 00-2.83 2M15 11h3m-3 4h2">
</path>
</svg>
身份证匹配
</button>
<button v-if="hasDataSource('cell')" type="button"
class="flex-1 py-2 px-4 text-sm font-medium rounded-r-md border transition-all duration-200 flex items-center flex-shrink-0"
:class="[
selectedDataSource === 'cell'
? 'bg-gradient-to-r from-green-500 to-green-600 text-white border-green-500 shadow-md'
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50',
]" @click="selectedDataSource = 'cell'">
<svg v-if="selectedDataSource === 'cell'" class="w-4 h-4 mr-1" fill="none" stroke="currentColor"
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<svg v-else class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"></path>
</svg>
手机号匹配
</button>
</div>
</template>
</div>
</div>
<!-- 数据类型说明 -->
<div class="mt-4 bg-blue-50 p-3 rounded-lg text-xs text-gray-700">
<p class="font-medium text-blue-800 mb-1">数据类型说明</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
<div class="flex items-start">
<span class="inline-block w-2 h-2 mt-1 mr-2 rounded-full flex-shrink-0 bg-blue-500"></span>
<span><strong>身份证匹配</strong>通过身份证号码匹配获取的借贷申请记录能够反映与身份证关联的所有金融机构申请行为</span>
</div>
<div class="flex items-start">
<span class="inline-block w-2 h-2 mt-1 mr-2 rounded-full flex-shrink-0 bg-green-500"></span>
<span><strong>手机号匹配</strong>通过手机号码匹配获取的借贷申请记录能够反映与手机号关联的所有金融机构申请行为</span>
</div>
</div>
</div>
</div>
<template v-if="hasDataSource(selectedDataSource)">
<!-- 借贷申请总体情况概览 -->
<div class="bg-gradient-to-br from-gray-50 to-gray-100 rounded-lg p-4 relative overflow-hidden">
<!-- 装饰元素 -->
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-blue-400 to-green-400 opacity-70"></div>
<div class="absolute bottom-4 right-4 opacity-10">
<svg xmlns="http://www.w3.org/2000/svg" class="h-32 w-32" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1"
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
<div class="grid grid-cols-2 md:grid-cols-2 lg:grid-cols-4 gap-4 relative z-10">
<div
class="bg-white rounded-lg p-3 md:p-5 shadow-sm border border-gray-100 transform transition-transform duration-300 hover:shadow-md hover:-translate-y-1">
<div class="flex items-start">
<div class="mr-2 md:mr-3 bg-blue-100 rounded-lg p-1 md:p-2 text-blue-600">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 md:h-6 md:w-6" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
</div>
<div>
<div class="text-xs md:text-sm text-gray-500">近1年总申请次数</div>
<div class="text-lg md:text-2xl font-bold mt-1 text-gray-800">
{{ totalStatsData[selectedDataSource]?.totalApplyCount[4] || 0 }}
</div>
</div>
</div>
</div>
<div
class="bg-white rounded-lg p-3 md:p-5 shadow-sm border border-gray-100 transform transition-transform duration-300 hover:shadow-md hover:-translate-y-1">
<div class="flex items-start">
<div class="mr-2 md:mr-3 bg-green-100 rounded-lg p-1 md:p-2 text-green-600">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 md:h-6 md:w-6" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
</div>
<div>
<div class="text-xs md:text-sm text-gray-500">近1年总申请机构数</div>
<div class="text-lg md:text-2xl font-bold mt-1 text-gray-800">
{{ totalStatsData[selectedDataSource]?.totalOrgCount[4] || 0 }}
</div>
</div>
</div>
</div>
<div
class="bg-white rounded-lg p-3 md:p-5 shadow-sm border border-gray-100 transform transition-transform duration-300 hover:shadow-md hover:-translate-y-1">
<div class="flex items-start">
<div class="mr-2 md:mr-3 bg-yellow-100 rounded-lg p-1 md:p-2 text-yellow-600">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 md:h-6 md:w-6" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
</svg>
</div>
<div>
<div class="text-xs md:text-sm text-gray-500">单月最高申请次数</div>
<div class="text-lg md:text-2xl font-bold mt-1 text-gray-800">
{{ data[`als_m12_${selectedDataSource}_max_monnum`] || 0 }}
</div>
</div>
</div>
</div>
<div
class="bg-white rounded-lg p-3 md:p-5 shadow-sm border border-gray-100 transform transition-transform duration-300 hover:shadow-md hover:-translate-y-1">
<div class="flex items-start">
<div class="mr-2 md:mr-3 bg-purple-100 rounded-lg p-1 md:p-2 text-purple-600">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 md:h-6 md:w-6" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
</svg>
</div>
<div>
<div class="text-xs md:text-sm text-gray-500">月均申请次数</div>
<div class="text-lg md:text-2xl font-bold mt-1 text-gray-800">
{{ data[`als_m12_${selectedDataSource}_avg_monnum`] || 0 }}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 申请次数和机构数趋势图 -->
<div>
<LTitle title="借贷申请趋势" />
<div ref="chartRef" class="chart-container"></div>
</div>
<!-- 借贷类型分析区域 -->
<div>
<LTitle title="借贷类型分析" />
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<!-- 借贷类型分布饼图 -->
<div class="relative">
<div ref="orgPieChartRef" class="chart-container-small"></div>
</div>
<!-- 借贷类型表格 -->
<div>
<div class="mb-2 flex justify-between items-center">
<LButtonGroup v-model="selectedStatType" :options="statTabOptions" />
</div>
<div class="overflow-x-auto">
<LTable :data="transformedTableData?.[selectedDataSource]?.[selectedStatType] || []"
class="w-full whitespace-nowrap">
<template #header>
<th class="border px-2 py-1 text-xs lg:px-3 lg:py-2 lg:text-sm w-[25%]">借贷类别</th>
<th class="border px-2 py-1 text-xs lg:px-3 lg:py-2 lg:text-sm w-[15%]">近7日</th>
<th class="border px-2 py-1 text-xs lg:px-3 lg:py-2 lg:text-sm w-[15%]">近1月</th>
<th class="border px-2 py-1 text-xs lg:px-3 lg:py-2 lg:text-sm w-[15%]">近3月</th>
<th class="border px-2 py-1 text-xs lg:px-3 lg:py-2 lg:text-sm w-[15%]">近6月</th>
<th class="border px-2 py-1 text-xs lg:px-3 lg:py-2 lg:text-sm w-[15%]">近1年</th>
</template>
<template #default="{ row }">
<td class="border px-2 py-1 text-xs lg:px-3 lg:py-2 lg:text-sm font-medium whitespace-nowrap">
{{ row.label }}
</td>
<td class="border px-2 py-1 text-xs lg:px-3 lg:py-2 lg:text-sm text-center">{{ row.d7 }}</td>
<td class="border px-2 py-1 text-xs lg:px-3 lg:py-2 lg:text-sm text-center">{{ row.m1 }}</td>
<td class="border px-2 py-1 text-xs lg:px-3 lg:py-2 lg:text-sm text-center">{{ row.m3 }}</td>
<td class="border px-2 py-1 text-xs lg:px-3 lg:py-2 lg:text-sm text-center">{{ row.m6 }}</td>
<td class="border px-2 py-1 text-xs lg:px-3 lg:py-2 lg:text-sm text-center">{{ row.m12 }}</td>
</template>
</LTable>
</div>
</div>
</div>
<!-- 借贷类型解释 -->
<div class="mt-4 bg-blue-50 p-3 rounded-lg text-sm">
<p class="font-medium text-blue-800 mb-1">借贷类型说明</p>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2">
<div v-for="(desc, key) in orgMappings" :key="key" class="flex items-start">
<span class="inline-block w-3 h-3 mt-1 mr-2 rounded-full flex-shrink-0"
:style="{ backgroundColor: opts.color[Object.keys(orgMappings).indexOf(key)] }"></span>
<span><strong>{{ desc }}</strong>{{key === 'bank' ? '包含各类银行贷款' : tableGroup[key].map(i =>
i.name).join('、')
}}</span>
</div>
</div>
</div>
</div>
<!-- 特殊时段申请记录 -->
<div>
<LTitle title="特殊时段申请记录" />
<div class="text-xs text-gray-500 my-2">
此表格展示在周末或夜间时段的借贷申请情况这类时段的高频申请可能需要额外关注
</div>
<div class="mb-2">
<LButtonGroup v-model="selectedStatType" :options="statTabOptions" />
</div>
<div class="overflow-x-auto">
<LTable :data="transformedDateTableData?.[selectedDataSource]?.[selectedStatType] || []"
class="w-full whitespace-nowrap">
<template #header>
<th class="border px-2 py-1 text-xs lg:px-3 lg:py-2 lg:text-sm w-[25%]">时段类型</th>
<th class="border px-2 py-1 text-xs lg:px-3 lg:py-2 lg:text-sm w-[15%]">近7日</th>
<th class="border px-2 py-1 text-xs lg:px-3 lg:py-2 lg:text-sm w-[15%]">近1月</th>
<th class="border px-2 py-1 text-xs lg:px-3 lg:py-2 lg:text-sm w-[15%]">近3月</th>
<th class="border px-2 py-1 text-xs lg:px-3 lg:py-2 lg:text-sm w-[15%]">近6月</th>
<th class="border px-2 py-1 text-xs lg:px-3 lg:py-2 lg:text-sm w-[15%]">近1年</th>
</template>
<template #default="{ row }">
<td class="border px-2 py-1 text-xs lg:px-3 lg:py-2 lg:text-sm font-medium whitespace-nowrap">
{{ row.label }}
</td>
<td class="border px-2 py-1 text-xs lg:px-3 lg:py-2 lg:text-sm text-center">
{{ row.specialDisplay ? '-' : row.d7 }}
</td>
<td class="border px-2 py-1 text-xs lg:px-3 lg:py-2 lg:text-sm text-center">
{{ row.specialDisplay ? '-' : row.m1 }}
</td>
<td class="border px-2 py-1 text-xs lg:px-3 lg:py-2 lg:text-sm text-center">
{{ row.specialDisplay ? '-' : row.m3 }}
</td>
<td class="border px-2 py-1 text-xs lg:px-3 lg:py-2 lg:text-sm text-center">
{{ row.specialDisplay ? '-' : row.m6 }}
</td>
<td class="border px-2 py-1 text-xs lg:px-3 lg:py-2 lg:text-sm text-center">{{ row.m12 }}</td>
</template>
</LTable>
</div>
</div>
<!-- <div v-if="data.flag_applyloanstr || data.flag_datastrategy">
<LTitle title="风险策略评估" />
<div class="bg-gray-50 p-4 rounded-lg">
<div class="flex flex-col gap-y-2">
<div v-if="data.flag_applyloanstr === '1'" class="flex items-center">
<span class="inline-block w-3 h-3 mr-2 rounded-full bg-yellow-500"></span>
<span>根据申请记录分析此用户存在多头申请特征</span>
</div>
<div v-if="data.flag_datastrategy === '1'" class="flex items-center">
<span class="inline-block w-3 h-3 mr-2 rounded-full bg-red-500"></span>
<span>根据数据策略评估此用户存在潜在风险特征</span>
</div>
</div>
</div>
</div> -->
</template>
<template v-else>
<div class="flex items-center justify-center h-60 bg-gray-50 rounded-lg">
<div class="text-center">
<div class="text-gray-400 text-4xl mb-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mx-auto" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div class="text-gray-600 text-xl font-medium">暂无借贷申请数据</div>
<div class="text-gray-500 mt-2">当前没有匹配到任何借贷申请记录</div>
</div>
</div>
</template>
</div>
</div>
</template>
<style scoped>
.chart-container {
width: 100%;
height: 350px;
}
.chart-container-small {
width: 100%;
height: 300px;
}
.form-radio {
@apply appearance-none rounded-full h-4 w-4 border border-gray-300 bg-white checked:bg-blue-600 checked:border-blue-600 focus:outline-none transition duration-200 align-top bg-no-repeat bg-center bg-contain float-left cursor-pointer;
}
.card {
@apply bg-white rounded-lg shadow-md p-4 md:p-6;
}
</style>