This commit is contained in:
2025-12-18 15:39:43 +08:00
parent d576d8e734
commit 0190e21287
48 changed files with 41428 additions and 379 deletions

View File

@@ -8838,5 +8838,908 @@
"success": true,
"timestamp": "2025-01-20 21:19:58"
}
},
{
"feature": {
"featureName": "借选指数评估",
"sort": 1
},
"data": {
"apiID": "JRZQ5E9F",
"data": {
"xyp_t01aazhzz": "1",
"xyp_t01abdzbz": "",
"xyp_t01aahzbz": "",
"xyp_t01ackzbz": "",
"xyp_t01cczgbc": "",
"xyp_t01adjzzz": "1",
"xyp_t01adzfbz": "",
"xyp_model_score_mid": "715",
"xyp_t01acizza": "",
"xyp_t01adjzzc": "1",
"xyp_t01aakzzz": "1",
"xyp_t02cchzza_cchzzz": "",
"xyp_t01acfzbc": "",
"xyp_t01degzbc": "0",
"xyp_t02cchzzc_cchzzz": "1.0",
"xyp_t01abgzzc": "1",
"xyp_t01dejzzc": "2",
"xyp_t02cckzbc_cckzbz": "",
"xyp_t01cclzba": "",
"xyp_t01ackzaz": "1",
"xyp_t01cclzbc": "",
"xyp_t01aazgzc": "1",
"xyp_t01aafzzc": "1",
"xyp_t01adgzbc": "",
"xyp_t01abjzbc": ""
}
}
},
{
"feature": {
"featureName": "特殊名单验证",
"sort": 1
},
"data": {
"apiID": "FLXG3D56",
"data": {
"data": {
"swift_number": "999333_20181029143459_23453A4E0",
"code": "00",
"flag_specialList_c": "1",
"sl_id_court_bad": "0",
"sl_id_court_executed": "0",
"sl_id_bank_bad": "0",
"sl_id_bank_overdue": "0",
"sl_id_bank_lost": "0",
"sl_id_nbank_bad": "0",
"sl_id_nbank_overdue": "0",
"sl_id_nbank_lost": "0",
"sl_id_nbank_nsloan_bad": "0",
"sl_id_nbank_nsloan_overdue": "0",
"sl_id_nbank_nsloan_lost": "0",
"sl_id_nbank_sloan_bad": "0",
"sl_id_nbank_sloan_overdue": "0",
"sl_id_nbank_sloan_lost": "0",
"sl_id_nbank_cons_bad": "0",
"sl_id_nbank_cons_overdue": "0",
"sl_id_nbank_cons_lost": "0",
"sl_id_nbank_finlea_bad": "0",
"sl_id_nbank_finlea_overdue": "0",
"sl_id_nbank_finlea_lost": "0",
"sl_id_nbank_autofin_bad": "0",
"sl_id_nbank_autofin_overdue": 0,
"sl_id_nbank_autofin_lost": "0",
"sl_id_nbank_other_bad": "0",
"sl_id_nbank_other_overdue": "0",
"sl_id_nbank_other_lost": "0",
"sl_id_court_bad_time": "0",
"sl_id_court_executed_time": "1",
"sl_id_bank_bad_time": "1",
"sl_id_bank_overdue_time": "2",
"sl_id_bank_lost_time": "0",
"sl_id_nbank_bad_time": "0",
"sl_id_nbank_overdue_time": "1",
"sl_id_nbank_lost_time": "1",
"sl_id_nbank_nsloan_bad_time": "0",
"sl_id_nbank_nsloan_overdue_time": "1",
"sl_id_nbank_nsloan_lost_time": "1",
"sl_id_nbank_sloan_bad_time": "0",
"sl_id_nbank_sloan_overdue_time": "1",
"sl_id_nbank_sloan_lost_time": "1",
"sl_id_nbank_cons_bad_time": "1",
"sl_id_nbank_cons_overdue_time": "0",
"sl_id_nbank_cons_lost_time": "1",
"sl_id_nbank_finlea_bad_time": "1",
"sl_id_nbank_finlea_overdue_time": "2",
"sl_id_nbank_finlea_lost_time": "0",
"sl_id_nbank_autofin_bad_time": "1",
"sl_id_nbank_autofin_overdue_time": "0",
"sl_id_nbank_autofin_lost_time": "2",
"sl_id_nbank_other_bad_time": "0",
"sl_id_nbank_other_overdue_time": "0",
"sl_id_nbank_other_lost_time": "0",
"sl_id_court_bad_allnum": "1",
"sl_id_court_executed_allnum": "1",
"sl_id_bank_bad_allnum": "1",
"sl_id_bank_overdue_allnum": "2",
"sl_id_bank_lost_allnum": "5",
"sl_id_nbank_bad_allnum": "6",
"sl_id_nbank_overdue_allnum": "1",
"sl_id_nbank_lost_allnum": "1",
"sl_id_nbank_nsloan_bad_allnum": "7",
"sl_id_nbank_nsloan_overdue_allnum": "1",
"sl_id_nbank_nsloan_lost_allnum": "1",
"sl_id_nbank_sloan_bad_allnum": "1",
"sl_id_nbank_sloan_overdue_allnum": "1",
"sl_id_nbank_sloan_lost_allnum": "1",
"sl_id_nbank_cons_bad_allnum": "1",
"sl_id_nbank_cons_overdue_allnum": "9",
"sl_id_nbank_cons_lost_allnum": "1",
"sl_id_nbank_finlea_bad_allnum": "1",
"sl_id_nbank_finlea_overdue_allnum": "2",
"sl_id_nbank_finlea_lost_allnum": "4",
"sl_id_nbank_autofin_bad_allnum": "1",
"sl_id_nbank_autofin_overdue_allnum": "8",
"sl_id_nbank_autofin_lost_allnum": "2",
"sl_id_nbank_other_bad_allnum": "5",
"sl_id_nbank_other_overdue_allnum": "7",
"sl_id_nbank_other_lost_allnum": "2",
"sl_cell_bank_bad": "0",
"sl_cell_bank_overdue": "0",
"sl_cell_bank_lost": "0",
"sl_cell_nbank_bad": "0",
"sl_cell_nbank_overdue": "0",
"sl_cell_nbank_lost": "0",
"sl_cell_nbank_nsloan_bad": "0",
"sl_cell_nbank_nsloan_overdue": "0",
"sl_cell_nbank_nsloan_lost": "0",
"sl_cell_nbank_sloan_bad": "0",
"sl_cell_nbank_sloan_overdue": "0",
"sl_cell_nbank_sloan_lost": "0",
"sl_cell_nbank_cons_bad": "0",
"sl_cell_nbank_cons_overdue": "0",
"sl_cell_nbank_cons_lost": "0",
"sl_cell_nbank_finlea_bad": "0",
"sl_cell_nbank_finlea_overdue": "0",
"sl_cell_nbank_finlea_lost": "0",
"sl_cell_nbank_autofin_bad": "0",
"sl_cell_nbank_autofin_overdue": "0",
"sl_cell_nbank_autofin_lost": "0",
"sl_cell_nbank_other_bad": "0",
"sl_cell_nbank_other_overdue": "0",
"sl_cell_nbank_other_lost": "0",
"sl_cell_bank_bad_time": "1",
"sl_cell_bank_overdue_time": "2",
"sl_cell_bank_lost_time": "0",
"sl_cell_nbank_bad_time": "0",
"sl_cell_nbank_overdue_time": "1",
"sl_cell_nbank_lost_time": "1",
"sl_cell_nbank_nsloan_bad_time": "0",
"sl_cell_nbank_nsloan_overdue_time": "1",
"sl_cell_nbank_nsloan_lost_time": "1",
"sl_cell_nbank_sloan_bad_time": "0",
"sl_cell_nbank_sloan_overdue_time": "1",
"sl_cell_nbank_sloan_lost_time": "1",
"sl_cell_nbank_cons_bad_time": "1",
"sl_cell_nbank_cons_overdue_time": "0",
"sl_cell_nbank_cons_lost_time": "1",
"sl_cell_nbank_finlea_bad_time": "1",
"sl_cell_nbank_finlea_overdue_time": "2",
"sl_cell_nbank_finlea_lost_time": "0",
"sl_cell_nbank_autofin_bad_time": "1",
"sl_cell_nbank_autofin_overdue_time": "0",
"sl_cell_nbank_autofin_lost_time": "2",
"sl_cell_nbank_other_bad_time": "0",
"sl_cell_nbank_other_overdue_time": "0",
"sl_cell_nbank_other_lost_time": "0",
"sl_cell_bank_bad_allnum": "1",
"sl_cell_bank_overdue_allnum": "2",
"sl_cell_bank_lost_allnum": "3",
"sl_cell_nbank_bad_allnum": "5",
"sl_cell_nbank_overdue_allnum": "1",
"sl_cell_nbank_lost_allnum": "1",
"sl_cell_nbank_nsloan_bad_allnum": "3",
"sl_cell_nbank_nsloan_overdue_allnum": "1",
"sl_cell_nbank_nsloan_lost_allnum": "1",
"sl_cell_nbank_sloan_bad_allnum": "7",
"sl_cell_nbank_sloan_overdue_allnum": "1",
"sl_cell_nbank_sloan_lost_allnum": "1",
"sl_cell_nbank_cons_bad_allnum": "1",
"sl_cell_nbank_cons_overdue_allnum": "8",
"sl_cell_nbank_cons_lost_allnum": "1",
"sl_cell_nbank_finlea_bad_allnum": "1",
"sl_cell_nbank_finlea_overdue_allnum": "2",
"sl_cell_nbank_finlea_lost_allnum": "4",
"sl_cell_nbank_autofin_bad_allnum": "1",
"sl_cell_nbank_autofin_overdue_allnum": "6",
"sl_cell_nbank_autofin_lost_allnum": "2",
"sl_cell_nbank_other_bad_allnum": "7",
"sl_cell_nbank_other_overdue_allnum": "6",
"sl_cell_nbank_other_lost_allnum": "9"
}
}
}
},
{
"feature": {
"featureName": "公安不良人员名单(加强版)",
"sort": 1
},
"data": {
"apiID": "FLXGDEA9",
"data": {
"level":"0"
}
}
},
{
"feature": {
"featureName": "学历信息查询A",
"sort": 1
},
"data": {
"apiID": "IVYZ9A2B",
"data": {
"data": {
"query_id": "202505213797602437204758911904",
"education_background": {
"msg": "查询成功有结果",
"data": [
{
"jsrq": "1806",
"xxxs": "普通全日制",
"xl": "大学专科",
"xxlx": "其他",
"zymc": "其他",
"ksrq": "1509"
},
{
"jsrq": "2206",
"xxxs": "普通全日制",
"xl": "大学本科",
"xxlx": "其他",
"zymc": "其他",
"ksrq": "1809"
}
],
"code": "9100"
}
},
"err_msg": "请求成功",
"err_code": "200"
}
}
}
,{
"feature": {
"featureName": "单人婚姻查询(登记时间版)",
"sort": 1
},
"data": {
"apiID": "IVYZ81NC",
"data": {
"code": "0",
"data": {
"op_date": "2025-04-16",
"op_type": "IB",
"op_type_desc": "离婚"
},
"message": "成功",
"seqNo": "0URE0UAL251011192554196"
}
}
}
,{
"feature": {
"featureName": "单人婚姻状态A",
"sort": 1
},
"data": {
"apiID": "IVYZ5733",
"data":{
"code": "0",
"data": {
"data": "INR:匹配不成功"
},
"seqNo": "YW0N4EH1250614162840933",
"message": "成功"
}
}
}
,{
"feature": {
"featureName": "借贷意向验证",
"sort": 1
},
"data": {
"apiID": "JRZQ0A03",
"data": {
"code": "00",
"data": {
"als_m1_cell_nbank_allnum": "1",
"als_m12_cell_nbank_nsloan_orgnum": "1",
"als_m6_id_bank_tra_allnum": "2",
"als_m6_cell_nbank_max_inteday": "45",
"als_m12_id_bank_selfnum": "0",
"als_m6_id_pdl_allnum": "2",
"als_m3_cell_min_monnum": "0",
"als_m1_cell_bank_tra_allnum": "1",
"als_m3_id_bank_tra_allnum": "2",
"als_m6_cell_nbank_orgnum": "6",
"als_m12_id_bank_min_monnum": "0",
"als_m12_id_bank_allnum": "2",
"als_d15_id_rel_orgnum": "1",
"als_m12_cell_nbank_sloan_orgnum": "1",
"als_m1_id_nbank_allnum": "1",
"als_m6_cell_nbank_max_monnum": "4",
"als_m12_cell_caon_allnum": "4",
"als_m6_cell_nbank_night_allnum": "4",
"als_m3_cell_nbank_orgnum": "3",
"als_m3_cell_min_inteday": "0",
"als_m6_cell_nbank_sloan_allnum": "5",
"als_m12_id_nbank_max_monnum": "4",
"als_m1_cell_nbank_sloan_allnum": "1",
"als_m3_id_min_inteday": "0",
"als_d15_cell_bank_orgnum": "1",
"als_m12_cell_bank_orgnum": "2",
"als_m1_cell_nbank_night_orgnum": "0",
"als_m1_id_nbank_cf_orgnum": "1",
"als_d15_cell_bank_selfnum": "0",
"als_m6_cell_bank_tra_allnum": "2",
"als_lst_cell_bank_csinteday": "1",
"als_d15_cell_bank_allnum": "1",
"als_m6_cell_nbank_night_orgnum": "4",
"als_m12_cell_nbank_nsloan_allnum": "1",
"als_m12_cell_nbank_max_inteday": "62",
"als_m6_cell_max_monnum": "4",
"als_m12_cell_min_monnum": "0",
"als_m12_cell_bank_allnum": "2",
"als_m3_id_nbank_max_monnum": "3",
"als_m3_cell_max_monnum": "4",
"als_m3_id_bank_min_monnum": "0",
"als_m6_cell_bank_tra_orgnum": "2",
"als_m1_cell_nbank_night_allnum": "0",
"als_m3_cell_nbank_allnum": "6",
"als_d15_cell_bank_week_orgnum": "0",
"als_m3_id_bank_tra_orgnum": "2",
"als_d15_cell_rel_orgnum": "1",
"als_m6_cell_nbank_oth_allnum": "7",
"als_m6_cell_nbank_selfnum": "0",
"als_m12_cell_nbank_sloan_allnum": "9",
"als_m12_id_nbank_cons_orgnum": "4",
"als_m3_cell_bank_max_monnum": "1",
"als_m12_cell_nbank_night_orgnum": "4",
"als_m6_id_nbank_nsloan_allnum": "1",
"als_m3_id_nbank_oth_allnum": "1",
"als_m12_id_bank_orgnum": "2",
"swift_number": "3034309_20250806163337_77293E53A19",
"als_m12_id_tot_mons": "9",
"als_m12_id_avg_monnum": "2.22",
"als_m3_id_nbank_sloan_orgnum": "1",
"als_m3_cell_nbank_max_inteday": "45",
"als_m1_id_nbank_orgnum": "1",
"als_m3_id_bank_selfnum": "0",
"als_m6_id_bank_tra_orgnum": "2",
"als_d15_cell_bank_night_allnum": "0",
"als_m1_cell_nbank_orgnum": "1",
"als_m6_id_bank_selfnum": "0",
"als_m6_id_max_monnum": "4",
"als_m12_cell_bank_selfnum": "0",
"als_m3_cell_nbank_selfnum": "0",
"als_m3_id_nbank_oth_orgnum": "1",
"als_m3_id_nbank_max_inteday": "45",
"als_m12_cell_nbank_max_monnum": "4",
"als_m6_cell_min_monnum": "0",
"als_d15_cell_rel_allnum": "1",
"als_m6_id_bank_avg_monnum": "1.00",
"als_m12_id_nbank_selfnum": "0",
"als_d15_cell_bank_week_allnum": "0",
"als_m6_cell_nbank_oth_orgnum": "4",
"als_m6_id_pdl_orgnum": "2",
"als_lst_id_nbank_consnum": "1",
"als_m6_id_rel_allnum": "6",
"als_m6_id_rel_orgnum": "2",
"als_m12_id_nbank_oth_allnum": "7",
"als_m12_id_cooff_orgnum": "1",
"als_m6_cell_tot_mons": "5",
"als_m12_id_bank_tra_allnum": "2",
"als_m3_id_max_inteday": "42",
"als_m3_cell_nbank_sloan_orgnum": "1",
"als_m1_id_nbank_night_orgnum": "0",
"als_m12_id_bank_tra_orgnum": "2",
"als_m6_cell_nbank_nsloan_allnum": "1",
"als_m6_cell_pdl_allnum": "2",
"flag_datastrategy": "1",
"als_m1_id_bank_orgnum": "1",
"als_m3_cell_pdl_allnum": "1",
"als_m1_id_bank_selfnum": "0",
"als_m12_cell_nbank_cf_allnum": "11",
"als_m3_id_rel_orgnum": "2",
"als_m6_id_nbank_min_inteday": "0",
"als_m3_id_caon_orgnum": "1",
"als_m6_id_nbank_nsloan_orgnum": "1",
"als_m6_cell_nbank_cons_allnum": "8",
"als_m1_cell_nbank_week_allnum": "0",
"als_m1_id_nbank_sloan_allnum": "1",
"als_d15_id_bank_night_allnum": "0",
"als_d15_id_bank_allnum": "1",
"als_m1_id_nbank_cf_allnum": "1",
"als_lst_cell_nbank_inteday": "20",
"als_m1_cell_bank_week_orgnum": "0",
"als_m3_cell_bank_week_allnum": "0",
"als_lst_id_bank_inteday": "14",
"als_m6_id_max_inteday": "42",
"als_m12_cell_rel_orgnum": "2",
"als_m1_id_bank_allnum": "1",
"als_m12_cell_nbank_min_monnum": "0",
"als_m6_id_tot_mons": "5",
"als_m1_id_bank_week_allnum": "0",
"als_m12_cell_bank_max_monnum": "1",
"als_m3_cell_tot_mons": "2",
"als_m12_id_rel_allnum": "10",
"als_m3_id_bank_max_inteday": "58",
"als_m3_id_rel_allnum": "4",
"als_m12_cell_nbank_selfnum": "0",
"als_m3_cell_bank_week_orgnum": "0",
"als_m12_cell_tot_mons": "9",
"als_m1_cell_bank_week_allnum": "0",
"als_m12_id_rel_orgnum": "2",
"als_lst_cell_bank_inteday": "14",
"als_m6_id_nbank_min_monnum": "0",
"als_d15_id_bank_night_orgnum": "0",
"als_m12_id_bank_max_inteday": "58",
"als_m6_id_bank_min_monnum": "0",
"als_m3_id_nbank_tot_mons": "2",
"als_m3_id_bank_orgnum": "2",
"als_m3_id_bank_week_allnum": "0",
"als_m3_cell_nbank_night_orgnum": "0",
"als_m6_cell_nbank_sloan_orgnum": "1",
"als_m1_id_nbank_sloan_orgnum": "1",
"als_m1_cell_bank_allnum": "1",
"als_m1_id_rel_allnum": "2",
"als_m12_cell_max_monnum": "4",
"als_m12_cell_nbank_allnum": "18",
"als_m1_cell_bank_night_allnum": "0",
"als_m1_id_nbank_night_allnum": "0",
"als_m12_cell_rel_allnum": "10",
"als_lst_id_bank_csinteday": "1",
"als_m1_id_bank_week_orgnum": "0",
"als_m3_id_bank_tot_mons": "2",
"als_m12_cell_bank_tra_allnum": "2",
"als_m3_id_bank_max_monnum": "1",
"als_m3_id_nbank_selfnum": "0",
"als_m1_cell_nbank_week_orgnum": "0",
"als_m3_cell_pdl_orgnum": "1",
"als_m6_cell_nbank_allnum": "14",
"als_m3_cell_nbank_cf_allnum": "5",
"als_m1_id_nbank_week_allnum": "0",
"als_m6_cell_pdl_orgnum": "2",
"als_m12_cell_bank_tra_orgnum": "2",
"als_d15_id_rel_allnum": "1",
"als_m6_id_bank_tot_mons": "2",
"als_m3_id_tot_mons": "2",
"als_m6_cell_nbank_nsloan_orgnum": "1",
"als_m12_id_bank_week_allnum": "0",
"als_lst_id_nbank_inteday": "20",
"als_m6_id_nbank_selfnum": "0",
"als_m6_id_min_monnum": "0",
"code": "00",
"als_d15_id_bank_selfnum": "0",
"als_m3_cell_bank_tra_orgnum": "2",
"als_fst_id_bank_inteday": "72",
"als_m3_id_avg_monnum": "4.00",
"als_m1_cell_bank_selfnum": "0",
"als_m3_cell_nbank_night_allnum": "0",
"als_m12_cell_nbank_orgnum": "6",
"als_m3_cell_nbank_sloan_allnum": "3",
"als_lst_id_nbank_csinteday": "1",
"als_m3_id_bank_week_orgnum": "0",
"als_m12_cell_nbank_avg_monnum": "2.00",
"als_m3_id_nbank_allnum": "6",
"als_m3_cell_bank_tra_allnum": "2",
"als_m1_cell_bank_night_orgnum": "0",
"als_m6_id_nbank_allnum": "14",
"als_m12_cell_nbank_cf_orgnum": "2",
"als_m6_cell_nbank_min_monnum": "0",
"als_m1_cell_bank_orgnum": "1",
"als_m12_id_max_monnum": "4",
"als_m12_cell_bank_week_allnum": "0",
"als_m12_cell_bank_min_inteday": "58",
"als_m12_cell_bank_night_orgnum": "1",
"als_m12_id_min_monnum": "0",
"als_m12_id_bank_week_orgnum": "0",
"als_m1_id_bank_night_allnum": "0",
"als_m3_id_max_monnum": "4",
"als_m3_cell_nbank_oth_orgnum": "1",
"als_m12_cell_min_inteday": "0",
"als_m3_id_nbank_orgnum": "3",
"als_m3_cell_bank_max_inteday": "58",
"als_lst_id_bank_consnum": "1",
"als_m1_id_nbank_week_orgnum": "0",
"als_m6_id_nbank_orgnum": "6",
"als_fst_cell_bank_inteday": "72",
"als_m6_id_avg_monnum": "3.20",
"als_m3_cell_nbank_cons_orgnum": "2",
"als_m3_id_bank_night_allnum": "1",
"DataStrategy": {
"strategy_version": "1.0",
"product_type": "100099",
"strategy_id": "DTA_BR0007512",
"product_name": "预置_借贷意向验证",
"scene": "lend"
},
"als_m6_cell_bank_week_orgnum": "0",
"als_m3_cell_bank_night_orgnum": "1",
"als_m3_cell_nbank_cf_orgnum": "2",
"als_m6_id_bank_night_orgnum": "1",
"als_m3_id_nbank_cons_orgnum": "2",
"als_m1_id_bank_tra_orgnum": "1",
"als_fst_id_nbank_inteday": "326",
"als_m1_cell_nbank_cf_allnum": "1",
"als_m12_id_nbank_night_allnum": "5",
"als_m3_cell_caon_orgnum": "1",
"als_m12_id_nbank_avg_monnum": "2.00",
"als_m12_id_nbank_cf_allnum": "11",
"als_m12_cell_bank_tot_mons": "2",
"als_m12_id_nbank_sloan_allnum": "9",
"als_m3_cell_nbank_oth_allnum": "1",
"als_m6_id_nbank_oth_allnum": "7",
"als_m3_id_nbank_cons_allnum": "3",
"als_m12_cell_nbank_cons_orgnum": "4",
"als_m3_id_nbank_cf_allnum": "5",
"als_m12_cell_bank_avg_monnum": "1.00",
"als_m3_cell_bank_night_allnum": "1",
"als_m6_cell_nbank_avg_monnum": "2.80",
"als_m3_cell_bank_orgnum": "2",
"als_m3_cell_caon_allnum": "2",
"als_d15_id_bank_orgnum": "1",
"als_m6_id_nbank_cf_orgnum": "2",
"als_m3_id_bank_allnum": "2",
"als_m3_id_nbank_cf_orgnum": "2",
"als_m12_id_nbank_night_orgnum": "4",
"als_m3_id_nbank_avg_monnum": "3.00",
"als_m3_id_min_monnum": "0",
"als_lst_cell_nbank_csinteday": "1",
"als_m1_id_bank_night_orgnum": "0",
"als_m1_id_rel_orgnum": "2",
"als_m1_cell_nbank_cf_orgnum": "1",
"als_m6_id_nbank_oth_orgnum": "4",
"als_m12_id_nbank_orgnum": "6",
"als_m3_id_nbank_night_allnum": "0",
"als_m3_cell_bank_selfnum": "0",
"als_m3_cell_nbank_cons_allnum": "3",
"als_m3_id_bank_min_inteday": "58",
"als_m6_cell_caon_orgnum": "3",
"als_m12_id_bank_avg_monnum": "1.00",
"als_m6_id_bank_night_allnum": "1",
"als_m6_cell_bank_tot_mons": "2",
"als_m3_id_caon_allnum": "2",
"als_m12_id_min_inteday": "0",
"als_m12_id_nbank_sloan_orgnum": "1",
"als_m12_id_pdl_orgnum": "2",
"als_m3_id_pdl_orgnum": "1",
"als_m1_id_bank_tra_allnum": "1",
"als_m6_cell_nbank_cf_orgnum": "2",
"als_m12_cell_bank_night_allnum": "1",
"als_m6_cell_nbank_tot_mons": "5",
"als_d15_id_bank_tra_allnum": "1",
"als_m6_cell_cooff_orgnum": "1",
"als_m12_cell_bank_week_orgnum": "0",
"als_m3_cell_bank_allnum": "2",
"als_m6_cell_nbank_cons_orgnum": "4",
"als_d15_id_bank_tra_orgnum": "1",
"als_m12_id_nbank_cf_orgnum": "2",
"als_m12_id_max_inteday": "62",
"als_m6_cell_bank_selfnum": "0",
"als_m1_cell_nbank_selfnum": "0",
"als_m12_id_cooff_allnum": "4",
"als_m6_id_nbank_night_allnum": "4",
"als_d15_id_bank_week_orgnum": "0",
"als_m6_id_nbank_avg_monnum": "2.80",
"als_m12_id_bank_tot_mons": "2",
"als_m6_cell_nbank_cf_allnum": "7",
"als_m6_id_nbank_sloan_orgnum": "1",
"als_m1_cell_rel_allnum": "2",
"als_m3_cell_bank_tot_mons": "2",
"als_m12_id_nbank_max_inteday": "62",
"als_m6_cell_max_inteday": "42",
"als_m6_id_caon_allnum": "4",
"als_m6_id_nbank_night_orgnum": "4",
"als_m3_cell_cooff_orgnum": "1",
"als_m6_id_bank_max_inteday": "58",
"als_m12_id_bank_min_inteday": "58",
"als_m3_cell_nbank_max_monnum": "3",
"als_m6_id_nbank_sloan_allnum": "5",
"als_m1_cell_rel_orgnum": "2",
"als_m6_cell_bank_min_inteday": "58",
"als_m12_cell_nbank_min_inteday": "0",
"als_m3_cell_cooff_allnum": "1",
"als_d15_id_bank_week_allnum": "0",
"als_m6_id_caon_orgnum": "3",
"als_m6_cell_cooff_allnum": "4",
"als_m3_id_cooff_allnum": "1",
"als_m6_cell_min_inteday": "0",
"als_m6_id_cooff_allnum": "4",
"als_m12_id_caon_orgnum": "3",
"als_m12_id_nbank_nsloan_allnum": "1",
"als_m6_cell_nbank_week_orgnum": "4",
"als_m3_cell_rel_orgnum": "2",
"als_m6_cell_rel_orgnum": "2",
"als_m12_id_nbank_oth_orgnum": "4",
"als_m12_id_nbank_cons_allnum": "8",
"als_m3_id_nbank_sloan_allnum": "3",
"als_m12_cell_avg_monnum": "2.22",
"als_m3_cell_max_inteday": "42",
"als_m12_cell_cooff_allnum": "4",
"als_m3_id_bank_avg_monnum": "1.00",
"flag_applyloanstr": "1",
"als_m6_cell_bank_night_allnum": "1",
"als_m6_id_bank_week_orgnum": "0",
"als_m12_id_nbank_week_orgnum": "5",
"als_m3_cell_nbank_tot_mons": "2",
"als_m3_cell_bank_avg_monnum": "1.00",
"als_d15_cell_bank_night_orgnum": "0",
"als_d15_cell_bank_tra_orgnum": "1",
"als_m12_cell_nbank_week_allnum": "8",
"als_d15_cell_bank_tra_allnum": "1",
"als_m12_id_nbank_week_allnum": "8",
"als_m6_cell_bank_min_monnum": "0",
"als_m3_cell_nbank_min_inteday": "0",
"als_m12_id_nbank_allnum": "18",
"als_m3_cell_avg_monnum": "4.00",
"als_m6_id_nbank_tot_mons": "5",
"als_m12_cell_nbank_oth_orgnum": "4",
"als_m3_cell_bank_min_inteday": "58",
"als_m12_cell_pdl_allnum": "2",
"als_m6_cell_caon_allnum": "4",
"als_m12_id_nbank_tot_mons": "9",
"als_m12_id_pdl_allnum": "2",
"als_m6_id_cooff_orgnum": "1",
"als_m3_id_cooff_orgnum": "1",
"als_m3_id_nbank_night_orgnum": "0",
"als_m12_id_nbank_min_inteday": "0",
"als_m6_cell_bank_allnum": "2",
"als_m6_cell_nbank_week_allnum": "6",
"als_m12_id_caon_allnum": "4",
"als_m12_cell_pdl_orgnum": "2",
"als_m6_cell_bank_max_monnum": "1",
"als_m3_id_nbank_min_inteday": "0",
"als_m6_cell_bank_max_inteday": "58",
"als_m6_cell_bank_orgnum": "2",
"als_m12_cell_cooff_orgnum": "1",
"als_m3_id_pdl_allnum": "1",
"als_m12_cell_nbank_oth_allnum": "7",
"als_m6_cell_avg_monnum": "3.20",
"als_m12_cell_nbank_night_allnum": "5",
"als_m6_id_bank_min_inteday": "58",
"als_m6_id_nbank_week_allnum": "6",
"als_m6_cell_bank_night_orgnum": "1",
"als_m1_cell_bank_tra_orgnum": "1",
"als_m1_id_nbank_selfnum": "0",
"als_m3_cell_bank_min_monnum": "0",
"als_m6_id_nbank_cf_allnum": "7",
"als_m6_id_bank_max_monnum": "1",
"als_m6_id_nbank_week_orgnum": "4",
"als_m6_id_bank_orgnum": "2",
"als_m6_cell_bank_avg_monnum": "1.00",
"als_m3_cell_nbank_min_monnum": "0",
"als_m6_id_min_inteday": "0",
"als_lst_cell_bank_consnum": "1",
"als_m6_id_nbank_max_inteday": "45",
"als_m12_cell_caon_orgnum": "3",
"als_m6_cell_bank_week_allnum": "0",
"als_m1_cell_nbank_sloan_orgnum": "1",
"als_m3_cell_rel_allnum": "4",
"als_m6_id_nbank_max_monnum": "4",
"als_m12_id_nbank_nsloan_orgnum": "1",
"als_m12_cell_nbank_cons_allnum": "8",
"als_m12_cell_nbank_tot_mons": "9",
"als_m6_cell_rel_allnum": "6",
"als_m12_cell_nbank_week_orgnum": "5",
"als_m6_id_bank_allnum": "2",
"als_m6_id_bank_week_allnum": "0",
"als_m6_id_nbank_cons_orgnum": "4",
"als_m3_id_nbank_week_orgnum": "1",
"als_m12_id_bank_night_allnum": "1",
"als_m3_id_bank_night_orgnum": "1",
"als_m3_id_nbank_min_monnum": "0",
"als_m12_id_bank_max_monnum": "1",
"als_fst_cell_nbank_inteday": "326",
"als_m3_id_nbank_week_allnum": "1",
"als_m6_id_nbank_cons_allnum": "8",
"als_m12_cell_max_inteday": "62",
"als_m6_cell_nbank_min_inteday": "0",
"als_m12_id_nbank_min_monnum": "0",
"als_lst_cell_nbank_consnum": "1",
"als_m3_cell_nbank_week_orgnum": "1",
"als_m12_cell_bank_max_inteday": "58",
"als_m12_cell_bank_min_monnum": "0",
"als_m3_cell_nbank_avg_monnum": "3.00",
"als_m12_id_bank_night_orgnum": "1",
"als_m3_cell_nbank_week_allnum": "1"
},
"flag_applyloanstr": "1"
}
}
}
,{
"feature": {
"featureName": "偿债压力指数",
"sort": 1
},
"data": {
"apiID": "JRZQ4AA8",
"data": {
"data": {
"swift_number": "999333_20181029143459_23453A4E0",
"code": "00",
"flag_debtrepaystress": "1",
"drs_nodebtscore": "80"
}
}
}
} ,{
"feature": {
"featureName": "借贷行为验证",
"sort": 1
},
"data": {
"apiID": "JRZQ8203",
"data":{
"data": {
"swift_number": "999333_20181029143459_23453A4E0",
"code": "00",
"flag_totalloan": "1",
"tl_id_eletail_lasttime": "2020-05-02",
"tl_id_eletail_lasttype": "c",
"tl_id_eletail_num": "1",
"tl_id_eletail_org": "1",
"tl_id_m1_nbank_passnum": "1",
"tl_id_m1_nbank_passorg": "1",
"tl_id_m1_nbank_passlendamt": "1",
"tl_id_m3_nbank_passnum": "1",
"tl_id_m3_nbank_passorg": "1",
"tl_id_m3_nbank_passlendamt": "1",
"tl_id_m6_nbank_passnum": "1",
"tl_id_m6_nbank_passorg": "1",
"tl_id_m6_nbank_passlendamt": "1",
"tl_id_m9_nbank_passnum": "1",
"tl_id_m9_nbank_passorg": "1",
"tl_id_m9_nbank_passlendamt": "1",
"tl_id_m12_nbank_passnum": "1",
"tl_id_m12_nbank_passorg": "1",
"tl_id_m12_nbank_passlendamt": "1",
"tl_id_t0_nbank_num": "1",
"tl_id_t0_nbank_org": "1",
"tl_id_t0_nbank_lendamt": "1",
"tl_id_t0_nbank_reamt": "1",
"tl_id_t1_nbank_num": "1",
"tl_id_t1_nbank_org": "1",
"tl_id_t1_nbank_lendamt": "1",
"tl_id_t1_nbank_reamt": "1",
"tl_id_t2_nbank_num": "1",
"tl_id_t2_nbank_org": "1",
"tl_id_t2_nbank_lendamt": "1",
"tl_id_t2_nbank_reamt": "1",
"tl_id_t3_nbank_num": "1",
"tl_id_t3_nbank_org": "1",
"tl_id_t3_nbank_lendamt": "1",
"tl_id_t3_nbank_reamt": "1",
"tl_id_t4_nbank_num": "1",
"tl_id_t4_nbank_org": "1",
"tl_id_t4_nbank_lendamt": "1",
"tl_id_t4_nbank_reamt": "1",
"tl_id_t5_nbank_num": "1",
"tl_id_t5_nbank_org": "1",
"tl_id_t5_nbank_lendamt": "1",
"tl_id_t5_nbank_reamt": "1",
"tl_id_t6_nbank_num": "1",
"tl_id_t6_nbank_org": "1",
"tl_id_t6_nbank_lendamt": "1",
"tl_id_t6_nbank_reamt": "1",
"tl_id_t7_nbank_num": "1",
"tl_id_t7_nbank_org": "1",
"tl_id_t7_nbank_lendamt": "1",
"tl_id_t7_nbank_reamt": "1",
"tl_id_t8_nbank_num": "1",
"tl_id_t8_nbank_org": "1",
"tl_id_t8_nbank_lendamt": "1",
"tl_id_t8_nbank_reamt": "1",
"tl_id_t9_nbank_num": "1",
"tl_id_t9_nbank_org": "1",
"tl_id_t9_nbank_lendamt": "1",
"tl_id_t9_nbank_reamt": "1",
"tl_id_t10_nbank_num": "1",
"tl_id_t10_nbank_org": "1",
"tl_id_t10_nbank_lendamt": "1",
"tl_id_t10_nbank_reamt": "1",
"tl_id_t11_nbank_num": "1",
"tl_id_t11_nbank_org": "1",
"tl_id_t11_nbank_lendamt": "1",
"tl_id_t11_nbank_reamt": "1",
"tl_cell_eletail_lasttime": "2020-05-02",
"tl_cell_eletail_lasttype": "c",
"tl_cell_eletail_num": "1",
"tl_cell_eletail_org": "1",
"tl_cell_m1_nbank_passnum": "1",
"tl_cell_m1_nbank_passorg": "1",
"tl_cell_m1_nbank_passlendamt": "1",
"tl_cell_m3_nbank_passnum": "1",
"tl_cell_m3_nbank_passorg": "1",
"tl_cell_m3_nbank_passlendamt": "1",
"tl_cell_m6_nbank_passnum": "1",
"tl_cell_m6_nbank_passorg": "1",
"tl_cell_m6_nbank_passlendamt": "1",
"tl_cell_m9_nbank_passnum": "1",
"tl_cell_m9_nbank_passorg": "1",
"tl_cell_m9_nbank_passlendamt": "1",
"tl_cell_m12_nbank_passnum": "1",
"tl_cell_m12_nbank_passorg": "1",
"tl_cell_m12_nbank_passlendamt": "1",
"tl_cell_t0_nbank_num": "1",
"tl_cell_t0_nbank_org": "1",
"tl_cell_t0_nbank_lendamt": "1",
"tl_cell_t0_nbank_reamt": "1",
"tl_cell_t1_nbank_num": "1",
"tl_cell_t1_nbank_org": "1",
"tl_cell_t1_nbank_lendamt": "1",
"tl_cell_t1_nbank_reamt": "1",
"tl_cell_t2_nbank_num": "1",
"tl_cell_t2_nbank_org": "1",
"tl_cell_t2_nbank_lendamt": "1",
"tl_cell_t2_nbank_reamt": "1",
"tl_cell_t3_nbank_num": "1",
"tl_cell_t3_nbank_org": "1",
"tl_cell_t3_nbank_lendamt": "1",
"tl_cell_t3_nbank_reamt": "1",
"tl_cell_t4_nbank_num": "1",
"tl_cell_t4_nbank_org": "1",
"tl_cell_t4_nbank_lendamt": "1",
"tl_cell_t4_nbank_reamt": "1",
"tl_cell_t5_nbank_num": "1",
"tl_cell_t5_nbank_org": "1",
"tl_cell_t5_nbank_lendamt": "1",
"tl_cell_t5_nbank_reamt": "1",
"tl_cell_t6_nbank_num": "1",
"tl_cell_t6_nbank_org": "1",
"tl_cell_t6_nbank_lendamt": "1",
"tl_cell_t6_nbank_reamt": "1",
"tl_cell_t7_nbank_num": "1",
"tl_cell_t7_nbank_org": "1",
"tl_cell_t7_nbank_lendamt": "1",
"tl_cell_t7_nbank_reamt": "1",
"tl_cell_t8_nbank_num": "1",
"tl_cell_t8_nbank_org": "1",
"tl_cell_t8_nbank_lendamt": "1",
"tl_cell_t8_nbank_reamt": "1",
"tl_cell_t9_nbank_num": "1",
"tl_cell_t9_nbank_org": "1",
"tl_cell_t9_nbank_lendamt": "1",
"tl_cell_t9_nbank_reamt": "1",
"tl_cell_t10_nbank_num": "1",
"tl_cell_t10_nbank_org": "1",
"tl_cell_t10_nbank_lendamt": "1",
"tl_cell_t10_nbank_reamt": "1",
"tl_cell_t11_nbank_num": "1",
"tl_cell_t11_nbank_org": "1",
"tl_cell_t11_nbank_lendamt": "1",
"tl_cell_t11_nbank_reamt": "1"
}
}
}
} ,{
"feature": {
"featureName": "名下车辆",
"sort": 1
},
"data": {
"apiID": "QCXG7A2B",
"data":
{
"carNum": "1"
}
}
} ,{
"feature": {
"featureName": "学籍学历核验(实时版)",
"sort": 1
},
"data": {
"apiID": "IVYZ3P9M",
"data": [
{
"specialtyName": "20307",
"graduationDate": "20210620",
"educationLevel": "2",
"studentName": "张三",
"enrollmentDate": "20180910",
"learningForm": "2",
"idNumber": "45212220000827423X",
"schoolName": "10001"
}
]
}
}
]

View File

@@ -251,7 +251,7 @@ const featureMap = {
remark: '基于个人收入指数进行消费能力等级评估,展示用户的月消费能力范围。消费能力等级反映用户的消费水平,等级越高对应的月消费能力越强。数据来源于个人收入指数评分,实际消费能力可能因个人消费习惯、地区差异等因素而有所不同,建议结合其他消费行为数据进行综合评估。'
},
// 名下车辆
// 名下车辆详细版
QCXG9P1C: {
name: "名下车辆",
component: defineAsyncComponent(() => import("@/ui/CQCXG9P1C.vue")),
@@ -270,6 +270,56 @@ const featureMap = {
component: defineAsyncComponent(() => import("@/ui/DWBG7F3A/index.vue")),
remark: '多头借贷行业风险版提供全面的多头借贷风险评估,包括多头共债子分、多头申请、多头逾期、圈团风险和可疑欺诈风险等多维度分析。'
},
JRZQ5E9F: {
name: "贷款风险评估",
component: defineAsyncComponent(() => import("@/ui/CJRZQ5E9F/index.vue")),
remark: '贷款风险评估提供全面的个人贷款风险分析,包括风险概览、信用评分、贷款行为分析、机构分析等多维度评估。'
},
FLXG3D56: {
name: "违约失信",
component: defineAsyncComponent(() => import("@/ui/CFLXG3D56.vue")),
remark: '违约失信用于检测个人在法院失信、银行风险、非银机构风险等多个维度的不良记录,包括法院失信被执行人、银行不良记录、非银机构逾期、失联等各类风险名单。'
},
FLXGDEA9: {
name: "本人不良",
component: defineAsyncComponent(() => import("@/ui/CFLXGDEA9.vue")),
remark: '本人不良记录查询结果来源于公安部门等权威机构,包括各类违法犯罪前科记录。查询结果仅供参考,具体信息以相关部门官方记录为准。'
},
IVYZ81NC:{
name: "婚姻状态",
component: defineAsyncComponent(() => import("@/ui/CIVYZ81NC.vue")),
remark: '查询结果为"未婚或尚未登记结婚"时,表示婚姻登记处暂无相关的登记记录。婚姻状态信息由婚姻登记处逐级上报,可能存在数据遗漏或更新滞后。当前可查询的婚姻状态包括:未婚或尚未登记结婚、已婚、离异。如您对查询结果有疑问,请联系客服反馈。',
},
IVYZ5733:{
name: "婚姻状态",
component: defineAsyncComponent(() => import("@/ui/CIVYZ5733.vue")),
remark: '查询结果为"未婚或尚未登记结婚"时,表示婚姻登记处暂无相关的登记记录。婚姻状态信息由婚姻登记处逐级上报,可能存在数据遗漏或更新滞后。当前可查询的婚姻状态包括:未婚或尚未登记结婚、已婚、离异。如您对查询结果有疑问,请联系客服反馈。',
},
JRZQ0A03:{
name: "借贷申请记录",
component: defineAsyncComponent(() => import("@/ui/CJRZQ0A03.vue")),
remark: '借贷申请记录通过分析用户在不同时间段的借贷申请行为、机构类型分布、申请频率等数据,评估用户的借贷意向强度和风险特征,帮助识别潜在的过度借贷风险。'
},
JRZQ4AA8:{
name: "偿债压力指数",
component: defineAsyncComponent(() => import("@/ui/CJRZQ4AA8.vue")),
remark: '偿债压力指数通过分析用户的债务规模、还款能力、收入水平等因素,计算并评估用户的偿债压力等级,帮助金融机构评估用户的还款能力和违约风险。'
},
JRZQ8203:{
name: "借贷行为记录",
component: defineAsyncComponent(() => import("@/ui/CJRZQ8203.vue")),
remark: '借贷行为记录通过分析用户的借款行为、还款行为、时间趋势等多维度数据,全面评估用户的借贷行为特征和信用表现,帮助识别异常借贷模式和潜在风险。'
},
QCXG7A2B:{
name: "名下车辆",
component: defineAsyncComponent(() => import("@/ui/CQCXG7A2B.vue")),
remark: '名下车辆查询结果来源于车辆管理部门等权威机构,包括各类车辆登记记录。查询结果仅供参考,具体信息以相关部门官方记录为准。'
},
IVYZ3P9M: {
name: "学历信息",
component: defineAsyncComponent(() => import("@/ui/IVYZ3P9M.vue")),
remark: '学历信息展示学生姓名、身份证号、学校、专业、入学与毕业时间、学历层次以及学习形式等字段,可结合字典编码了解具体含义。',
},
// 特殊名单验证B
JRZQ8A2D: {
@@ -429,14 +479,24 @@ const featureRiskLevels = {
'JRZQ8A2D': 9, // 特殊名单验证
'JRZQ7F1A': 8, // 全景雷达
'JRZQ6F2A': 7, // 借贷意向验证A
'JRZQ5E9F': 8, // 借选指数评估
'JRZQ0A03': 7, // 借贷意向验证
'JRZQ8203': 7, // 借贷行为验证
'JRZQ4AA8': 6, // 偿债压力指数
'FLXG3D56': 9, // 特殊名单验证
'YYSY7D3E': 5, // 手机携号转网
'YYSY8B1C': 5, // 手机在网时长
// 🟡 中风险类 - 权重 5
'QYGL3F8E': 5, // 人企关系加强版
'QCXG9P1C': 5, // 名下车辆
'QCXG7A2B': 3, // 名下车辆(简化版)
'JRZQ09J8': 5, // 收入评估
'JRZQ8B3C': 5, // 个人消费能力等级
'IVYZ3P9M': 4, // 学籍学历核验(实时版)
'FLXGDEA9': 15, // 公安不良人员名单(加强版)
'IVYZ81NC': 3, // 单人婚姻查询(登记时间版)
'IVYZ5733': 3, // 单人婚姻状态A
// 📊 复合报告类 - 按子模块动态计算
'DWBG8B4D': 0, // 谛听多维报告(由子模块计算)

80
src/components/LTable.vue Normal file
View File

@@ -0,0 +1,80 @@
<script setup>
import { computed, onMounted } from "vue";
// 接收表格数据和类型的 props
const props = defineProps({
data: {
type: Array,
required: true,
},
type: {
type: String,
default: "purple-pink", // 默认渐变颜色
},
});
// 根据 type 设置不同的渐变颜色(偶数行)
const evenClass = computed(() => {
// 统一使用主题色浅色背景
return "bg-red-50/40";
});
// 动态计算表头的背景颜色和文本颜色
const headerClass = computed(() => {
// 统一使用主题色浅色背景
return "bg-red-100";
});
// 斑马纹样式,偶数行带颜色,奇数行没有颜色,且从第二行开始
function zebraClass(index) {
return index % 2 === 1 ? evenClass.value : "";
}
</script>
<template>
<div class="l-table overflow-x-auto">
<table
class="min-w-full border-collapse table-auto text-center text-size-xs"
>
<thead :class="headerClass">
<tr>
<!-- 插槽渲染表头 -->
<slot name="header" />
</tr>
</thead>
<tbody>
<tr
v-for="(row, index) in props.data"
:key="index"
:class="zebraClass(index)"
class="border-t"
>
<slot :row="row" />
</tr>
</tbody>
</table>
</div>
</template>
<style scoped>
/* 基础表格样式 */
th {
font-weight: bold;
padding: 12px;
text-align: left;
border: 1px solid #e5e7eb;
}
/* 表格行样式 */
td {
padding: 12px;
border: 1px solid #e5e7eb;
}
table {
width: 100%;
border-spacing: 0;
}
.l-table {
@apply rounded-xl;
overflow: hidden;
}
</style>

View File

@@ -0,0 +1,27 @@
<script setup>
// 接收 props
const props = defineProps({
title: String,
})
const titleClass = computed(() => {
// 统一使用主题色
return 'bg-primary'
})
</script>
<template>
<div class="relative">
<!-- 标题部分 -->
<div :class="titleClass" class="inline-block rounded-lg px-2 py-1 text-white font-bold shadow-md">
{{ title }}
</div>
<!-- 左上角修饰 -->
<div
class="absolute left-0 top-0 h-4 w-4 transform rounded-full bg-white shadow-md -translate-x-2 -translate-y-2" />
</div>
</template>
<style scoped></style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

9745
src/example.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,747 @@
<script setup>
const props = defineProps({
data: {
type: Object,
default: () => ({})
},
apiId: {
type: String,
default: '',
},
index: {
type: Number,
default: 0,
},
notifyRiskStatus: {
type: Function,
default: () => { },
},
})
// 风险等级转换为文字描述
const riskLevelText = (level, type) => {
if (type === 'black_gray_level') {
const levels = {
'': '无风险',
1: '低风险',
2: '中等风险',
3: '高风险',
4: '极高风险',
}
return levels[level] || '未知风险'
} else if (type === 'telefraud_level') {
const levels = {
0: '无风险',
1: '极低风险',
2: '低风险',
3: '中低风险',
4: '中等风险',
5: '高风险',
6: '极高风险',
}
return levels[level] || '未知风险'
} else if (type === 'frg_list_level') {
if (level >= '3' && level <= '5') return '低风险团伙'
if (level >= '6' && level <= '7') return '中风险团伙'
if (level >= '8' && level <= '10') return '高风险团伙'
return '无风险'
} else if (type === 'risk_level') {
const levels = {
A: '无风险',
F: '低风险',
C: '中风险',
D: '中风险',
B: '高风险',
E: '高风险',
}
return levels[level] || '未知风险'
} else if (type === 'gaming') {
const levelNum = parseInt(level)
if (levelNum === 0) return '无风险'
if (levelNum > 0 && levelNum <= 20) return '极低风险'
if (levelNum > 20 && levelNum <= 40) return '低风险'
if (levelNum > 40 && levelNum <= 60) return '中等风险'
if (levelNum > 60 && levelNum <= 80) return '高风险'
if (levelNum > 80) return '极高风险'
return '未知风险'
}
return '未知风险'
}
// 风险等级转换为颜色
const riskLevelColor = (level, type) => {
if (type === 'black_gray_level') {
if (level === '' || level === '1') return 'bg-gradient-to-r from-emerald-400 to-teal-500'
if (level === '2') return 'bg-gradient-to-r from-amber-400 to-yellow-500'
if (level === '3') return 'bg-gradient-to-r from-orange-400 to-amber-600'
if (level === '4') return 'bg-gradient-to-r from-rose-400 to-red-500'
return 'bg-gradient-to-r from-gray-400 to-gray-500'
} else if (type === 'telefraud_level') {
if (level === '0') return 'bg-gradient-to-r from-emerald-400 to-teal-500'
if (level === '1' || level === '2') return 'bg-gradient-to-r from-teal-300 to-green-400'
if (level === '3' || level === '4') return 'bg-gradient-to-r from-amber-400 to-yellow-500'
if (level === '5') return 'bg-gradient-to-r from-orange-400 to-amber-600'
if (level === '6') return 'bg-gradient-to-r from-rose-400 to-red-500'
return 'bg-gradient-to-r from-gray-400 to-gray-500'
} else if (type === 'frg_list_level') {
if (level >= '3' && level <= '5') return 'bg-gradient-to-r from-emerald-400 to-teal-500'
if (level >= '6' && level <= '7') return 'bg-gradient-to-r from-amber-400 to-yellow-500'
if (level >= '8' && level <= '10') return 'bg-gradient-to-r from-rose-400 to-red-500'
return 'bg-gradient-to-r from-gray-400 to-gray-500'
} else if (type === 'risk_level') {
if (level === 'A') return 'bg-gradient-to-r from-emerald-400 to-teal-500'
if (level === 'F') return 'bg-gradient-to-r from-amber-400 to-yellow-500'
if (level === 'C' || level === 'D') return 'bg-gradient-to-r from-orange-400 to-amber-600'
if (level === 'B' || level === 'E') return 'bg-gradient-to-r from-rose-400 to-red-500'
return 'bg-gradient-to-r from-gray-400 to-gray-500'
} else if (type === 'gaming') {
const levelNum = parseInt(level)
if (levelNum === 0) return 'bg-gradient-to-r from-emerald-400 to-teal-500'
if (levelNum > 0 && levelNum <= 20) return 'bg-gradient-to-r from-teal-300 to-green-400'
if (levelNum > 20 && levelNum <= 40) return 'bg-gradient-to-r from-green-400 to-green-500'
if (levelNum > 40 && levelNum <= 60) return 'bg-gradient-to-r from-amber-400 to-yellow-500'
if (levelNum > 60 && levelNum <= 80) return 'bg-gradient-to-r from-orange-400 to-amber-600'
if (levelNum > 80) return 'bg-gradient-to-r from-rose-400 to-red-500'
return 'bg-gradient-to-r from-gray-400 to-gray-500'
}
return 'bg-gradient-to-r from-gray-400 to-gray-500'
}
// 根据风险类型获取名称
const getRiskTypeName = type => {
const types = {
110: '疑似欺诈',
130: '疑似赌博庄家',
150: '疑似赌博玩家',
170: '疑似涉赌跑分',
}
return types[type] || '未知类型'
}
// 获取团伙规模描述
const getGroupSizeDesc = code => {
const sizes = {
a: '小规模(少于50人)',
b: '中等规模(50-100人)',
c: '大规模(100-500人)',
d: '超大规模(500人以上)',
}
return sizes[code] || '未知规模'
}
// 获取风险图标
const getRiskIcon = type => {
switch (type) {
case '110':
return 'fa-exclamation-triangle'
case '130':
return 'fa-dice'
case '150':
return 'fa-gamepad'
case '170':
return 'fa-money-bill-wave'
default:
return 'fa-question-circle'
}
}
// 获取不良记录详情
const getRiskLevelDetail = level => {
switch (level) {
case 'A':
return '无任何不良记录'
case 'F':
return '涉稳、寻衅滋事'
case 'C':
case 'D':
return '吸毒、涉毒、犯罪前科'
case 'B':
case 'E':
return '涉案人员、在逃、犯罪嫌疑人'
default:
return '未知记录'
}
}
// 风险评估总结
const getRiskSummary = () => {
if (!props.data) return { text: '无法评估风险', level: 'low', color: 'text-gray-500' }
let highRiskCount = 0
let mediumRiskCount = 0
// 检查黑灰产等级
if (props.data.black_gray_level && parseInt(props.data.black_gray_level) > 2) {
highRiskCount++
} else if (props.data.black_gray_level && parseInt(props.data.black_gray_level) === 2) {
mediumRiskCount++
}
// 检查电诈风险
if (props.data.telefraud_level && parseInt(props.data.telefraud_level) > 4) {
highRiskCount++
} else if (props.data.telefraud_level && parseInt(props.data.telefraud_level) > 2) {
mediumRiskCount++
}
// 检查团伙欺诈
if (
props.data.fraud_group &&
props.data.fraud_group.frg_list_level &&
parseInt(props.data.fraud_group.frg_list_level) > 7
) {
highRiskCount++
} else if (
props.data.fraud_group &&
props.data.fraud_group.frg_list_level &&
parseInt(props.data.fraud_group.frg_list_level) > 5
) {
mediumRiskCount++
}
// 检查风险等级
if (props.data.risk_level && props.data.risk_level.risk_level) {
if (['B', 'E'].includes(props.data.risk_level.risk_level)) {
highRiskCount++
} else if (['C', 'D'].includes(props.data.risk_level.risk_level)) {
mediumRiskCount++
} else if (props.data.risk_level.risk_level === 'F') {
// 低风险,不增加计数
}
}
// 检查反诈反赌核验
if (props.data.anti_fraud_gaming) {
props.data.anti_fraud_gaming.forEach(item => {
const levelNum = parseInt(item.riskLevel)
if (levelNum > 60) {
highRiskCount++
} else if (levelNum > 40) {
mediumRiskCount++
}
})
}
if (highRiskCount > 0) {
return {
text: '该用户存在较高风险行为,建议进行进一步核实和监控',
level: 'high',
color: 'text-red-500',
}
} else if (mediumRiskCount > 0) {
return {
text: '该用户存在一定风险行为,建议提高警惕',
level: 'medium',
color: 'text-yellow-500',
}
} else {
return {
text: '该用户行为正常,风险较低',
level: 'low',
color: 'text-green-500',
}
}
}
const summary = getRiskSummary()
// 计算风险评分0-100分分数越高越安全
const riskScore = computed(() => {
// 计算总风险项数量
const totalRiskCount = Object.values(summary).reduce((sum, item) => sum + item.count, 0);
// 根据风险项数量计算评分
// 0项100分最安全
// 1-2项80分较安全
// 3-5项60分中等风险
// 6-10项40分较高风险
// 10项以上20分高风险
if (totalRiskCount === 0) return 100;
if (totalRiskCount <= 2) return 80;
if (totalRiskCount <= 5) return 60;
if (totalRiskCount <= 10) return 40;
return 20;
});
// 使用 composable 通知父组件风险评分
useRiskNotifier(props, riskScore);
// 暴露给父组件
defineExpose({
riskScore
});
</script>
<template>
<div class="card main-card">
<div v-if="!data || Object.keys(data).length === 0" class="py-4 text-center text-gray-500">
暂无风险行为扫描数据
</div>
<div v-else class="risk-content">
<!-- 风险总结 -->
<div class="summary-card" :class="{
'border-red-500 glow-red': summary.level === 'high',
'border-yellow-500 glow-yellow': summary.level === 'medium',
'border-green-500 glow-green': summary.level === 'low',
}">
<div class="flex items-center">
<div class="summary-icon" :class="summary.color">
<i class="fas" :class="summary.level === 'high'
? 'fa-exclamation-triangle'
: summary.level === 'medium'
? 'fa-exclamation-circle'
: 'fa-check-circle'
"></i>
</div>
<div class="font-bold text-lg" :class="summary.color">风险评估总结</div>
</div>
<div class="mt-1 text-gray-700">{{ summary.text }}</div>
</div>
<div class="grid-container">
<!-- 左侧列 -->
<div class="grid-left">
<!-- 黑灰产等级 -->
<!-- <div class="risk-section hover-lift">
<div class="section-title flex items-center">
<div class="title-icon bg-indigo-100 text-indigo-600">
<i class="fas fa-user-secret"></i>
</div>
<span>黑灰产等级</span>
</div>
<div class="section-content">
<div class="risk-level-indicator">
<div class="indicator-label">风险等级</div>
<div class="indicator-bar">
<div class="indicator-value" :class="riskLevelColor(data.black_gray_level || '', 'black_gray_level')"
:style="{
width: data.black_gray_level ? `${Math.min(parseInt(data.black_gray_level) * 25, 100)}%` : '0%',
}"></div>
</div>
<div class="indicator-text" :class="{
'text-green-500': (data.black_gray_level || '') === '' || (data.black_gray_level || '') === '1',
'text-yellow-500': (data.black_gray_level || '') === '2',
'text-orange-500': (data.black_gray_level || '') === '3',
'text-red-500': (data.black_gray_level || '') === '4',
}">
{{ riskLevelText(data.black_gray_level || '', 'black_gray_level') }}
</div>
</div>
<div class="description">黑灰产等级评估用户是否参与非法活动等级越高风险越大</div>
</div>
</div> -->
<!-- 电诈风险预警 -->
<!-- <div class="risk-section hover-lift">
<div class="section-title flex items-center">
<div class="title-icon bg-red-100 text-red-600">
<i class="fas fa-phone-slash"></i>
</div>
<span>电诈风险预警</span>
</div>
<div class="section-content">
<div class="risk-level-indicator">
<div class="indicator-label">风险等级</div>
<div class="indicator-bar">
<div class="indicator-value" :class="riskLevelColor(data.telefraud_level || '0', 'telefraud_level')"
:style="{ width: `${Math.min(parseInt(data.telefraud_level || '0') * 16.6, 100)}%` }"></div>
</div>
<div class="indicator-text" :class="{
'text-green-500':
(data.telefraud_level || '0') === '0' ||
(data.telefraud_level || '0') === '1' ||
(data.telefraud_level || '0') === '2',
'text-yellow-500': (data.telefraud_level || '0') === '3' || (data.telefraud_level || '0') === '4',
'text-orange-500': (data.telefraud_level || '0') === '5',
'text-red-500': (data.telefraud_level || '0') === '6',
}">
{{ riskLevelText(data.telefraud_level || '0', 'telefraud_level') }}
</div>
</div>
<div class="description">电诈风险预警评估用户是否涉及电信诈骗活动值越大风险越高</div>
</div>
</div> -->
<!-- 综合风险等级 -->
<!-- <div class="risk-section hover-lift">
<div class="section-title flex items-center">
<div class="title-icon bg-emerald-100 text-emerald-600">
<i class="fas fa-shield-alt"></i>
</div>
<span>不良个人核查</span>
</div>
<div class="section-content">
<div v-if="data.risk_level" class="flex items-center justify-center py-3">
<div
class="risk-level-badge"
:class="{
'bg-green-100 text-green-700 badge-pulse-green': data.risk_level.risk_level === 'A',
'bg-yellow-100 text-yellow-700 badge-pulse-yellow': data.risk_level.risk_level === 'F',
'bg-orange-100 text-orange-700 badge-pulse-orange': ['C', 'D'].includes(data.risk_level.risk_level),
'bg-red-100 text-red-700 badge-pulse-red': ['B', 'E'].includes(data.risk_level.risk_level),
}"
>
<span class="text-xl font-bold">{{
riskLevelText(data.risk_level.risk_level || 'A', 'risk_level')
}}</span>
</div>
<div class="ml-4 text-sm">
<div class="font-medium">详情:</div>
<div
class="mt-1"
:class="{
'text-green-600': data.risk_level.risk_level === 'A',
'text-yellow-600': data.risk_level.risk_level === 'F',
'text-orange-600': ['C', 'D'].includes(data.risk_level.risk_level),
'text-red-600': ['B', 'E'].includes(data.risk_level.risk_level),
}"
>
{{ getRiskLevelDetail(data.risk_level.risk_level || 'A') }}
</div>
</div>
</div>
<div v-else class="text-center py-2 text-gray-500">暂无不良个人核查数据</div>
<div class="description">不良个人核查评估用户的风险状况从无风险到高风险分级</div>
</div>
</div> -->
</div>
<div class="grid-right">
<!-- <div class="risk-section hover-lift">
<div class="section-title flex items-center">
<div class="title-icon bg-amber-100 text-amber-600">
<i class="fas fa-users-slash"></i>
</div>
<span>团伙欺诈排查</span>
</div>
<div class="section-content">
<div v-if="data.fraud_group" class="flex flex-col md:flex-row gap-3">
<div class="risk-level-indicator flex-1">
<div class="indicator-label">团伙风险等级</div>
<div class="indicator-bar">
<div class="indicator-value"
:class="riskLevelColor(data.fraud_group.frg_list_level || '3', 'frg_list_level')" :style="{
width: `${Math.min((parseInt(data.fraud_group.frg_list_level || '3') - 2) * 12.5, 100)}%`,
}"></div>
</div>
<div class="indicator-text" :class="{
'text-green-500': parseInt(data.fraud_group.frg_list_level || '3') <= 5,
'text-yellow-500':
parseInt(data.fraud_group.frg_list_level || '3') >= 6 &&
parseInt(data.fraud_group.frg_list_level || '3') <= 7,
'text-red-500': parseInt(data.fraud_group.frg_list_level || '3') >= 8,
}">
{{ riskLevelText(data.fraud_group.frg_list_level || '3', 'frg_list_level') }}
</div>
</div>
<div class="group-size flex-1">
<div class="font-medium text-gray-700">团伙规模</div>
<div class="mt-2 flex items-center">
<i class="fas fa-users text-blue-500 mr-2 text-xl"></i>
<span>{{ getGroupSizeDesc(data.fraud_group.frg_group_num || 'a') }}</span>
</div>
</div>
</div>
<div v-else class="text-center py-2 text-gray-500">暂无团伙欺诈数据</div>
<div class="description mt-1">团伙欺诈排查评估用户是否属于欺诈团伙及团伙规模大小</div>
</div>
</div> -->
<div class="risk-section hover-lift">
<div class="section-title flex items-center">
<div class="title-icon bg-purple-100 text-purple-600">
<i class="fas fa-dice-slash"></i>
</div>
<span>反诈反赌核验</span>
</div>
<div class="section-content">
<div v-if="data.anti_fraud_gaming && data.anti_fraud_gaming.length > 0" class="grid grid-cols-1 gap-3">
<div v-for="(item, index) in data.anti_fraud_gaming" :key="index" class="gaming-item" :class="parseInt(item.riskLevel) === 0
? 'border-green-500'
: parseInt(item.riskLevel) < 4
? 'border-green-400'
: parseInt(item.riskLevel) < 7
? 'border-yellow-500'
: 'border-red-500'
">
<div class="gaming-icon" :class="parseInt(item.riskLevel) === 0
? 'bg-green-100 text-green-500'
: parseInt(item.riskLevel) < 4
? 'bg-green-100 text-green-500'
: parseInt(item.riskLevel) < 7
? 'bg-yellow-100 text-yellow-600'
: 'bg-red-100 text-red-500'
">
<i class="fas" :class="getRiskIcon(item.riskType)"></i>
</div>
<div class="flex-1">
<div class="font-medium text-sm">{{ getRiskTypeName(item.riskType) }}</div>
<div class="flex items-center mt-2">
<div class="progress-container">
<div class="progress-bar" :class="riskLevelColor(item.riskLevel, 'gaming')"
:style="{ width: `${Math.min(parseInt(item.riskLevel), 100)}%` }"></div>
</div>
<span class="risk-level-text" :class="{
'text-green-500': parseInt(item.riskLevel) <= 20,
'text-green-600': parseInt(item.riskLevel) > 20 && parseInt(item.riskLevel) <= 40,
'text-yellow-500': parseInt(item.riskLevel) > 40 && parseInt(item.riskLevel) <= 60,
'text-orange-500': parseInt(item.riskLevel) > 60 && parseInt(item.riskLevel) <= 80,
'text-red-500': parseInt(item.riskLevel) > 80,
}">
{{ riskLevelText(item.riskLevel, 'gaming') }}
</span>
</div>
</div>
</div>
</div>
<div v-else class="text-center py-2 text-gray-500">暂无反诈反赌核验数据</div>
<div class="description mt-1">反诈反赌核验评估用户是否有涉及诈骗或赌博活动的风险</div>
</div>
</div>
<div class="security-tips hover-lift">
<div class="flex items-center">
<div class="title-icon bg-blue-100 text-blue-600 mr-2">
<i class="fas fa-lightbulb"></i>
</div>
<div class="font-bold text-blue-700">安全建议</div>
</div>
<div class="tip-list">
<div class="tip-item">
<i class="fas fa-check-circle text-green-500 mr-1"></i>
<span>定期更新密码使用复杂且不易猜测的密码</span>
</div>
<div class="tip-item">
<i class="fas fa-check-circle text-green-500 mr-1"></i>
<span>开启双因素认证提高账户安全性</span>
</div>
<div class="tip-item">
<i class="fas fa-check-circle text-green-500 mr-1"></i>
<span>不点击来源不明的链接或下载不明文件</span>
</div>
<div class="tip-item">
<i class="fas fa-check-circle text-green-500 mr-1"></i>
<span>不向陌生人透露个人敏感信息</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.main-card {
@apply bg-white shadow-md rounded-xl p-4 mb-3 border border-gray-100;
}
.risk-content {
@apply space-y-4;
}
.grid-container {
@apply grid grid-cols-1 md:grid-cols-2 gap-4;
}
.grid-left,
.grid-right {
@apply flex flex-col gap-4;
}
.summary-card {
@apply p-4 rounded-xl shadow-sm bg-gradient-to-br from-sky-50 to-indigo-100 border-l-4 transition-all duration-300;
}
.glow-red {
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.15);
border-color: rgba(239, 68, 68, 0.6);
}
.glow-yellow {
box-shadow: 0 4px 12px rgba(245, 158, 11, 0.15);
border-color: rgba(245, 158, 11, 0.6);
}
.glow-green {
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.15);
border-color: rgba(16, 185, 129, 0.6);
}
.summary-icon {
@apply mr-2 text-xl;
}
.risk-section {
@apply bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden transition-all duration-300;
}
.hover-lift:hover {
transform: translateY(-3px);
box-shadow:
0 8px 16px -2px rgba(0, 0, 0, 0.1),
0 4px 8px -2px rgba(0, 0, 0, 0.05);
}
.section-title {
@apply bg-gradient-to-r from-gray-50 to-gray-100 px-4 py-3 font-bold text-gray-700 border-b border-gray-200 flex items-center;
}
.title-icon {
@apply w-7 h-7 rounded-full flex items-center justify-center mr-3 shadow-sm;
}
.section-content {
@apply p-4;
}
.risk-level-indicator {
@apply mb-2;
}
.indicator-label {
@apply text-gray-700 font-medium mb-1 text-sm;
}
.indicator-bar {
@apply w-full bg-gray-200 rounded-full h-3 overflow-hidden shadow-inner;
}
.indicator-value {
@apply h-3 rounded-full transition-all duration-500;
}
.indicator-text {
@apply mt-1 font-medium text-sm;
}
.description {
@apply text-xs text-gray-500 mt-2 italic;
}
.risk-level-badge {
@apply flex flex-col items-center justify-center w-20 h-20 rounded-full shadow-md border transition-transform duration-300 backdrop-blur-sm;
}
.badge-pulse-green {
background: linear-gradient(135deg, rgba(16, 185, 129, 0.15), rgba(5, 150, 105, 0.3));
border-color: rgba(5, 150, 105, 0.4);
animation: pulse-green 3s infinite;
}
.badge-pulse-yellow {
background: linear-gradient(135deg, rgba(245, 158, 11, 0.15), rgba(217, 119, 6, 0.3));
border-color: rgba(217, 119, 6, 0.4);
animation: pulse-yellow 3s infinite;
}
.badge-pulse-orange {
background: linear-gradient(135deg, rgba(249, 115, 22, 0.15), rgba(234, 88, 12, 0.3));
border-color: rgba(234, 88, 12, 0.4);
animation: pulse-orange 3s infinite;
}
.badge-pulse-red {
background: linear-gradient(135deg, rgba(239, 68, 68, 0.15), rgba(220, 38, 38, 0.3));
border-color: rgba(220, 38, 38, 0.4);
animation: pulse-red 3s infinite;
}
@keyframes pulse-green {
0% {
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.3);
}
70% {
box-shadow: 0 0 0 8px rgba(16, 185, 129, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0);
}
}
@keyframes pulse-yellow {
0% {
box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.3);
}
70% {
box-shadow: 0 0 0 8px rgba(245, 158, 11, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(245, 158, 11, 0);
}
}
@keyframes pulse-orange {
0% {
box-shadow: 0 0 0 0 rgba(249, 115, 22, 0.3);
}
70% {
box-shadow: 0 0 0 8px rgba(249, 115, 22, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(249, 115, 22, 0);
}
}
@keyframes pulse-red {
0% {
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.3);
}
70% {
box-shadow: 0 0 0 8px rgba(239, 68, 68, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0);
}
}
.group-size {
@apply bg-gradient-to-br from-gray-50 to-gray-100 p-3 rounded-lg shadow-sm;
}
.gaming-item {
@apply flex items-center bg-white shadow-sm rounded-lg p-3 border-l-2 transition-all duration-300;
}
.gaming-item:hover {
@apply shadow-md;
transform: scale(1.01);
}
.gaming-icon {
@apply w-9 h-9 flex items-center justify-center rounded-full mr-3 shadow-sm;
}
.progress-container {
@apply w-full bg-gray-200 rounded-full h-3 mr-3 flex-1 shadow-inner;
}
.progress-bar {
@apply h-3 rounded-full transition-all duration-500;
}
.risk-level-text {
@apply text-xs whitespace-nowrap min-w-[3.5rem] text-right font-semibold;
}
.security-tips {
@apply bg-gradient-to-br from-sky-50 to-indigo-100 rounded-xl p-4 shadow-sm border border-blue-200;
}
.tip-list {
@apply mt-3 space-y-2;
}
.tip-item {
@apply flex items-start text-sm text-gray-700 bg-white p-2 rounded-lg shadow-sm border border-gray-100;
}
</style>

1007
src/ui/CFLXG3D56.vue Normal file

File diff suppressed because it is too large Load Diff

666
src/ui/CFLXGDEA9.vue Normal file
View File

@@ -0,0 +1,666 @@
<template>
<div class="personal-bad-record card">
<!-- 空数据提示 -->
<div v-if="!hasData" class="py-8 text-center text-gray-500">
<div class="flex flex-col items-center">
<div class="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mb-3">
<span class="text-2xl text-gray-400">📋</span>
</div>
<p>暂无本人不良数据</p>
</div>
</div>
<div v-else class="space-y-4">
<!-- 风险总览 -->
<div class="">
<div class="flex items-center mb-3">
<div class="w-12 h-12">
<img src="@/assets/images/report/gazdryhycp.png" alt="本人不良记录" class="w-8 h-8 object-contain" />
</div>
<div>
<h2 class="text-lg font-bold text-gray-800">风险总览</h2>
<p class="text-sm text-[#999999]">本人不良记录风险评估</p>
</div>
</div>
<!-- 风险统计 -->
<div class="bg-[#FFF0F0] border border-red-200 rounded-lg p-4" v-if="!isNormalPerson">
<div class="grid grid-cols-3 gap-4">
<div class="text-center">
<div class="text-sm text-[#666666] mb-1">总风险点</div>
<div class="text-lg font-bold text-[#E53935]">{{ hitRiskTypes.length }}</div>
</div>
<div class="text-center">
<div class="text-sm text-[#666666] mb-1">高风险</div>
<div class="text-lg font-bold text-[#E53935]">{{ getHighRiskCount() }}</div>
</div>
<div class="text-center">
<div class="text-sm text-[#666666] mb-1">中风险</div>
<div class="text-lg font-bold text-[#FFC107]">{{ getMiddleRiskCount() }}</div>
</div>
</div>
</div>
<!-- 正常人员显示 -->
<div class="bg-[#F0FFF0] border border-green-200 rounded-lg p-4" v-else>
<div class="flex items-center justify-center">
<div class="w-10 h-10 mr-4">
<img src="@/assets/images/report/zq.png" alt="暂无风险" class="w-10 h-10 object-contain" />
</div>
<div class="flex-1">
<div class="font-bold text-[#333333] text-center">正常人员</div>
<div class="text-sm text-[#999999] text-center">无不良记录属于正常人员</div>
</div>
</div>
</div>
</div>
<!-- 所有风险类型列表 -->
<div class="space-y-3">
<!-- 正常人员 -->
<div class="rounded-lg p-4 border-2 relative" :class="getRiskItemClass('0')">
<div
:class="['absolute top-0 right-0 px-1.5 py-0.5 text-sm font-bold text-white rounded-bl-lg rounded-tr-lg', getRiskBadgeClass('0')]">
{{ getNormalPersonBadgeText() }}
</div>
<div class="flex items-center pr-12">
<div class="w-8 h-8 mr-3 flex-shrink-0 flex items-center justify-center">
<img :src="getRiskItemIcon('0')" alt="正常人员" class="w-8 h-8 object-contain" />
</div>
<div class="flex-1">
<div class="font-bold text-sm" :class="getRiskItemTextColor('0')">
{{ getRiskTypeInfo('0').text }}
</div>
<div class="text-sm text-[#999999] mt-0.5">{{ getRiskTypeInfo('0').description }}</div>
</div>
</div>
</div>
<!-- A类侵犯公民人身权利民主权利 -->
<div class="risk-group">
<div class="text-sm font-semibold text-gray-700 mb-2 px-2">A类侵犯公民人身权利民主权利</div>
<div class="space-y-2">
<div v-for="code in ['A', 'A1', 'A2', 'A3', 'A4', 'A5']" :key="code"
class="rounded-lg p-4 border-2 relative" :class="getRiskItemClass(code)">
<div
:class="['absolute top-0 right-0 px-1.5 py-0.5 text-sm font-bold text-white rounded-bl-lg rounded-tr-lg', getRiskBadgeClass(code)]">
{{ isHit(code) ? '异常' : '正常' }}
</div>
<div class="flex items-center pr-12">
<div class="w-8 h-8 mr-3 flex-shrink-0 flex items-center justify-center">
<img :src="getRiskItemIcon(code)" :alt="getRiskTypeInfo(code).text"
class="w-8 h-8 object-contain" />
</div>
<div class="flex-1">
<div class="font-bold text-sm" :class="getRiskItemTextColor(code)">
{{ getRiskTypeInfo(code).text }}
</div>
<div class="text-sm text-[#999999] mt-0.5">{{ getRiskTypeInfo(code).description }}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- B类经济类前科 -->
<div class="risk-group">
<div class="text-sm font-semibold text-gray-700 mb-2 px-2">B类经济类前科</div>
<div class="space-y-2">
<div v-for="code in ['B', 'B1', 'B2', 'B3', 'B4', 'B5']" :key="code"
class="rounded-lg p-4 border-2 relative" :class="getRiskItemClass(code)">
<div
:class="['absolute top-0 right-0 px-1.5 py-0.5 text-sm font-bold text-white rounded-bl-lg rounded-tr-lg', getRiskBadgeClass(code)]">
{{ isHit(code) ? '异常' : '正常' }}
</div>
<div class="flex items-center pr-12">
<div class="w-8 h-8 mr-3 flex-shrink-0 flex items-center justify-center">
<img :src="getRiskItemIcon(code)" :alt="getRiskTypeInfo(code).text"
class="w-8 h-8 object-contain" />
</div>
<div class="flex-1">
<div class="font-bold text-sm" :class="getRiskItemTextColor(code)">
{{ getRiskTypeInfo(code).text }}
</div>
<div class="text-sm text-[#999999] mt-0.5">{{ getRiskTypeInfo(code).description }}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- C类妨害社会管理秩序 -->
<div class="risk-group">
<div class="text-sm font-semibold text-gray-700 mb-2 px-2">C类妨害社会管理秩序</div>
<div class="space-y-2">
<div v-for="code in ['C', 'C1', 'C2', 'C3', 'C4', 'C5']" :key="code"
class="rounded-lg p-4 border-2 relative" :class="getRiskItemClass(code)">
<div
:class="['absolute top-0 right-0 px-1.5 py-0.5 text-sm font-bold text-white rounded-bl-lg rounded-tr-lg', getRiskBadgeClass(code)]">
{{ isHit(code) ? '异常' : '正常' }}
</div>
<div class="flex items-center pr-12">
<div class="w-8 h-8 mr-3 flex-shrink-0 flex items-center justify-center">
<img :src="getRiskItemIcon(code)" :alt="getRiskTypeInfo(code).text"
class="w-8 h-8 object-contain" />
</div>
<div class="flex-1">
<div class="font-bold text-sm" :class="getRiskItemTextColor(code)">
{{ getRiskTypeInfo(code).text }}
</div>
<div class="text-sm text-[#999999] mt-0.5">{{ getRiskTypeInfo(code).description }}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- D类重点 -->
<div class="risk-group">
<div class="text-sm font-semibold text-gray-700 mb-2 px-2">D类重点</div>
<div class="space-y-2">
<div v-for="code in ['D', 'D1', 'D2', 'D3', 'D4', 'D5']" :key="code"
class="rounded-lg p-4 border-2 relative" :class="getRiskItemClass(code)">
<div
:class="['absolute top-0 right-0 px-1.5 py-0.5 text-sm font-bold text-white rounded-bl-lg rounded-tr-lg', getRiskBadgeClass(code)]">
{{ isHit(code) ? '异常' : '正常' }}
</div>
<div class="flex items-center pr-12">
<div class="w-8 h-8 mr-3 flex-shrink-0 flex items-center justify-center">
<img :src="getRiskItemIcon(code)" :alt="getRiskTypeInfo(code).text"
class="w-8 h-8 object-contain" />
</div>
<div class="flex-1">
<div class="font-bold text-sm" :class="getRiskItemTextColor(code)">
{{ getRiskTypeInfo(code).text }}
</div>
<div class="text-sm text-[#999999] mt-0.5">{{ getRiskTypeInfo(code).description }}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- E类涉交通案件 -->
<div class="risk-group">
<div class="text-sm font-semibold text-gray-700 mb-2 px-2">E类涉交通案件</div>
<div class="space-y-2">
<div class="rounded-lg p-4 border-2 relative" :class="getRiskItemClass('E')">
<div
:class="['absolute top-0 right-0 px-1.5 py-0.5 text-sm font-bold text-white rounded-bl-lg rounded-tr-lg', getRiskBadgeClass('E')]">
{{ isHit('E') ? '异常' : '正常' }}
</div>
<div class="flex items-center pr-12">
<div class="w-8 h-8 mr-3 flex-shrink-0 flex items-center justify-center">
<img :src="getRiskItemIcon('E')" :alt="getRiskTypeInfo('E').text"
class="w-8 h-8 object-contain" />
</div>
<div class="flex-1">
<div class="font-bold text-sm" :class="getRiskItemTextColor('E')">
{{ getRiskTypeInfo('E').text }}
</div>
<div class="text-sm text-[#999999] mt-0.5">{{ getRiskTypeInfo('E').description }}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- F类法院文书 -->
<div class="risk-group">
<div class="text-sm font-semibold text-gray-700 mb-2 px-2">F类法院文书</div>
<div class="space-y-2">
<div class="rounded-lg p-4 border-2 relative" :class="getRiskItemClass('F')">
<div
:class="['absolute top-0 right-0 px-1.5 py-0.5 text-sm font-bold text-white rounded-bl-lg rounded-tr-lg', getRiskBadgeClass('F')]">
{{ isHit('F') ? '异常' : '正常' }}
</div>
<div class="flex items-center pr-12">
<div class="w-8 h-8 mr-3 flex-shrink-0 flex items-center justify-center">
<img :src="getRiskItemIcon('F')" :alt="getRiskTypeInfo('F').text"
class="w-8 h-8 object-contain" />
</div>
<div class="flex-1">
<div class="font-bold text-sm" :class="getRiskItemTextColor('F')">
{{ getRiskTypeInfo('F').text }}
</div>
<div class="text-sm text-[#999999] mt-0.5">{{ getRiskTypeInfo('F').description }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from "vue";
import LRemark from '@/components/LRemark.vue'
import { useRiskNotifier } from '@/composables/useRiskNotifier'
// 导入风险类型图标
import iconZfx from '@/assets/images/report/zfx.png'
import iconGfx from '@/assets/images/report/gfx.png'
import iconSafe from '@/assets/images/report/zq.png'
const props = defineProps({
data: {
type: Object,
default: () => ({}),
},
params: {
type: Object,
default: () => ({}),
},
apiId: {
type: String,
default: '',
},
index: {
type: Number,
default: 0,
},
notifyRiskStatus: {
type: Function,
default: () => { },
},
})
// 风险类型代码映射
const riskTypeMap = {
'0': {
text: '正常人员',
description: '无不良记录,属于正常人员',
level: 'normal',
riskLevel: '正常'
},
'A': {
text: '前科:侵犯公民人身权利,民主权利',
description: '存在侵犯公民人身权利、民主权利的前科记录(在逃,盗窃、诈骗、抢劫、故意伤害、强奸等在刑或前科等)',
level: 'high',
riskLevel: '高风险'
},
'A1': {
text: '盗窃',
description: '存在盗窃前科记录',
level: 'high',
riskLevel: '高风险'
},
'A2': {
text: '诈骗',
description: '存在诈骗前科记录',
level: 'high',
riskLevel: '高风险'
},
'A3': {
text: '抢劫/夺',
description: '存在抢劫、抢夺前科记录',
level: 'high',
riskLevel: '高风险'
},
'A4': {
text: '故意伤害/杀人',
description: '存在故意伤害、杀人前科记录',
level: 'high',
riskLevel: '高风险'
},
'A5': {
text: '强奸/性侵/猥亵',
description: '存在强奸、性侵、猥亵前科记录',
level: 'high',
riskLevel: '高风险'
},
'B': {
text: '经济类前科',
description: '存在破坏金融秩序、非法吸存、违发贷款、金融诈骗、集资诈骗、保险诈骗、假币等在刑或前科等',
level: 'medium',
riskLevel: '中风险'
},
'B1': {
text: '走私',
description: '存在走私前科记录',
level: 'medium',
riskLevel: '中风险'
},
'B2': {
text: '破坏金融管理秩序',
description: '存在破坏金融管理秩序前科记录',
level: 'medium',
riskLevel: '中风险'
},
'B3': {
text: '金融诈骗',
description: '存在金融诈骗前科记录',
level: 'medium',
riskLevel: '中风险'
},
'B4': {
text: '洗钱',
description: '存在洗钱前科记录',
level: 'medium',
riskLevel: '中风险'
},
'B5': {
text: '偷漏税',
description: '存在偷漏税前科记录',
level: 'medium',
riskLevel: '中风险'
},
'C': {
text: '妨害社会管理秩序',
description: '存在扰乱公共秩序、妨害司法、妨害国境管理、妨害文物管理、涉毒、涉黄等在刑或前科等',
level: 'medium',
riskLevel: '中风险'
},
'C1': {
text: '扰乱公共秩序',
description: '存在扰乱公共秩序前科记录',
level: 'medium',
riskLevel: '中风险'
},
'C2': {
text: '妨害司法',
description: '存在妨害司法前科记录',
level: 'medium',
riskLevel: '中风险'
},
'C3': {
text: '涉毒',
description: '存在涉毒前科记录',
level: 'medium',
riskLevel: '中风险'
},
'C4': {
text: '涉黄刑案',
description: '存在涉黄刑案前科记录',
level: 'medium',
riskLevel: '中风险'
},
'C5': {
text: '帮信/掩隐/侵公',
description: '存在帮助信息网络犯罪活动、掩饰隐瞒犯罪所得、侵犯公民个人信息前科记录',
level: 'medium',
riskLevel: '中风险'
},
'D': {
text: '重点',
description: '存在危害国家、公共安全,涉恐、疆藏,涉稳、涉黑、涉及境外等',
level: 'critical',
riskLevel: '高风险'
},
'D1': {
text: '危害国家、公共安全',
description: '存在危害国家、公共安全前科记录',
level: 'critical',
riskLevel: '高风险'
},
'D2': {
text: '涉稳',
description: '存在涉稳前科记录',
level: 'critical',
riskLevel: '高风险'
},
'D3': {
text: '涉及境外',
description: '存在涉及境外前科记录',
level: 'critical',
riskLevel: '高风险'
},
'D4': {
text: '涉恐、疆藏',
description: '存在涉恐、疆藏前科记录',
level: 'critical',
riskLevel: '高风险'
},
'D5': {
text: '涉黑',
description: '存在涉黑前科记录',
level: 'critical',
riskLevel: '高风险'
},
'E': {
text: '涉交通案件',
description: '存在危险驾驶、交通肇事等交通案件前科记录',
level: 'low',
riskLevel: '低风险'
},
'F': {
text: '法院文书',
description: '存在法院文书记录',
level: 'low',
riskLevel: '低风险'
}
}
// 获取风险类型信息
const getRiskTypeInfo = (type) => {
return riskTypeMap[type] || riskTypeMap['F']
}
// 解析命中的风险代码
const hitRiskCodes = computed(() => {
const levelData = props.data?.data?.level || props.data?.level
if (!levelData) return []
const levelStr = levelData.toString()
return levelStr.split(',').map(code => code.trim()).filter(code => code)
})
// 判断是否命中某个风险代码
const isHit = (code) => {
if (code === '0') {
// 如果level是'0',则正常人员命中
return hitRiskCodes.value.includes('0')
}
// 如果直接包含该代码,则命中
if (hitRiskCodes.value.includes(code)) {
return true
}
// 对于父级类型A、B、C、D如果子类型命中父类型也算命中
if (code === 'A') {
// 如果 A1、A2、A3、A4、A5 任何一个命中A 也算命中
return ['A1', 'A2', 'A3', 'A4', 'A5'].some(subCode => hitRiskCodes.value.includes(subCode))
}
if (code === 'B') {
// 如果 B1、B2、B3、B4、B5 任何一个命中B 也算命中
return ['B1', 'B2', 'B3', 'B4', 'B5'].some(subCode => hitRiskCodes.value.includes(subCode))
}
if (code === 'C') {
// 如果 C1、C2、C3、C4、C5 任何一个命中C 也算命中
return ['C1', 'C2', 'C3', 'C4', 'C5'].some(subCode => hitRiskCodes.value.includes(subCode))
}
if (code === 'D') {
// 如果 D1、D2、D3、D4、D5 任何一个命中D 也算命中
return ['D1', 'D2', 'D3', 'D4', 'D5'].some(subCode => hitRiskCodes.value.includes(subCode))
}
return false
}
// 获取命中的风险类型列表
const hitRiskTypes = computed(() => {
return hitRiskCodes.value.filter(code => code !== '0').map(code => ({
code,
...getRiskTypeInfo(code)
}))
})
// 判断是否有数据
const hasData = computed(() => {
const levelData = props.data?.data?.level || props.data?.level
return levelData && Object.keys(props.data || {}).length > 0
})
// 判断是否为正常人员
const isNormalPerson = computed(() => {
return hitRiskCodes.value.includes('0') && hitRiskCodes.value.length === 1
})
// 获取高风险数量
const getHighRiskCount = () => {
return hitRiskTypes.value.filter(risk =>
risk.level === 'high' || risk.level === 'critical'
).length
}
// 获取中风险数量
const getMiddleRiskCount = () => {
return hitRiskTypes.value.filter(risk => risk.level === 'medium').length
}
// 获取风险项样式类
const getRiskItemClass = (code) => {
const hit = isHit(code)
const riskInfo = getRiskTypeInfo(code)
if (code === '0') {
// 正常人员:如果是正常人员显示绿色,否则显示红色(存在异常)
return isNormalPerson.value ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'
}
if (hit) {
if (riskInfo.level === 'critical' || riskInfo.level === 'high') {
return 'bg-red-50 border-red-200'
} else if (riskInfo.level === 'medium') {
return 'bg-orange-50 border-orange-200'
} else {
return 'bg-yellow-50 border-yellow-200'
}
}
return 'bg-gray-50 border-gray-200'
}
// 获取风险项文本颜色
const getRiskItemTextColor = (code) => {
const hit = isHit(code)
const riskInfo = getRiskTypeInfo(code)
if (code === '0') {
// 正常人员:如果是正常人员显示绿色,否则显示红色(存在异常)
return isNormalPerson.value ? 'text-green-600' : 'text-red-600'
}
if (hit) {
if (riskInfo.level === 'critical' || riskInfo.level === 'high') {
return 'text-red-600'
} else if (riskInfo.level === 'medium') {
return 'text-orange-600'
} else {
return 'text-yellow-600'
}
}
return 'text-gray-600'
}
// 获取风险项图标
const getRiskItemIcon = (code) => {
const hit = isHit(code)
const riskInfo = getRiskTypeInfo(code)
if (code === '0') {
// 正常人员:如果是正常人员显示安全图标,否则显示风险图标(存在异常)
return isNormalPerson.value ? iconSafe : iconGfx
}
if (hit) {
if (riskInfo.level === 'critical' || riskInfo.level === 'high') {
return iconGfx
} else {
return iconZfx
}
}
return iconSafe
}
// 获取正常人员标签文本
const getNormalPersonBadgeText = () => {
// 只有当 level 只有 '0' 时才显示"正常人员"
if (isNormalPerson.value) {
return '正常'
}
// 否则显示"存在异常"
return '存在异常'
}
// 获取风险标签样式类
const getRiskBadgeClass = (code) => {
const hit = isHit(code)
const riskInfo = getRiskTypeInfo(code)
if (code === '0') {
// 正常人员:如果是正常人员显示绿色,否则显示红色(存在异常)
return isNormalPerson.value ? 'bg-green-500' : 'bg-[#E53935]'
}
if (hit) {
if (riskInfo.level === 'critical' || riskInfo.level === 'high') {
return 'bg-[#E53935]'
} else if (riskInfo.level === 'medium') {
return 'bg-[#D6943E]'
} else {
return 'bg-yellow-500'
}
}
// 未命中显示绿色(正常)
return 'bg-green-500'
}
// 计算风险评分0-100分分数越高越安全
const riskScore = computed(() => {
if (isNormalPerson.value) {
return 100 // 正常人员,最安全
}
if (hitRiskTypes.value.length === 0) {
return 100 // 无风险数据,最安全
}
// 获取高风险和中风险数量
const highRiskCount = getHighRiskCount()
const middleRiskCount = getMiddleRiskCount()
// 高风险数量越多,分数越低
let score = 100
// 高风险扣分每个高风险扣15分
score -= highRiskCount * 15
// 中风险扣分每个中风险扣8分
score -= middleRiskCount * 8
// 确保分数在合理范围内最低20分
return Math.max(20, Math.min(100, score))
})
// 使用 composable 通知父组件风险评分
useRiskNotifier(props, riskScore)
// 暴露给父组件
defineExpose({
riskScore
})
</script>
<style lang="scss" scoped>
.personal-bad-record {
@apply space-y-4;
}
.risk-group {
@apply mb-4;
}
</style>

120
src/ui/CIVYZ5733.vue Normal file
View File

@@ -0,0 +1,120 @@
<script setup>
import LTitle from "@/components/LTitle.vue";
import { computed, watch } from "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 statusMap = {
0: {
text: "未婚或尚未登记结婚",
bgClass: "bg-yellow-100",
textClass: "text-yellow-700",
description: "未进行民政登记婚姻",
},
1: {
text: "已婚",
bgClass: "bg-green-100",
textClass: "text-green-700",
description: "已登记婚姻,家庭幸福美满",
},
2: {
text: "离异",
bgClass: "bg-red-100",
textClass: "text-red-700",
description: "离异状态,未来生活可期",
},
3: {
text: "离婚冷静期",
bgClass: "bg-blue-100",
textClass: "text-blue-700",
description: "目前处于离婚冷静期,请谨慎决策",
},
};
// 根据 `data.status` 确定当前状态,默认值为 "无相关记录"
const currentStatus =
data && data.status !== undefined
? statusMap[data.status] || statusMap["0"]
: {
text: "无相关记录",
bgClass: "bg-gray-200",
textClass: "text-gray-500",
description: "暂无婚姻相关记录",
};
// 计算风险评分0-100分分数越高越安全
const riskScore = computed(() => {
// 未婚(0)或已婚(1)100分最安全
// 离异(2)30分有风险
// 离婚冷静期(3)20分高风险
if (data?.status === 0 || data?.status === 1) return 100;
if (data?.status === 2) return 30;
if (data?.status === 3) return 20;
return 100; // 默认最安全
});
// 使用 composable 通知父组件风险评分
useRiskNotifier(props, riskScore);
// 暴露给父组件
defineExpose({
riskScore
});
</script>
<template>
<div class="card">
<div class="status-info flex flex-col items-center">
<div
:class="`status-label rounded-full px-6 py-3 text-center font-bold shadow-md ${currentStatus.bgClass} ${currentStatus.textClass}`">
{{ currentStatus.text }}
</div>
<p class="status-description mt-3 text-sm text-gray-600">
{{ currentStatus.description }}
</p>
</div>
</div>
</template>
<style lang="scss" scoped>
.status-info {
text-align: center;
}
.status-label {
font-size: 1.25rem;
padding: 0.75rem 1.5rem;
border-radius: 9999px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.status-description {
color: #4a5568;
margin-top: 0.5rem;
font-weight: 500;
}
.additional-info p {
margin-top: 0.5rem;
}
</style>

106
src/ui/CIVYZ81NC.vue Normal file
View File

@@ -0,0 +1,106 @@
<script setup>
const props = defineProps({
data: {
type: Object,
required: false,
default: () => null,
},
});
console.log("data", props.data);
// 获取实际的数据对象
const actualData = props.data?.data;
// 日期格式化函数,将 2009-04-16 转换为 2009年04月16日
const formatDate = (dateStr) => {
if (!dateStr) return "";
const [year, month, day] = dateStr.split("-");
return `${year}${month}${day}`;
};
// 状态映射,根据 op_type 判断
const statusMap = {
IA: {
text: "已婚",
bgClass: "bg-green-100",
textClass: "text-green-700",
description: "已登记婚姻,家庭幸福美满",
},
IB: {
text: "离异",
bgClass: "bg-red-100",
textClass: "text-red-700",
description: "离异状态,未来生活可期",
},
INR: {
text: "未登记",
bgClass: "bg-yellow-100",
textClass: "text-yellow-700",
description: "未进行民政登记婚姻",
},
};
// 无记录时的状态
const noRecordStatus = {
text: "无相关记录",
bgClass: "bg-gray-200",
textClass: "text-gray-500",
description: "暂无婚姻相关记录",
opDate: null,
};
// 根据 op_type 确定当前状态,默认值为 "无相关记录"
const currentStatus = !actualData
? noRecordStatus
: actualData.op_type
? { ...statusMap[actualData.op_type], opDate: formatDate(actualData.op_date) }
: noRecordStatus;
</script>
<template>
<div class="card">
<div class="status-info flex flex-col items-center">
<div
:class="`status-label rounded-full px-6 py-3 text-center font-bold shadow-md ${currentStatus.bgClass} ${currentStatus.textClass}`">
{{ currentStatus.text }}
</div>
<div v-if="currentStatus.opDate" class="op-date-container mt-4 px-4 py-2 bg-blue-50 rounded-lg border border-blue-200">
<p class="op-date text-sm font-medium text-blue-700">
登记日期{{ currentStatus.opDate }}
</p>
</div>
<p v-html="currentStatus.description" class="status-description mt-3 text-sm text-gray-600"></p>
</div>
</div>
</template>
<style lang="scss" scoped>
.status-info {
text-align: center;
}
.status-label {
font-size: 1.25rem;
padding: 0.75rem 1.5rem;
border-radius: 9999px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.status-description {
color: #4a5568;
margin-top: 0.5rem;
font-weight: 500;
}
.additional-info p {
margin-top: 0.5rem;
}
.op-date-container {
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.1);
}
.op-date-container:hover {
box-shadow: 0 4px 8px rgba(59, 130, 246, 0.15);
}
</style>

818
src/ui/CIVYZ9A2B.vue Normal file
View File

@@ -0,0 +1,818 @@
<script setup>
import { computed, watch } from 'vue';
import { useRiskNotifier } from '@/composables/useRiskNotifier';
const props = defineProps({
data: {
type: Object,
default: () => ({})
},
apiId: {
type: String,
default: '',
},
index: {
type: Number,
default: 0,
},
notifyRiskStatus: {
type: Function,
default: () => { },
},
});
onMounted(() => {
console.log("data", props.data);
});
// 计算风险评分0-100分分数越高越安全
const riskScore = computed(() => {
// 学历信息不算风险始终返回100分最安全
return 100;
});
// 使用 composable 通知父组件风险评分
useRiskNotifier(props, riskScore);
// 暴露给父组件
defineExpose({
riskScore
});
// 格式化日期从YYMM格式转换为YYYY年MM月格式
const formatDate = (dateStr) => {
if (!dateStr || dateStr.length !== 4) return "未知";
const yearShort = dateStr.substring(0, 2);
const month = dateStr.substring(2, 4);
// 假设都是21世纪的年份
const fullYear = `20${yearShort}`;
return `${fullYear}${month}`;
};
// 获取学历等级对应的描述
const getEducationDesc = (education) => {
const descriptions = {
大学专科:
"专科学历是高等教育的重要组成部分,培养具有专业知识和技能的应用型人才。",
大学本科:
"本科学历是高等教育的基础学位,培养具有系统专业知识和基本技能的高级人才。",
硕士研究生:
"硕士学位是较高层次的学位,培养具有较深厚理论基础和专业技能的高级专门人才。",
博士研究生:
"博士学位是最高学位,培养能够独立从事科学研究工作、具有创新能力的高级专门人才。",
博士后: "博士后是在获得博士学位后进行的进一步研究和深造,是学术界的高级研究人员。",
};
return descriptions[education] || "";
};
// 根据学校类型获取不同的样式类
const getSchoolTypeClass = (type) => {
const classes = {
"985学校": "border-amber-500 bg-amber-50",
"211学校": "border-blue-500 bg-blue-50",
双一流学校: "border-green-500 bg-green-50",
其他: "border-gray-300 bg-gray-50",
};
return classes[type] || "border-gray-300 bg-gray-50";
};
// 根据学历等级获取时间线点的样式类
const getTimelinePointClass = (education) => {
const classes = {
大学专科: "bg-gray-500",
大学本科: "bg-blue-500",
硕士研究生: "bg-green-500",
博士研究生: "bg-amber-500",
博士后: "bg-amber-500",
};
return classes[education] || "bg-blue-500";
};
// 获取学校类型对应的标语
const getSchoolSlogan = (type) => {
if (type === "985学校") return "国家重点建设的高水平大学";
if (type === "211学校") return "面向21世纪重点建设的高等学校";
if (type === "双一流学校") return "世界一流大学和一流学科建设高校";
return "";
};
// 获取学历等级
const getEducationLevel = (education) => {
const levels = {
大学专科: 1,
大学本科: 2,
硕士研究生: 3,
博士研究生: 4,
博士后: 5,
};
return levels[education] || 0;
};
// 获取学历图标SVG根据学历类型返回不同的图标
const getEducationSvgIcon = (education) => {
// 默认图标 - 毕业帽
let svgIcon = `<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 14l9-5-9-5-9 5 9 5z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 14l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14z"></path>
</svg>`;
if (education === "大学本科") {
// 本科 - 文凭
svgIcon = `<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>`;
} else if (education === "硕士研究生") {
// 硕士 - 书和毕业帽
svgIcon = `<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path>
</svg>`;
} else if (education === "博士研究生" || education === "博士后") {
// 博士/博士后 - 灯泡(创新)
svgIcon = `<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"></path>
</svg>`;
}
return svgIcon;
};
// 计算排序后的学历数据
const sortedEducations = computed(() => {
const educationList = props.data?.data || [];
if (educationList.length <= 1) return educationList;
// 按照入学日期从早到晚排序
return [...educationList].sort((a, b) => {
// 假设ksrq是MMDD格式转为数字比较
const dateA = parseInt(a.ksrq || "0000");
const dateB = parseInt(b.ksrq || "0000");
return dateA - dateB;
});
});
// 判断是否有学历数据
const hasEducationData = computed(() => {
return props.data?.status === 1 && sortedEducations.value.length > 0;
});
// 判断是否有多个学历
const hasMultipleEducations = computed(() => {
return sortedEducations.value.length > 1;
});
</script>
<template>
<div class="w-full max-w-md mx-auto bg-white rounded-xl overflow-hidden font-sans shadow-md">
<div v-if="hasEducationData" class="p-6 pl-2">
<!-- 学历时间线 -->
<div class="relative pb-4">
<!-- 垂直时间线 -->
<div v-if="hasMultipleEducations"
class="absolute left-0 top-14 h-[calc(100%-44px)] w-0.5 bg-gradient-to-b from-blue-400 to-blue-200 rounded-full ml-5">
</div>
<!-- 学历卡片列表 -->
<div class="space-y-10 relative">
<div v-for="(education, index) in sortedEducations" :key="index" class="relative">
<!-- 时间线点 -->
<div v-if="hasMultipleEducations" :class="[
'absolute left-5 w-10 h-10 rounded-full border-4 border-white shadow-md flex items-center justify-center text-white transform -translate-x-1/2',
getTimelinePointClass(education.xl),
]">
<span class="text-sm font-bold">{{
index + 1
}}</span>
</div>
<!-- 学历卡片 -->
<div :class="[
'relative rounded-lg transition-all duration-300 ml-12',
'p-6 hover:-translate-y-1 hover:shadow-lg',
getSchoolTypeClass(education.xxlx),
]">
<!-- 顶部彩色条 -->
<div :class="[
'absolute top-0 left-0 w-full h-1.5 rounded-t-lg',
education.xxlx === '985学校'
? 'bg-gradient-to-r from-amber-500 to-amber-300'
: education.xxlx === '211学校'
? 'bg-gradient-to-r from-blue-500 to-blue-300'
: education.xxlx === '双一流学校'
? 'bg-gradient-to-r from-green-500 to-green-300'
: 'bg-gradient-to-r from-gray-400 to-gray-300',
]"></div>
<!-- 时间和学校类型标签 -->
<div class="flex flex-col sm:flex-row sm:justify-between mb-5 pt-2">
<!-- 学校类型标签 -->
<div v-if="education.xxlx !== '其他'"
class="bg-gradient-to-r from-yellow-400 to-yellow-500 text-white text-sm font-medium px-4 py-1.5 rounded-full shadow-sm inline-flex items-center mb-3 sm:mb-0 max-w-fit">
<svg class="w-4 h-4 mr-1.5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M6.267 3.455a3.066 3.066 0 001.745-.723 3.066 3.066 0 013.976 0 3.066 3.066 0 001.745.723 3.066 3.066 0 012.812 2.812c.051.643.304 1.254.723 1.745a3.066 3.066 0 010 3.976 3.066 3.066 0 00-.723 1.745 3.066 3.066 0 01-2.812 2.812 3.066 3.066 0 00-1.745.723 3.066 3.066 0 01-3.976 0 3.066 3.066 0 00-1.745-.723 3.066 3.066 0 01-2.812-2.812 3.066 3.066 0 00-.723-1.745 3.066 3.066 0 010-3.976 3.066 3.066 0 00.723-1.745 3.066 3.066 0 012.812-2.812zm7.44 5.252a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd"></path>
</svg>
{{ education.xxlx }}
</div>
<!-- 时间信息 -->
<div class="flex items-center text-gray-600 text-sm sm:text-base mt-1 sm:mt-0">
<svg class="w-4 h-4 text-blue-500 mr-2 flex-shrink-0" fill="none"
stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z">
</path>
</svg>
<span>{{ formatDate(education.ksrq) }} -
{{ formatDate(education.jsrq) }}</span>
</div>
</div>
<!-- 学历标题区域 -->
<div class="flex items-start mb-6">
<div
class="w-14 h-14 rounded-full bg-blue-50 flex items-center justify-center mr-5 flex-shrink-0 text-blue-500">
<span v-html="getEducationSvgIcon(education.xl)
"></span>
</div>
<div class="flex-1">
<div class="flex justify-between items-start">
<h3 class="text-xl font-semibold text-gray-800 mb-1">
{{ education.xl }}
</h3>
<!-- 高级学历标签 -->
<div v-if="
getEducationLevel(
education.xl
) >= 4
"
class="bg-red-500 text-white text-xs font-bold px-2.5 py-1 rounded-md shadow ml-2 animate-pulse">
顶级
</div>
</div>
<p v-if="education.xxlx !== '其他'" class="text-sm text-yellow-600 italic">
"{{ getSchoolSlogan(education.xxlx) }}"
</p>
</div>
</div>
<!-- 分隔线 -->
<div class="w-full h-px bg-gray-200 my-5"></div>
<!-- 学历详情 -->
<div class="space-y-4 mb-6">
<!-- 入学和毕业时间(改为两行布局) -->
<!-- 入学时间 -->
<div class="flex items-center text-gray-700">
<svg class="w-5 h-5 text-green-500 mr-3 flex-shrink-0" fill="none"
stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z">
</path>
</svg>
<span class="text-gray-500 mr-3 text-sm sm:text-base">入学时间:</span>
<span class="text-gray-700 text-sm sm:text-base font-medium">{{
formatDate(education.ksrq) }}</span>
</div>
<!-- 毕业时间 -->
<div class="flex items-center text-gray-700">
<svg class="w-5 h-5 text-red-500 mr-3 flex-shrink-0" fill="none"
stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z">
</path>
</svg>
<span class="text-gray-500 mr-3 text-sm sm:text-base">毕业时间:</span>
<span class="text-gray-700 text-sm sm:text-base font-medium">{{
formatDate(education.jsrq) }}</span>
</div>
<!-- 专业 -->
<div class="flex items-center text-gray-700">
<svg class="w-5 h-5 text-blue-400 mr-3 flex-shrink-0" fill="none"
stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253">
</path>
</svg>
<span class="text-gray-500 mr-3 text-sm sm:text-base">专业:</span>
<span class="text-gray-700 text-sm sm:text-base">{{ education.zymc }}</span>
</div>
<!-- 学习方式 -->
<div class="flex items-center text-gray-700">
<svg class="w-5 h-5 text-blue-400 mr-3 flex-shrink-0" fill="none"
stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span class="text-gray-500 mr-3 text-sm sm:text-base">学习方式:</span>
<span class="text-gray-700 text-sm sm:text-base">{{ education.xxxs }}</span>
</div>
</div>
<!-- 学历描述 -->
<div class="relative overflow-hidden rounded-lg mt-5">
<!-- 背景效果 -->
<div class="absolute inset-0 bg-gradient-to-br from-blue-50 to-gray-50 rounded-lg">
</div>
<!-- 装饰性元素 - 只在高级学历显示 -->
<div v-if="getEducationLevel(education.xl) >= 4"
class="absolute -right-4 -top-4 w-16 h-16 bg-yellow-100 rounded-full opacity-50">
</div>
<div v-if="getEducationLevel(education.xl) >= 4"
class="absolute -left-4 -bottom-4 w-12 h-12 bg-blue-100 rounded-full opacity-50">
</div>
<!-- 内容 -->
<div class="relative z-10 p-5">
<h4 class="font-medium text-base text-gray-700 mb-2 flex items-center">
<span class="mr-2">{{ education.xl }}学历</span>
<span v-if="
getEducationLevel(
education.xl
) >= 4
" class="text-xs bg-yellow-400 text-white px-2 py-0.5 rounded">高级</span>
</h4>
<p class="text-gray-600 text-sm leading-relaxed">
{{ getEducationDesc(education.xl) }}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 无数据状态 -->
<div v-else class="flex flex-col items-center py-10 px-5 text-center bg-gray-50">
<svg class="w-16 h-16 text-gray-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253">
</path>
</svg>
<div class="text-gray-600 text-sm max-w-md">
暂无学历信息记录。这可能是因为:
<ul class="text-left mt-3 pl-5">
<li
class="mb-2 relative before:content-['•'] before:absolute before:left-[-15px] before:text-blue-500">
学历信息不公开
</li>
<li
class="mb-2 relative before:content-['•'] before:absolute before:left-[-15px] before:text-blue-500">
暂无高等教育学历
</li>
<li
class="mb-2 relative before:content-['•'] before:absolute before:left-[-15px] before:text-blue-500">
学历较早暂未被教育部门数字化收录
</li>
</ul>
</div>
</div>
</div>
</template>
<style scoped>
.education-history {
padding: 24px;
border-radius: 16px;
background-color: #fff;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08);
font-family: "PingFang SC", "Helvetica Neue", Arial, sans-serif;
position: relative;
overflow: hidden;
}
.education-history::before {
content: "";
position: absolute;
top: 0;
right: 0;
width: 150px;
height: 150px;
background: radial-gradient(circle at top right,
rgba(64, 158, 255, 0.05),
transparent 70%);
border-radius: 0 0 0 100%;
z-index: 0;
}
.education-content {
padding: 10px 0;
position: relative;
z-index: 1;
}
.decoration-top {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 30px;
}
.decoration-line {
height: 1px;
background: linear-gradient(to right,
transparent,
rgba(64, 158, 255, 0.5),
transparent);
flex-grow: 1;
}
.decoration-icon {
font-size: 22px;
margin: 0 15px;
background-color: #f0f7ff;
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.2);
}
.decoration-bottom {
display: flex;
justify-content: center;
margin-top: 20px;
}
.decoration-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background-color: #409eff;
margin: 0 3px;
opacity: 0.5;
}
.timeline {
position: relative;
padding-left: 20px;
}
.timeline-item {
position: relative;
padding-bottom: 40px;
}
.timeline-item:last-child {
padding-bottom: 0;
}
.timeline-point {
position: absolute;
left: -10px;
top: 0;
width: 20px;
height: 20px;
border-radius: 50%;
background-color: #409eff;
border: 4px solid #fff;
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.3);
z-index: 2;
transition: all 0.3s ease;
}
.edu-associate .timeline-point {
background-color: #909399;
}
.edu-bachelor .timeline-point {
background-color: #409eff;
}
.edu-master .timeline-point {
background-color: #67c23a;
}
.edu-doctor .timeline-point,
.edu-postdoc .timeline-point {
background-color: #ff9900;
}
.timeline-item:hover .timeline-point {
transform: scale(1.2);
}
.timeline-line {
position: absolute;
left: 0;
top: 20px;
height: calc(100% - 20px);
width: 2px;
background: linear-gradient(to bottom, #409eff, #67c23a);
z-index: 1;
}
.education-card {
margin-left: 20px;
padding: 20px;
border-radius: 12px;
background-color: #f5f7fa;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
border-left: none;
}
.education-card::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 5px;
background: linear-gradient(to right, #409eff, #67c23a);
opacity: 0.7;
}
.school-985::after {
background: linear-gradient(to right, #ff9900, #ffba56);
}
.school-211::after {
background: linear-gradient(to right, #409eff, #6ac0ff);
}
.school-double-first-class::after {
background: linear-gradient(to right, #67c23a, #95d475);
}
.education-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
}
.card-corner {
position: absolute;
bottom: 0;
right: 0;
width: 30px;
height: 30px;
background: linear-gradient(to bottom right,
transparent 49%,
rgba(64, 158, 255, 0.1) 50%);
}
.school-985 .card-corner {
background: linear-gradient(to bottom right,
transparent 49%,
rgba(255, 153, 0, 0.1) 50%);
}
.school-211 .card-corner {
background: linear-gradient(to bottom right,
transparent 49%,
rgba(64, 158, 255, 0.1) 50%);
}
.school-double-first-class .card-corner {
background: linear-gradient(to bottom right,
transparent 49%,
rgba(103, 194, 58, 0.1) 50%);
}
.education-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.education-time {
font-size: 16px;
color: #606266;
display: flex;
align-items: center;
}
.time-icon {
margin-right: 5px;
font-style: normal;
}
.education-level {
font-size: 18px;
font-weight: bold;
color: #303133;
padding: 4px 12px;
border-radius: 30px;
background-color: rgba(0, 0, 0, 0.03);
}
.education-details {
margin-bottom: 16px;
position: relative;
}
.school-type-badge {
display: inline-flex;
align-items: center;
padding: 6px 12px;
margin-bottom: 12px;
border-radius: 20px;
font-size: 14px;
font-weight: bold;
background-color: #409eff;
color: white;
box-shadow: 0 2px 6px rgba(64, 158, 255, 0.3);
}
.badge-icon {
margin-right: 5px;
}
.school-985 .school-type-badge {
background-color: #ff9900;
box-shadow: 0 2px 6px rgba(255, 153, 0, 0.3);
}
.school-211 .school-type-badge {
background-color: #409eff;
box-shadow: 0 2px 6px rgba(64, 158, 255, 0.3);
}
.school-double-first-class .school-type-badge {
background-color: #67c23a;
box-shadow: 0 2px 6px rgba(103, 194, 58, 0.3);
}
.education-major,
.education-mode {
margin-bottom: 10px;
font-size: 15px;
color: #606266;
display: flex;
align-items: center;
}
.label {
color: #909399;
margin-right: 8px;
display: flex;
align-items: center;
}
.major-icon,
.mode-icon {
font-style: normal;
margin-right: 5px;
}
.value {
color: #303133;
font-weight: 500;
}
.education-description {
margin-top: 16px;
padding: 14px;
background-color: rgba(0, 0, 0, 0.02);
border-radius: 8px;
border-left: 3px solid rgba(64, 158, 255, 0.3);
}
.school-985 .education-description {
border-left: 3px solid rgba(255, 153, 0, 0.3);
}
.school-211 .education-description {
border-left: 3px solid rgba(64, 158, 255, 0.3);
}
.school-double-first-class .education-description {
border-left: 3px solid rgba(103, 194, 58, 0.3);
}
.desc-title {
font-size: 15px;
font-weight: bold;
margin-bottom: 8px;
color: #303133;
}
.desc-content {
font-size: 14px;
color: #606266;
line-height: 1.6;
}
/* 不同学历等级的样式 */
.edu-associate .education-level {
color: #909399;
background-color: rgba(144, 147, 153, 0.1);
}
.edu-bachelor .education-level {
color: #409eff;
background-color: rgba(64, 158, 255, 0.1);
}
.edu-master .education-level {
color: #67c23a;
background-color: rgba(103, 194, 58, 0.1);
}
.edu-doctor .education-level,
.edu-postdoc .education-level {
color: #ff9900;
background-color: rgba(255, 153, 0, 0.1);
position: relative;
}
.edu-doctor .education-level:after,
.edu-postdoc .education-level:after {
content: "🎓";
position: absolute;
right: -24px;
top: -2px;
}
.edu-postdoc .education-level:after {
content: "🏆";
}
/* 无数据样式 */
.no-data {
display: flex;
flex-direction: column;
align-items: center;
padding: 40px 20px;
text-align: center;
background: linear-gradient(to bottom, #ffffff, #f5f7fa);
border-radius: 12px;
}
.no-data-icon {
font-size: 54px;
margin-bottom: 20px;
opacity: 0.6;
background: linear-gradient(to bottom, #409eff, #67c23a);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.no-data-text {
color: #606266;
font-size: 15px;
max-width: 400px;
}
.no-data-text ul {
text-align: left;
margin-top: 10px;
padding-left: 20px;
}
.no-data-text li {
margin-bottom: 8px;
position: relative;
}
.no-data-text li:before {
content: "•";
position: absolute;
left: -15px;
color: #409eff;
}
/* 适配移动端 */
@media screen and (max-width: 768px) {
.education-history {
padding: 16px;
}
.education-card {
margin-left: 10px;
padding: 16px;
}
.timeline-point {
left: -8px;
width: 16px;
height: 16px;
}
.education-header {
flex-direction: column;
align-items: flex-start;
}
.education-level {
margin-top: 8px;
}
}
</style>

1307
src/ui/CJRZQ0A03.vue Normal file

File diff suppressed because it is too large Load Diff

369
src/ui/CJRZQ4AA8.vue Normal file
View File

@@ -0,0 +1,369 @@
<script setup>
import { computed, ref, onMounted, onUnmounted, watch } from "vue";
import * as echarts from "echarts";
import { useRiskNotifier } from "@/composables/useRiskNotifier";
const props = defineProps({
data: {
type: Object,
default: () => ({}),
},
apiId: {
type: String,
default: '',
},
index: {
type: Number,
default: 0,
},
notifyRiskStatus: {
type: Function,
default: () => { },
},
});
// 计算得分如果没有数据则默认为0
const score = computed(() => {
return props.data?.score ? Number(props.data.score) : 0;
});
// 计算风险评分0-100分分数越高越安全
const riskScore = computed(() => {
// 还款压力分数越高风险越大,转换为安全分数
// 压力分数 0-20100分最安全
// 压力分数 20-5070分较安全
// 压力分数 50-8040分有风险
// 压力分数 80-10010分高风险
const pressure = score.value;
if (pressure <= 20) return 100;
if (pressure <= 50) return 70;
if (pressure <= 80) return 40;
return 10;
});
// 使用 composable 通知父组件风险评分
useRiskNotifier(props, riskScore);
// 暴露给父组件
defineExpose({
riskScore
});
// 根据分值确定压力等级
const pressureLevel = computed(() => {
if (score.value <= 20)
return {
level: "低",
color: "#67C23A",
text: "还款压力小",
bgGradient: "from-green-500 to-green-300",
lightBg: "bg-green-50",
borderColor: "border-green-200",
gradient: [
{ offset: 0, color: "#67C23A" },
{ offset: 1, color: "#85ce61" }
]
};
if (score.value <= 50)
return {
level: "中",
color: "#E6A23C",
text: "还款压力中等",
bgGradient: "from-yellow-500 to-yellow-300",
lightBg: "bg-yellow-50",
borderColor: "border-yellow-200",
gradient: [
{ offset: 0, color: "#E6A23C" },
{ offset: 1, color: "#ebb563" }
]
};
if (score.value <= 80)
return {
level: "高",
color: "#E53E3E",
text: "还款压力较大",
bgGradient: "from-orange-500 to-red-400",
lightBg: "bg-red-50",
borderColor: "border-red-200",
gradient: [
{ offset: 0, color: "#E53E3E" },
{ offset: 1, color: "#fc8181" }
]
};
return {
level: "极高",
color: "#FF0000",
text: "还款压力非常大",
bgGradient: "from-red-600 to-red-500",
lightBg: "bg-red-50",
borderColor: "border-red-300",
gradient: [
{ offset: 0, color: "#FF0000" },
{ offset: 1, color: "#ff3333" }
]
};
});
// 计算进度条宽度百分比
const progressWidth = computed(() => {
return `${score.value}%`;
});
// 计算评分对应的Tailwind文本颜色类
const scoreColorClass = computed(() => {
if (score.value <= 20) return "text-green-500";
if (score.value <= 50) return "text-yellow-500";
if (score.value <= 80) return "text-orange-500";
return "text-red-600";
});
// 获取图标路径(根据压力等级)
const getIconPath = () => {
// 低压力使用 zq
if (score.value <= 20) {
return new URL('@/assets/images/report/zq.png', import.meta.url).href
}
// 中等压力使用 zfx
if (score.value <= 50) {
return new URL('@/assets/images/report/zfx.png', import.meta.url).href
}
// 高压力和极高压力使用 gfx
return new URL('@/assets/images/report/gfx.png', import.meta.url).href
};
// 获取边框颜色
const getBorderColor = () => {
if (score.value <= 20) return '#bbf7d0'; // 绿色
if (score.value <= 50) return '#fef3c7'; // 黄色
if (score.value <= 80) return '#fecaca'; // 红色
return '#fecaca'; // 极高压力也是红色
};
// ECharts 仪表盘
const chartRef = ref(null);
let chartInstance = null;
const initChart = () => {
if (!chartRef.value) return;
chartInstance = echarts.init(chartRef.value);
updateChart();
};
const updateChart = () => {
if (!chartInstance) return;
const risk = pressureLevel.value;
const option = {
series: [
{
type: "gauge",
startAngle: 180,
endAngle: 0,
min: 0,
max: 100,
radius: "100%",
center: ["50%", "80%"],
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, risk.gradient),
shadowBlur: 6,
shadowColor: risk.color,
},
progress: {
show: true,
width: 20,
roundCap: true,
clip: false
},
axisLine: {
roundCap: true,
lineStyle: {
width: 20,
color: [
[1, new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{
offset: 0,
color: risk.color + "30"
},
{
offset: 1,
color: risk.color + "25"
}
])]
]
}
},
axisTick: {
show: true,
distance: -30,
length: 6,
splitNumber: 10,
lineStyle: {
color: risk.color,
width: 1,
opacity: 0.5
}
},
splitLine: {
show: true,
distance: -36,
length: 12,
splitNumber: 9,
lineStyle: {
color: risk.color,
width: 2,
opacity: 0.5
}
},
axisLabel: {
show: false,
},
anchor: {
show: false
},
pointer: {
icon: "triangle",
iconStyle: {
color: risk.color,
borderColor: risk.color,
borderWidth: 1
},
offsetCenter: ["7%", "-67%"],
length: "10%",
width: 15
},
detail: {
valueAnimation: true,
fontSize: 30,
fontWeight: "bold",
color: risk.color,
offsetCenter: [0, "-25%"],
formatter: function (value) {
return `{value|${value}分}\n{level|${risk.level}级还款压力}`;
},
rich: {
value: {
fontSize: 30,
fontWeight: 'bold',
color: risk.color,
padding: [0, 0, 5, 0]
},
level: {
fontSize: 14,
fontWeight: 'normal',
color: risk.color,
padding: [5, 0, 0, 0]
}
}
},
data: [
{
value: score.value
}
],
title: {
fontSize: 14,
color: risk.color,
offsetCenter: [0, "10%"],
formatter: risk.level + "级还款压力"
}
}
]
};
chartInstance.setOption(option);
};
watch(
() => score.value,
() => {
updateChart();
}
);
onMounted(() => {
initChart();
window.addEventListener("resize", () => {
if (chartInstance) {
chartInstance.resize();
}
});
});
onUnmounted(() => {
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
window.removeEventListener("resize", chartInstance?.resize);
});
</script>
<template>
<div class="card">
<div class="rounded-lg border border-gray-200 pb-2 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/hkylfx.png" alt="还款压力分析" class="w-8 h-8 object-contain" />
</div>
<span class="font-bold text-gray-800">还款压力分析</span>
</div>
<div class="px-4 pb-4">
<!-- 仪表盘图表 -->
<div class="mb-6">
<div ref="chartRef" :style="{ width: '100%', height: '200px' }"></div>
</div>
<!-- 压力等级显示 -->
<div class="mb-6">
<div class="space-y-3 p-4 rounded-lg border" :class="pressureLevel.lightBg"
:style="{ borderColor: getBorderColor() }">
<div class="flex items-start">
<div class="mr-3 mt-1">
<img :src="getIconPath()" alt="还款压力" class="w-10 h-10 object-contain" />
</div>
<div class="flex-1">
<h4 class="font-semibold text-gray-800 mb-2">{{ pressureLevel.text }}</h4>
<p class="text-gray-400 text-sm">
分值越高表示还款压力越大建议关注债务比例
</p>
</div>
</div>
</div>
</div>
<!-- 财务建议 -->
<div class="mb-6">
<div class="flex items-center mb-3">
<div class="w-4 h-4 flex items-center justify-center mr-2">
<img src="@/assets/images/report/wxts_icon.png" alt="财务建议" class="w-4 h-4 object-contain" />
</div>
<div class="font-bold text-gray-800">财务建议</div>
</div>
<div class="ml-6 text-sm text-gray-600 space-y-1">
<p v-if="score > 50">
建议合理规划财务控制债务比例增加收入来源避免过度负债
</p>
<p v-if="score > 50" class="mt-1">
可尝试分期付款或延长还款周期减轻每月还款压力
</p>
<p v-else>
当前还款压力在可控范围内继续保持良好的财务习惯
</p>
<p v-if="score <= 50" class="mt-1">
建议定期检查收支平衡确保及时还款维持良好信用记录
</p>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
/* 样式已通过 Tailwind CSS 类实现 */
</style>

246
src/ui/CJRZQ5E9F/README.md Normal file
View File

@@ -0,0 +1,246 @@
# 贷款风险报告组件 (CJRZQ5E9F) - 模块化架构
## 概述
贷款风险报告组件采用模块化架构设计将完整的贷款风险评估拆分成7个独立的模块每个模块都可以作为独立的tab显示具有独立的大标题。
## 数据结构
贷款风险报告的数据结构如下:
```javascript
{
"apiID": "CJRZQ5E9F",
"data": {
"xyp_cpl0001": "5", // 贷款总机构数
"xyp_cpl0002": "3", // 已结清机构数
"xyp_cpl0007": "2", // 消费金融类机构数
"xyp_cpl0008": "1", // 网络贷款类机构数
"xyp_cpl0009": "1", // 最近7天机构数
"xyp_cpl0014": "15", // 历史成功还款笔数
"xyp_cpl0015": "2", // 历史失败还款笔数
"xyp_cpl0016": "1", // 最近1天失败笔数
"xyp_cpl0017": "3", // 最近1天成功笔数
"xyp_cpl0018": "1", // 最近7天失败笔数
"xyp_cpl0019": "5", // 最近7天成功笔数
"xyp_cpl0020": "1", // 最近14天失败笔数
"xyp_cpl0021": "7", // 最近14天成功笔数
"xyp_cpl0022": "2", // 最近30天失败笔数
"xyp_cpl0023": "10", // 最近30天成功笔数
"xyp_cpl0024": "2", // 最近90天失败笔数
"xyp_cpl0025": "12", // 最近90天成功笔数
"xyp_cpl0026": "2", // 最近180天失败笔数
"xyp_cpl0027": "14", // 最近180天成功笔数
"xyp_cpl0028": "0", // 最近1天逾期标识
"xyp_cpl0029": "0", // 最近7天逾期标识
"xyp_cpl0030": "0", // 最近14天逾期标识
"xyp_cpl0031": "0", // 最近30天逾期标识
"xyp_cpl0032": "1000", // 最近1天失败金额
"xyp_cpl0033": "5000", // 最近1天成功金额
"xyp_cpl0034": "2000", // 最近7天失败金额
"xyp_cpl0035": "8000", // 最近7天成功金额
"xyp_cpl0036": "1500", // 最近14天失败金额
"xyp_cpl0037": "12000", // 最近14天成功金额
"xyp_cpl0038": "3000", // 最近30天失败金额
"xyp_cpl0039": "20000", // 最近30天成功金额
"xyp_cpl0040": "4000", // 最近90天失败金额
"xyp_cpl0041": "35000", // 最近90天成功金额
"xyp_cpl0042": "5000", // 最近180天失败金额
"xyp_cpl0043": "45000", // 最近180天成功金额
"xyp_cpl0044": "0", // 当前逾期状态
"xyp_cpl0045": "365", // 信用贷款时长
"xyp_cpl0046": "30", // 最近一次交易距今天数
"xyp_cpl0064": "1", // 最近21天成功笔数
"xyp_cpl0065": "0", // 最近21天失败笔数
"xyp_cpl0066": "1000", // 最近21天失败金额
"xyp_cpl0067": "6000", // 最近21天成功金额
"xyp_cpl0068": "15", // 最近一次还款距今天数
"xyp_cpl0070": "1", // 最近1天机构数
"xyp_cpl0071": "0", // 当前逾期机构数
"xyp_cpl0072": "0", // 当前逾期金额
"xyp_cpl0073": "0.85", // 近5次金额成功率
"xyp_cpl0074": "0.80", // 近5次还款成功率
"xyp_cpl0075": "0.75", // 近20次小贷成功率
"xyp_cpl0079": "0.70", // 近90天金额成功率
"xyp_cpl0080": "0.65", // 近90天还款成功率
"xyp_cpl0081": "0.25", // 信用风险评分
"xyp_cpl0082": "0.30", // 履约金额综合指数
"xyp_cpl0083": "0.35", // 履约笔数综合指数
"xyp_model_score_high": "750", // 小额网贷分
"xyp_model_score_mid": "680", // 小额分期分
"xyp_model_score_low": "720", // 中大额分期分
"xyp_t0400002": "0.78", // 近20次还款成功率
"xyp_t0400003": "0.82", // 近50次还款成功率
"xyp_t0400004": "0.80" // 近100次还款成功率
},
"success": true,
"timestamp": "2025-01-20 21:19:58"
}
```
## 模块拆分
贷款风险报告被拆分成以下7个独立模块
| API ID | 模块名称 | 包含数据 | 组件文件 |
|--------|----------|----------|----------|
| `CJRZQ5E9F_RiskOverview` | 风险概览 | 综合风险等级、当前状态、关键指标 | RiskOverview.vue |
| `CJRZQ5E9F_CreditScores` | 信用评分 | 综合信用指数、专业模型评分、还款表现 | CreditScores.vue |
| `CJRZQ5E9F_LoanBehaviorAnalysis` | 贷款行为分析 | 机构类型分布、还款表现统计、时间维度分析 | LoanBehaviorAnalysis.vue |
| `CJRZQ5E9F_InstitutionAnalysis` | 机构分析 | 机构类型分析、合作机构详情 | InstitutionAnalysis.vue |
| `CJRZQ5E9F_TimeTrendAnalysis` | 时间趋势分析 | 历史趋势、周期性分析 | TimeTrendAnalysis.vue |
| `CJRZQ5E9F_RiskIndicators` | 风险指标详情 | 详细风险指标、风险因子分析 | RiskIndicators.vue |
| `CJRZQ5E9F_RiskAdvice` | 专业建议 | 风险评估建议、优化建议 | RiskAdvice.vue |
## 使用方法
### 1. 前端自动拆分
BaseReport.vue 已自动配置支持贷款风险报告的模块化显示:
```javascript
import { splitCJRZQ5E9FForTabs } from '@/ui/CJRZQ5E9F/utils/simpleSplitter.js';
// 处理数据拆分支持DWBG8B4D、DWBG6A2C和CJRZQ5E9F
const processedReportData = computed(() => {
let data = reportData.value;
// 拆分DWBG8B4D数据
data = splitDWBG8B4DForTabs(data);
// 拆分DWBG6A2C数据
data = splitDWBG6A2CForTabs(data);
// 拆分CJRZQ5E9F数据
data = splitCJRZQ5E9FForTabs(data);
return data;
});
```
### 2. 组件配置
BaseReport.vue 中已配置所有贷款风险报告模块:
```javascript
// 贷款风险报告
JRZQ5E9F: {
name: "贷款风险评估",
component: defineAsyncComponent(() => import("@/ui/CJRZQ5E9F/index.vue")),
remark: '贷款风险评估提供全面的个人贷款风险分析,包括风险概览、信用评分、贷款行为分析、机构分析等多维度评估。'
},
// ... 其他模块配置
```
## 组件结构
```
src/ui/CJRZQ5E9F/
├── index.vue # 原始完整组件(保留)
├── README.md # 文档说明
├── components/ # 子组件目录
│ ├── RiskOverview.vue # 风险概览
│ ├── CreditScores.vue # 信用评分
│ ├── LoanBehaviorAnalysis.vue # 贷款行为分析
│ ├── InstitutionAnalysis.vue # 机构分析
│ ├── TimeTrendAnalysis.vue # 时间趋势分析
│ ├── RiskIndicators.vue # 风险指标详情
│ └── RiskAdvice.vue # 专业建议
└── utils/
└── simpleSplitter.js # 数据拆分工具
```
## 特色功能
### 1. 智能风险评估
- 多维度风险等级计算
- 智能颜色编码
- 动态风险提示
### 2. 数据可视化
- 渐变色彩设计
- 图标化展示
- 响应式布局
- 交互式图表
### 3. 用户友好
- 清晰的层次结构
- 详细的说明文档
- 直观的风险提示
- 专业的建议指导
### 4. 模块化设计
- 独立的模块组件
- 可复用的工具函数
- 灵活的数据拆分
- 易于维护和扩展
## 工具函数
`utils/simpleSplitter.js` 提供了以下工具函数:
- `splitCJRZQ5E9FForTabs()` - 数据拆分
- `parseIntervalValue()` - 解析区间化数值
- `formatMetricValue()` - 格式化指标值
- `formatDays()` - 格式化天数显示
- `formatAmount()` - 格式化金额显示
- `calculateRiskLevel()` - 计算风险等级
- `calculateCreditScore()` - 计算信用评分
- `getCreditScoreLevel()` - 获取信用等级描述
- `getCreditScoreBadgeClass()` - 获取信用等级样式
- `getScoreClass()` - 获取评分样式
- `getCircleStyle()` - 获取圆形进度样式
- `hasRiskData()` - 检查是否有风险数据
## 使用示例
```javascript
// 在页面中使用
<BaseReport
:reportData="reportData"
:reportParams="reportParams"
reportName="贷款风险评估"
feature="CJRZQ5E9F"
:isEmpty="false"
:isDone="true"
/>
```
## 数据字段说明
### 主要指标字段
- `xyp_cpl0001`: 贷款总机构数
- `xyp_cpl0002`: 已结清机构数
- `xyp_cpl0044`: 当前逾期状态 (0: 无逾期, 1: 有逾期)
- `xyp_cpl0081`: 信用风险评分 (0-1)
- `xyp_cpl0082`: 履约金额综合指数 (0-1)
- `xyp_cpl0083`: 履约笔数综合指数 (0-1)
### 模型评分字段
- `xyp_model_score_high`: 小额网贷分 (350-950)
- `xyp_model_score_mid`: 小额分期分 (350-950)
- `xyp_model_score_low`: 中大额分期分 (350-950)
### 还款表现字段
- `xyp_cpl0073`: 近5次金额成功率
- `xyp_cpl0074`: 近5次还款成功率
- `xyp_t0400002`: 近20次还款成功率
- `xyp_t0400003`: 近50次还款成功率
- `xyp_t0400004`: 近100次还款成功率
## 注意事项
1. 确保数据格式符合贷款风险报告的标准结构
2. 所有模块都支持数据为空的情况
3. 风险评估基于实际数据动态计算
4. 组件采用 Tailwind CSS 进行样式设计
5. 支持移动端响应式布局
6. 区间化数值会自动解析为具体数值进行显示
## 更新日志
- v1.0.0: 初始版本,支持完整的贷款风险报告模块化显示
- 包含7个独立模块
- 支持自动数据拆分
- 提供完整的风险评估功能
- 支持多种数据可视化方式

View File

@@ -0,0 +1,579 @@
<template>
<div class="rounded-lg border border-[#99999933]">
<div class="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/xypf2.png" alt="信用评分" class="w-8 h-8 object-contain" />
</div>
<span class="font-bold text-gray-800">信用评分</span>
</div>
<div class="pb-4">
<!-- 综合信用指数 -->
<div class="mb-6">
<LTitle title="综合信用指数" class="mb-2" />
<p class="text-gray-400 text-sm mb-4 px-4">基于多维度风险评估的综合评分</p>
<!-- 信用分仪表盘 -->
<div class="flex flex-col items-center mb-4">
<div ref="chartRef" :style="{ width: '100%', height: '200px' }"></div>
<div class="text-center mt-[-14px]">
<div class=" text-[#999999]">评分范围: 150-1000</div>
<div class="px-10 py-1 rounded-full font-medium inline-block mt-2" :class="getCreditScoreBadgeClass()">
{{ getCreditScoreLevel() }}
</div>
</div>
</div>
<!-- 详细指标 -->
<div class="space-y-3 px-4 ">
<div class="bg-green-50 rounded-lg p-4 border border-green-200">
<div class="flex items-start">
<img src="@/assets/images/report/zq.png" alt="信用风险评分"
class="w-10 h-10 object-contain mr-4 flex-shrink-0" />
<div class="flex-1">
<div class="flex items-center justify-between mb-2">
<span class="text-sm font-medium text-gray-800">信用风险评分</span>
<span class="text-sm font-bold" :class="getScoreTextClass()">{{ (creditRiskScore * 100).toFixed(0)
}}%</span>
</div>
<div class="h-2" :style="`background-color: ${getLightScoreColor()}`">
<div class="h-2 transition-all duration-500"
:style="`width: ${Math.max(creditRiskScore * 100, 2)}%; background-color: ${getScoreColor()}`">
</div>
</div>
</div>
</div>
</div>
<div class="bg-green-50 rounded-lg p-4 border border-green-200">
<div class="flex items-start">
<img src="@/assets/images/report/zq.png" alt="履约金额综合指数"
class="w-10 h-10 object-contain mr-4 flex-shrink-0" />
<div class="flex-1">
<div class="flex items-center justify-between mb-2">
<span class="text-sm font-medium text-gray-800">履约金额综合指数</span>
<span class="text-sm font-bold" :class="getScoreTextClass()">{{ (amountComplianceIndex *
100).toFixed(0)
}}%</span>
</div>
<div class="h-2" :style="`background-color: ${getLightScoreColor()}`">
<div class="h-2 transition-all duration-500"
:style="`width: ${Math.max(amountComplianceIndex * 100, 2)}%; background-color: ${getScoreColor()}`">
</div>
</div>
</div>
</div>
</div>
<div class="bg-green-50 rounded-lg p-4 border border-green-200">
<div class="flex items-start">
<img src="@/assets/images/report/zq.png" alt="履约笔数综合指数"
class="w-10 h-10 object-contain mr-4 flex-shrink-0" />
<div class="flex-1">
<div class="flex items-center justify-between mb-2">
<span class="text-sm font-medium text-gray-800">履约笔数综合指数</span>
<span class="text-sm font-bold" :class="getScoreTextClass()">{{ (countComplianceIndex *
100).toFixed(0)
}}%</span>
</div>
<div class="h-2" :style="`background-color: ${getLightScoreColor()}`">
<div class="h-2 transition-all duration-500"
:style="`width: ${Math.max(countComplianceIndex * 100, 2)}%; background-color: ${getScoreColor()}`">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 专业模型评分 -->
<div class=" mb-4">
<div class="">
<LTitle title="专业模型评分" class="mb-2" />
<p class="text-gray-400 text-sm mb-4 px-4">星耀Pro系列AI评分模型</p>
<div class="space-y-3 px-4">
<!-- 小额网贷分 V1 -->
<div :class="getModelCardClass(highRiskScore)">
<div class="flex items-center mb-2">
<img :src="getModelIcon(highRiskScore)" alt="小额网贷分 V1"
class="w-10 h-10 object-contain mr-3 flex-shrink-0" />
<div class="flex-1">
<div class="font-medium text-gray-800 mb-1">小额网贷分 V1</div>
<div class="text-sm text-gray-600">针对小额网贷风险评估</div>
</div>
</div>
<div class="text-sm text-gray-600 ml-[52px]">评分: <span class="font-bold">350-950</span></div>
<div class="absolute top-0 right-0 bg-[#999999] text-white px-2 py-1 rounded-bl-lg rounded-tr-lg text-xs">
{{ formatModelScore(highRiskScore) }}
</div>
</div>
<!-- 小额分期分 V1 -->
<div :class="getModelCardClass(midRiskScore)">
<div class="flex items-center mb-2">
<img :src="getModelIcon(midRiskScore)" alt="小额分期分 V1"
class="w-10 h-10 object-contain mr-3 flex-shrink-0" />
<div class="flex-1">
<div class="font-medium text-gray-800 mb-1">小额分期分 V1</div>
<div class="text-sm text-gray-600">针对小额分期产品评估</div>
</div>
</div>
<div class="text-sm text-gray-600 ml-[52px]">评分: <span class="font-bold">350-950</span></div>
<div class="absolute top-0 right-0 bg-[#999999] text-white px-2 py-1 rounded-bl-lg rounded-tr-lg text-xs">
{{ formatModelScore(midRiskScore) }}
</div>
</div>
<!-- 中大额分期分 V1 -->
<div :class="getModelCardClass(lowRiskScore)">
<div class="flex items-center mb-2">
<img :src="getModelIcon(lowRiskScore)" alt="中大额分期分 V1"
class="w-10 h-10 object-contain mr-3 flex-shrink-0" />
<div class="flex-1">
<div class="font-medium text-gray-800 mb-1">中大额分期分 V1</div>
<div class="text-sm text-gray-600">针对中大额分期产品评估</div>
</div>
</div>
<div class="text-sm text-gray-600 ml-[52px]">评分: <span class="font-bold">350-950</span></div>
<div class="absolute top-0 right-0 bg-[#999999] text-white px-2 py-1 rounded-bl-lg rounded-tr-lg text-xs">
{{ formatModelScore(lowRiskScore) }}
</div>
</div>
</div>
</div>
</div>
<!-- 还款表现指标 -->
<div class="mb-8">
<div class="">
<LTitle title="还款表现指标" class="mb-2" />
<p class="text-gray-400 text-sm mb-4 px-4">近期还款成功率统计</p>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-3 px-4">
<!-- 近5次 -->
<div class="bg-[#ECF9EF] rounded-lg p-3 border border-[#91D69F]">
<div class="flex flex-col items-center justify-center h-full w-full">
<div class="flex flex-col items-center justify-center w-full h-full text-center">
<div class="text-lg text-[#999999] mt-1">金额: <span class="font-bold text-green-600 text-2xl">{{
(recent5AmountRatio * 100).toFixed(0) }}</span> %</div>
<div class="font-medium text-[#666666] mb-2">近5次</div>
</div>
</div>
</div>
<!-- 近20次 -->
<div class="bg-[#ECF9EF] rounded-lg p-3 border border-[#91D69F]">
<div class="flex flex-col items-center justify-center h-full w-full">
<div class="flex flex-col items-center justify-center w-full h-full text-center">
<div class="text-lg text-[#999999] mt-1">小贷: <span class="font-bold text-green-600 text-2xl">{{
(recent20SmallLoanRatio * 100).toFixed(0) }}</span> %</div>
<div class="font-medium text-[#666666] mb-2">近20次</div>
</div>
</div>
</div>
<!-- 近90天 -->
<div class="bg-[#ECF9EF] rounded-lg p-3 border border-[#91D69F]">
<div class="flex flex-col items-center justify-center h-full w-full">
<div class="flex flex-col items-center justify-center w-full h-full text-center">
<div class="text-lg text-[#999999] mt-1">金额: <span class="font-bold text-green-600 text-2xl">{{
(recent90AmountRatio * 100).toFixed(0) }}</span> %</div>
<div class="font-medium text-[#666666] mb-2">近90天</div>
</div>
</div>
</div>
<!-- 近50次 -->
<div class="bg-[#ECF9EF] rounded-lg p-3 border border-[#91D69F]">
<div class="flex flex-col items-center justify-center h-full w-full">
<div class="flex flex-col items-center justify-center w-full h-full text-center">
<div class="text-lg text-[#999999] mt-1">成功率: <span class="font-bold text-green-600 text-2xl">{{
(recent50PaymentRatio * 100).toFixed(0) }}</span> %</div>
<div class="font-medium text-[#666666] mb-2">近50次</div>
</div>
</div>
</div>
<!-- 近100次 -->
<div class="bg-[#ECF9EF] rounded-lg p-3 border border-[#91D69F]">
<div class="flex flex-col items-center justify-center h-full w-full">
<div class="flex flex-col items-center justify-center w-full h-full text-center">
<div class="text-lg text-[#999999] mt-1">成功率: <span class="font-bold text-green-600 text-2xl">{{
(recent100PaymentRatio * 100).toFixed(0) }}</span> %</div>
<div class="font-medium text-[#666666] mb-2">近100次</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 温馨提示 -->
<LRemark
content="信用评分系统基于多维度数据建立的综合信用评估模型提供综合信用指数、分类信用评分和信用等级分析。评分范围为0-1000分分数越高代表信用状况越好。系统会根据还款表现、借贷历史、负债情况等因素进行评分。建议结合具体业务场景设定信用门槛并定期更新评分模型以提高预测准确性。" />
</template>
<script>
import LTitle from '@/components/LTitle.vue'
import LRemark from '@/components/LRemark.vue'
import * as echarts from 'echarts'
export default {
name: 'CreditScores',
components: {
LTitle,
LRemark
},
props: {
data: {
type: Object,
default: () => ({})
}
},
data() {
return {
chartInstance: null
}
},
computed: {
creditRiskScore() {
return parseFloat(this.data.xyp_cpl0081) || 0
},
amountComplianceIndex() {
return parseFloat(this.data.xyp_cpl0082) || 0
},
countComplianceIndex() {
return parseFloat(this.data.xyp_cpl0083) || 0
},
// 模型评分
highRiskScore() {
const score = parseInt(this.data.xyp_model_score_high)
return isNaN(score) || score === -1 ? null : score
},
midRiskScore() {
const score = parseInt(this.data.xyp_model_score_mid)
return isNaN(score) || score === -1 ? null : score
},
lowRiskScore() {
const score = parseInt(this.data.xyp_model_score_low)
return isNaN(score) || score === -1 ? null : score
},
// 综合信用评分计算
creditScoreDisplay() {
const avgRisk = (this.creditRiskScore + this.amountComplianceIndex + this.countComplianceIndex) / 3
// 风险越高,信用分越低
return Math.round((1 - avgRisk) * 850 + 150)
},
creditScoreColor() {
if (this.creditScoreDisplay >= 750) return '#1FBE5D'
if (this.creditScoreDisplay >= 650) return '#f59e0b'
return '#ef4444'
},
// 还款比例计算
recent5PaymentRatio() {
return parseFloat(this.data.xyp_cpl0074) || 0
},
recent5AmountRatio() {
return parseFloat(this.data.xyp_cpl0073) || 0
},
recent20PaymentRatio() {
return parseFloat(this.data.xyp_t0400002) || 0
},
recent20SmallLoanRatio() {
return parseFloat(this.data.xyp_cpl0075) || 0
},
recent90DayRatio() {
return parseFloat(this.data.xyp_cpl0080) || 0
},
recent90AmountRatio() {
return parseFloat(this.data.xyp_cpl0079) || 0
},
recent50PaymentRatio() {
return parseFloat(this.data.xyp_t0400003) || 0
},
recent100PaymentRatio() {
return parseFloat(this.data.xyp_t0400004) || 0
}
},
mounted() {
this.initChart()
window.addEventListener('resize', this.handleResize)
},
beforeUnmount() {
if (this.chartInstance) {
this.chartInstance.dispose()
this.chartInstance = null
}
window.removeEventListener('resize', this.handleResize)
},
watch: {
creditScoreDisplay() {
this.updateChart()
}
},
methods: {
initChart() {
if (!this.$refs.chartRef) return
this.chartInstance = echarts.init(this.$refs.chartRef)
this.updateChart()
},
updateChart() {
if (!this.chartInstance) return
const scoreColor = this.creditScoreColor
const gradientColors = this.getGradientColors()
const option = {
series: [
{
type: 'gauge',
startAngle: 180,
endAngle: 0,
min: 150,
max: 1000,
radius: '100%',
center: ['50%', '80%'],
itemStyle: {
color: scoreColor,
shadowBlur: 6,
shadowColor: scoreColor
},
progress: {
show: true,
width: 20,
roundCap: true,
clip: false
},
axisLine: {
roundCap: true,
lineStyle: {
width: 20,
color: [
[1, new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: scoreColor + '30' },
{ offset: 1, color: scoreColor + '25' }
])]
]
}
},
axisTick: {
show: true,
distance: -30,
length: 6,
splitNumber: 10,
lineStyle: {
color: scoreColor,
width: 1,
opacity: 0.5
}
},
splitLine: {
show: true,
distance: -36,
length: 12,
splitNumber: 9,
lineStyle: {
color: scoreColor,
width: 2,
opacity: 0.5
}
},
axisLabel: {
show: false
},
anchor: {
show: false
},
pointer: {
show: false
},
detail: {
valueAnimation: true,
fontSize: 30,
fontWeight: 'bold',
color: scoreColor,
offsetCenter: [0, 0],
formatter: (value) => {
return `{value|${value}}{label|信用分}`
},
rich: {
value: {
fontSize: 30,
fontWeight: 'bold',
color: scoreColor,
padding: [0, 0, 5, 10]
},
label: {
fontSize: 14,
fontWeight: 'normal',
color: scoreColor,
padding: [0, 0, 0, 5]
}
}
},
data: [
{
value: this.creditScoreDisplay
}
]
}
]
}
this.chartInstance.setOption(option)
},
getGradientColors() {
const color = this.creditScoreColor
return [
{ offset: 0, color: color },
{ offset: 1, color: this.lightenColor(color, 0.3) }
]
},
lightenColor(color, amount) {
const num = parseInt(color.replace('#', ''), 16)
const r = Math.min(255, (num >> 16) + amount * 255)
const g = Math.min(255, ((num >> 8) & 0x00ff) + amount * 255)
const b = Math.min(255, (num & 0x0000ff) + amount * 255)
return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`
},
handleResize() {
if (this.chartInstance) {
this.chartInstance.resize()
}
},
formatModelScore(score) {
return score === null ? '未命中' : score.toString()
},
getScoreClass(score) {
if (score === null) return 'text-gray-400'
if (score >= 750) return 'text-green-600'
if (score >= 650) return 'text-yellow-600'
return 'text-red-600'
},
getScoreTextClass() {
if (this.creditScoreDisplay >= 750) return 'text-green-600'
if (this.creditScoreDisplay >= 650) return 'text-yellow-600'
return 'text-red-600'
},
getScoreColor() {
return this.creditScoreColor
},
getLightScoreColor() {
const color = this.creditScoreColor
// 将颜色转换为淡色版本(增加透明度)
if (color === '#1FBE5D') return '#E8F8F0' // 绿色淡色
if (color === '#f59e0b') return '#FEF3C7' // 黄色淡色
if (color === '#ef4444') return '#FEE2E2' // 红色淡色
return '#F3F4F6' // 默认灰色
},
getModelIcon(score) {
// 如果分数为null或未命中返回灰色图标
if (score === null) {
return new URL('@/assets/images/report/wmz.png', import.meta.url).href
}
// 如果命中,返回绿色图标
return new URL('@/assets/images/report/zq.png', import.meta.url).href
},
getModelCardClass(score) {
// 如果分数为null或未命中返回未命中样式
if (score === null) {
return 'bg-[#F0F0F0] rounded-lg p-4 border border-[#D4D4D4] relative'
}
// 如果命中,返回默认样式
return 'bg-gray-50 rounded-lg p-4 border border-gray-200 relative'
},
getCircleStyle(ratio) {
let color = '#ef4444'
if (ratio >= 0.8) color = '#10b981'
else if (ratio >= 0.6) color = '#f59e0b'
// 确保至少显示10度让用户知道是图表
const minDegree = 10
const actualDegree = Math.max(ratio * 360, minDegree)
return {
background: `conic-gradient(${color} ${actualDegree}deg, #e5e7eb 0deg)`
}
},
getCreditScoreLevel() {
if (this.creditScoreDisplay >= 800) return '优秀'
if (this.creditScoreDisplay >= 700) return '良好'
if (this.creditScoreDisplay >= 600) return '一般'
if (this.creditScoreDisplay >= 500) return '较差'
return '很差'
},
getCreditScoreBadgeClass() {
if (this.creditScoreDisplay >= 800) return 'bg-[#1FBE5D] text-white'
if (this.creditScoreDisplay >= 700) return 'bg-blue-600 text-white'
if (this.creditScoreDisplay >= 600) return 'bg-yellow-600 text-white'
if (this.creditScoreDisplay >= 500) return 'bg-orange-600 text-white'
return 'bg-red-600 text-white'
}
}
}
</script>
<style scoped>
/* 组件容器样式 */
.credit-scores {
background: white;
border-radius: 0.75rem;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
border: 1px solid #e5e7eb;
padding: 1.5rem;
margin-bottom: 1.25rem;
transition: all 0.3s ease;
}
.credit-scores: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(-0.25rem);
}
.section-spacing {
height: 1.25rem;
}
/* 响应式设计 */
@media (max-width: 768px) {
.credit-scores {
padding: 1rem;
}
}
@media (max-width: 480px) {
.credit-scores {
padding: 0.75rem;
}
}
</style>

View File

@@ -0,0 +1,372 @@
<template>
<div class="">
<div class="rounded-lg border border-[#99999933] mb-4">
<div class="pb-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/jgfx.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" />
<p class="text-gray-400 text-sm mb-4 px-4">不同类型贷款机构数量统计</p>
<div class="space-y-3 px-4">
<!-- 消费金融类 -->
<div class="bg-[#ECF2FD] rounded-lg p-4 border border-[#CADAF9] relative">
<div class="flex items-center">
<div class="flex-1">
<div class="font-bold text-[#333333] mb-1">消费金融类</div>
<div class="text-sm text-[#999999]">有场景分期贷款</div>
</div>
</div>
<div class="absolute top-0 right-0 bg-[#5079EA] text-white px-2 py-1 rounded-bl-lg rounded-tr-lg text-xs">
{{ formatMetricValue(consumerFinanceInstitutions) }} 家机构
</div>
</div>
<!-- 小贷担保类 -->
<div class="bg-[#ECF2FD] rounded-lg p-4 border border-[#CADAF9] relative">
<div class="flex items-center">
<div class="flex-1">
<div class="font-bold text-[#333333] mb-1">小贷担保类</div>
<div class="text-sm text-[#999999]">现金贷等小额贷款</div>
</div>
</div>
<div class="absolute top-0 right-0 bg-[#5079EA] text-white px-2 py-1 rounded-bl-lg rounded-tr-lg text-xs">
{{ formatMetricValue(smallLoanSuccessInstitutions) }} 家机构
</div>
</div>
<!-- 网络贷款类 -->
<div class="bg-[#ECF2FD] rounded-lg p-4 border border-[#CADAF9] relative">
<div class="flex items-center">
<div class="flex-1">
<div class="font-bold text-[#333333] mb-1">网络贷款类</div>
<div class="text-sm text-[#999999]">网络现金贷</div>
</div>
</div>
<div class="absolute top-0 right-0 bg-[#5079EA] text-white px-2 py-1 rounded-bl-lg rounded-tr-lg text-xs">
{{ formatMetricValue(networkLoanInstitutions) }} 家机构
</div>
</div>
</div>
</div>
<!-- 交易金额统计 -->
<div class="mb-6">
<LTitle title="交易金额统计" class="mb-2" />
<p class="text-gray-400 text-sm mb-4 px-4">不同时间段的交易金额分析</p>
<!-- 数据表格 -->
<div class="mb-4 border border-gray-200 rounded-lg overflow-hidden mx-4">
<!-- 表头 -->
<div class="bg-[#922D2A] text-white">
<div class="grid grid-cols-5 text-sm" style="grid-template-columns: 1.5fr 1fr 1fr 1fr 1fr;">
<div class="py-3 px-4 text-left font-semibold border-r border-white whitespace-nowrap">时间段</div>
<div class="py-3 px-4 text-center font-semibold border-r border-white whitespace-nowrap">最大</div>
<div class="py-3 px-4 text-center font-semibold border-r border-white whitespace-nowrap">最小</div>
<div class="py-3 px-4 text-center font-semibold border-r border-white whitespace-nowrap">平均</div>
<div class="py-3 px-4 text-center font-semibold whitespace-nowrap">总计</div>
</div>
</div>
<!-- 数据行 -->
<div class="bg-white">
<!-- 近5次 -->
<div class="grid grid-cols-5 border-b border-gray-200 text-sm"
style="grid-template-columns: 1.5fr 1fr 1fr 1fr 1fr;">
<div class="py-3 px-4 font-medium text-gray-800 border-r border-gray-200 whitespace-nowrap">近5次</div>
<div class="py-3 px-4 text-center text-[#333333] border-r border-gray-200 whitespace-nowrap">
{{ formatAmount(transactionAmountStats.recent5.max) }}
</div>
<div class="py-3 px-4 text-center text-[#333333] border-r border-gray-200 whitespace-nowrap">
{{ formatAmount(transactionAmountStats.recent5.min) }}
</div>
<div class="py-3 px-4 text-center text-[#333333] border-r border-gray-200 whitespace-nowrap">
{{ formatAmount(transactionAmountStats.recent5.avg) }}
</div>
<div class="py-3 px-4 text-center text-[#333333] font-semibold whitespace-nowrap">
{{ formatAmount(transactionAmountStats.recent5.sum) }}
</div>
</div>
<!-- 近20次 -->
<div class="grid grid-cols-5 border-b border-gray-200 text-sm"
style="grid-template-columns: 1.5fr 1fr 1fr 1fr 1fr;">
<div class="py-3 px-4 font-medium text-gray-800 border-r border-gray-200 whitespace-nowrap">近20次</div>
<div class="py-3 px-4 text-center text-[#333333] border-r border-gray-200 whitespace-nowrap">
{{ formatAmount(transactionAmountStats.recent20.max) }}
</div>
<div class="py-3 px-4 text-center text-[#333333] border-r border-gray-200 whitespace-nowrap">
{{ formatAmount(transactionAmountStats.recent20.min) }}
</div>
<div class="py-3 px-4 text-center text-[#333333] border-r border-gray-200 whitespace-nowrap">
{{ formatAmount(transactionAmountStats.recent20.avg) }}
</div>
<div class="py-3 px-4 text-center text-[#333333] font-semibold whitespace-nowrap">
{{ formatAmount(transactionAmountStats.recent20.sum) }}
</div>
</div>
<!-- 近50次 -->
<div class="grid grid-cols-5 border-b border-gray-200 text-sm"
style="grid-template-columns: 1.5fr 1fr 1fr 1fr 1fr;">
<div class="py-3 px-4 font-medium text-gray-800 border-r border-gray-200 whitespace-nowrap">近50次</div>
<div class="py-3 px-4 text-center text-[#333333] border-r border-gray-200 whitespace-nowrap">
{{ formatAmount(transactionAmountStats.recent50.max) }}
</div>
<div class="py-3 px-4 text-center text-[#333333] border-r border-gray-200 whitespace-nowrap">
{{ formatAmount(transactionAmountStats.recent50.min) }}
</div>
<div class="py-3 px-4 text-center text-[#333333] border-r border-gray-200 whitespace-nowrap">
{{ formatAmount(transactionAmountStats.recent50.avg) }}
</div>
<div class="py-3 px-4 text-center text-[#333333] font-semibold whitespace-nowrap">
{{ formatAmount(transactionAmountStats.recent50.sum) }}
</div>
</div>
<!-- 近100次 -->
<div class="grid grid-cols-5 text-sm" style="grid-template-columns: 1.5fr 1fr 1fr 1fr 1fr;">
<div class="py-3 px-4 font-medium text-gray-800 border-r border-gray-200 whitespace-nowrap">近100次</div>
<div class="py-3 px-4 text-center text-[#333333] border-r border-gray-200 whitespace-nowrap">
{{ formatAmount(transactionAmountStats.recent100.max) }}
</div>
<div class="py-3 px-4 text-center text-[#333333] border-r border-gray-200 whitespace-nowrap">
{{ formatAmount(transactionAmountStats.recent100.min) }}
</div>
<div class="py-3 px-4 text-center text-[#333333] border-r border-gray-200 whitespace-nowrap">
{{ formatAmount(transactionAmountStats.recent100.avg) }}
</div>
<div class="py-3 px-4 text-center text-[#333333] font-semibold whitespace-nowrap">
{{ formatAmount(transactionAmountStats.recent100.sum) }}
</div>
</div>
</div>
</div>
</div>
<!-- 机构风险评估 -->
<div class="mb-6">
<LTitle title="机构风险评估" class="mb-2" />
<p class="text-gray-400 text-sm mb-4 px-4">不同风险等级的机构分布</p>
<div class="space-y-3 px-4">
<!-- 高风险 -->
<div class="bg-[#FFF0F0] rounded-lg p-4 border border-[#F0CACA] relative">
<div class="flex items-center">
<div class="flex-1">
<div class="font-bold text-[#333333] mb-1">高风险</div>
<div class="text-sm text-[#999999]">多次失败</div>
</div>
</div>
<div class="absolute top-0 right-0 bg-[#D44643] text-white px-2 py-1 rounded-bl-lg rounded-tr-lg text-xs">
{{ formatMetricValue(highRiskInstitutions) }}
</div>
</div>
<!-- 中风险 -->
<div class="bg-[#FFF8E7] rounded-lg p-4 border border-[#F5D980] relative">
<div class="flex items-center">
<div class="flex-1">
<div class="font-bold text-[#333333] mb-1">中风险</div>
<div class="text-sm text-[#999999]">偶有失败</div>
</div>
</div>
<div class="absolute top-0 right-0 bg-[#F5A623] text-white px-2 py-1 rounded-bl-lg rounded-tr-lg text-xs">
{{ formatMetricValue(mediumRiskInstitutions) }}
</div>
</div>
<!-- 低风险 -->
<div class="bg-[#ECF9EF] rounded-lg p-4 border border-[#CAECD3] relative">
<div class="flex items-center">
<div class="flex-1">
<div class="font-bold text-[#333333] mb-1">低风险</div>
<div class="text-sm text-[#999999]">记录良好</div>
</div>
</div>
<div class="absolute top-0 right-0 bg-[#5EBC62] text-white px-2 py-1 rounded-bl-lg rounded-tr-lg text-xs">
{{ formatMetricValue(lowRiskInstitutions) }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 温馨提示 -->
<LRemark
content="机构分析提供申请人在不同类型金融机构中的借贷表现和风险情况。包括消费金融类、小贷担保类和网络贷款类机构的数量统计和交易金额分析。通过机构分布情况可以了解申请人的借贷偏好和风险集中度。建议关注机构数量过多或单一机构集中度过高的情况,这可能暗示过度借贷或特定风险。" />
</template>
<script>
import LTitle from '@/components/LTitle.vue'
import LRemark from '@/components/LRemark.vue'
export default {
name: 'InstitutionAnalysis',
components: {
LTitle,
LRemark
},
props: {
data: {
type: Object,
default: () => ({})
}
},
computed: {
// 消费金融类机构数
consumerFinanceInstitutions() {
return this.parseIntervalValue(this.data.xyp_cpl0007)
},
// 小贷担保类成功还款机构数
smallLoanSuccessInstitutions() {
return this.parseIntervalValue(this.data.xyp_t01degzbc)
},
// 网络贷款类机构数(估算)
networkLoanInstitutions() {
const total = this.parseIntervalValue(this.data.xyp_cpl0001)
const consumer = this.consumerFinanceInstitutions
const smallLoan = this.parseIntervalValue(this.data.xyp_cpl0008)
return Math.max(0, total - consumer - smallLoan)
},
// 交易金额统计(最大值、最小值、平均值)
transactionAmountStats() {
return {
// 近5次交易
recent5: {
max: this.parseIntervalValue(this.data.xyp_t01aaizzz),
min: this.parseIntervalValue(this.data.xyp_t01abizbz),
avg: this.parseIntervalValue(this.data.xyp_t01adizzz),
sum: this.parseIntervalValue(this.data.xyp_t01acizzz)
},
// 近20次交易
recent20: {
max: this.parseIntervalValue(this.data.xyp_t01aajzzc),
min: this.parseIntervalValue(this.data.xyp_t01abjzzc),
avg: this.parseIntervalValue(this.data.xyp_t01adjzzc),
sum: this.parseIntervalValue(this.data.xyp_t01acjzzz)
},
// 近50次交易
recent50: {
max: this.parseIntervalValue(this.data.xyp_t01aakzzz),
min: this.parseIntervalValue(this.data.xyp_t01abkzbz),
avg: this.parseIntervalValue(this.data.xyp_t01adkzzc),
sum: this.parseIntervalValue(this.data.xyp_t01ackzzz)
},
// 近100次交易
recent100: {
max: this.parseIntervalValue(this.data.xyp_t01aalzzz),
min: this.parseIntervalValue(this.data.xyp_t01ablzbc),
avg: this.parseIntervalValue(this.data.xyp_t01adlzzc),
sum: this.parseIntervalValue(this.data.xyp_t01aclzzz)
}
}
},
// 机构风险评估
highRiskInstitutions() {
// 基于交易失败机构数估算
return this.parseIntervalValue(this.data.xyp_t03td111) || 0
},
mediumRiskInstitutions() {
// 基于部分失败机构数估算
const total = this.parseIntervalValue(this.data.xyp_cpl0001)
const high = this.highRiskInstitutions
const low = this.lowRiskInstitutions
return Math.max(0, total - high - low)
},
lowRiskInstitutions() {
// 基于成功还款机构数
return this.parseIntervalValue(this.data.xyp_t01degzzc) || 0
}
},
methods: {
parseIntervalValue(value) {
if (value === null || value === undefined || value === '') return 0
const num = parseInt(value)
if (isNaN(num)) return 0
// 根据区间值返回中位数估算
switch (num) {
case 1: return 2
case 2: return 7
case 3: return 15
case 4: return 25
case 5: return 35
default: return num
}
},
formatMetricValue(value) {
if (value === 0) return '0'
if (value < 5) return `${value}`
return `${value}+`
},
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)}万元`
}
}
}
</script>
<style scoped>
.institution-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;
}
.institution-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);
}
/* 表格样式 */
.border-b:last-child {
@apply border-b-0;
}
/* 响应式设计 */
@media (max-width: 768px) {
.institution-analysis {
padding: 16px;
}
.grid {
grid-template-columns: 1fr;
gap: 1rem;
}
}
@media (max-width: 480px) {
.institution-analysis {
padding: 12px;
}
}
</style>

View File

@@ -0,0 +1,715 @@
<template>
<div class="">
<div class="rounded-lg border border-[#99999933] mb-4">
<div class="pb-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/dkxwfx.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" />
<p class="text-gray-400 text-sm mb-4 px-4">不同类型贷款机构数量统计</p>
<div class="space-y-3 px-4">
<!-- 消费金融类 -->
<div class="bg-[#ECF2FD] rounded-lg p-4 border border-[#CADAF9] relative">
<div class="flex items-center">
<div class="flex-1">
<div class="font-bold text-[#333333] mb-1">消费金融类</div>
<div class="text-sm text-[#999999]">有场景分期贷款</div>
</div>
</div>
<div class="absolute top-0 right-0 bg-[#5079EA] text-white px-2 py-1 rounded-bl-lg rounded-tr-lg text-xs">
{{ formatMetricValue(consumerFinanceInstitutions) }} 家机构
</div>
</div>
<!-- 小贷担保类 -->
<div class="bg-[#ECF2FD] rounded-lg p-4 border border-[#CADAF9] relative">
<div class="flex items-center">
<div class="flex-1">
<div class="font-bold text-[#333333] mb-1">小贷担保类</div>
<div class="text-sm text-[#999999]">现金贷等小额贷款</div>
</div>
</div>
<div class="absolute top-0 right-0 bg-[#5079EA] text-white px-2 py-1 rounded-bl-lg rounded-tr-lg text-xs">
{{ formatMetricValue(smallLoanInstitutions) }} 家机构
</div>
</div>
<!-- 网络贷款类 -->
<div class="bg-[#ECF2FD] rounded-lg p-4 border border-[#CADAF9] relative">
<div class="flex items-center">
<div class="flex-1">
<div class="font-bold text-[#333333] mb-1">网络贷款类</div>
<div class="text-sm text-[#999999]">网络现金贷</div>
</div>
</div>
<div class="absolute top-0 right-0 bg-[#5079EA] text-white px-2 py-1 rounded-bl-lg rounded-tr-lg text-xs">
{{ formatMetricValue(networkLoanInstitutions) }} 家机构
</div>
</div>
</div>
</div>
<!-- 还款表现统计 -->
<div class="">
<LTitle title="还款表现统计" class="mb-2" />
<p class="text-gray-400 text-sm mb-4 px-4">历史还款成功与失败记录</p>
<div class="space-y-3 px-4">
<!-- 历史成功还款 -->
<div class="bg-[#ECF9EF] rounded-lg p-4 border border-[#CAECD3] relative">
<div class="flex items-center">
<div class="flex-1">
<div class="font-bold text-[#333333] mb-1">历史成功还款</div>
<div class="text-sm text-[#999999]">成功还款记录</div>
</div>
</div>
<div class="absolute top-0 right-0 bg-[#5EBC62] text-white px-2 py-1 rounded-bl-lg rounded-tr-lg text-xs">
{{ formatMetricValue(historicalSuccessPayments) }}
</div>
</div>
<!-- 历史交易失败 -->
<div class="bg-[#F9ECEC] rounded-lg p-4 border border-[#F0CACA] relative">
<div class="flex items-center">
<div class="flex-1">
<div class="font-bold text-[#333333] mb-1">历史交易失败</div>
<div class="text-sm text-[#999999]">失败交易记录</div>
</div>
</div>
<div class="absolute top-0 right-0 bg-[#D44643] text-white px-2 py-1 rounded-bl-lg rounded-tr-lg text-xs">
{{ formatMetricValue(historicalFailurePayments) }}
</div>
</div>
<!-- 整体成功率 -->
<div class="bg-[#ECF2FD] rounded-lg p-4 border border-[#CADAF9] relative">
<div class="flex items-center">
<div class="flex-1">
<div class="font-bold text-[#333333] mb-1">整体成功率</div>
<div class="text-sm text-[#999999]">还款成功比例</div>
</div>
</div>
<div class="absolute top-0 right-0 bg-[#5079EA] text-white px-2 py-1 rounded-bl-lg rounded-tr-lg text-xs">
{{ overallSuccessRate }} %
</div>
</div>
</div>
</div>
</div>
<!-- 时间维度还款分析 -->
<div class="mb-6">
<LTitle title="时间维度还款分析" class="mb-2" />
<p class="text-gray-400 text-sm mb-4 px-4">不同时间段的还款成功和失败统计</p>
<!-- ECharts 图表 -->
<div class="mb-6">
<div ref="chartRef" :style="{ width: '100%', height: '300px' }"></div>
</div>
<!-- 标签页布局 -->
<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="activeTimePeriod" line-width="20" line-height="2"
color="var(--color-primary)" class="loan-evaluation-tabs">
<van-tab v-for="period in timePeriods" :key="period.name" :name="period.name"
:title="period.name" />
</van-tabs>
</div>
<!-- 内容显示 -->
<div class="loan-evaluation-content">
<!-- 总笔数 -->
<div class="text-lg text-gray-800 mb-3">
<span class="font-bold">{{ currentPeriod.total }}</span>
</div>
<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">{{ currentPeriod.successRate.toFixed(1) }}%</span>
</div>
<div class="h-2 rounded-full" :style="`background-color: ${getSuccessRateLightColor()}`">
<div class="h-2 rounded-full transition-all duration-500"
:style="`width: ${Math.max(currentPeriod.successRate, 2)}%; background-color: ${getSuccessRateColor()}`">
</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">{{ currentPeriod.success }} </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">{{ currentPeriod.failure }} </span>
</div>
<!-- 成功金额 -->
<div class="flex justify-between items-center" v-if="currentPeriod.amounts">
<span class="text-sm text-gray-600">成功金额</span>
<span class="text-sm font-bold text-gray-800">{{ formatAmount(currentPeriod.amounts.success)
}}</span>
</div>
<!-- 失败金额 -->
<div class="flex justify-between items-center" v-if="currentPeriod.amounts">
<span class="text-sm text-gray-600">失败金额</span>
<span class="text-sm font-bold text-gray-800">{{ formatAmount(currentPeriod.amounts.failure)
}}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 逾期行为分析 -->
<div class="mb-6">
<LTitle title="逾期行为分析" class="mb-2" />
<p class="text-gray-400 text-sm mb-4 px-4">各时间段逾期情况统计</p>
<div class="space-y-3 px-4">
<div class="rounded-xl p-4 relative" :class="getOverdueTimelineCardClass(item.hasOverdue)"
v-for="item in overdueTimeline" :key="item.period">
<div class="absolute top-0 right-0">
<div class="px-2 py-1 text-xs text-white rounded-bl-xl rounded-tr-xl"
:class="getOverdueTimelineTagClass(item.hasOverdue)">
{{ item.hasOverdue ? '有逾期' : '无逾期' }}
</div>
</div>
<div class="flex items-center">
<div class="w-10 h-10 mr-4">
<img :src="getOverdueTimelineIcon(item.hasOverdue)" :alt="item.hasOverdue ? '有逾期' : '无逾期'"
class="w-10 h-10 object-contain" />
</div>
<div class="flex-1">
<div class="font-bold text-gray-800">{{ item.period }}</div>
<div class="text-sm text-[#999999] mt-1">{{ item.description }}</div>
</div>
</div>
</div>
</div>
</div>
<!-- 当前逾期详情 -->
<div class="mb-6" v-if="hasCurrentOverdue">
<LTitle title="当前逾期提醒" class="mb-2" />
<p class="text-gray-400 text-sm mb-4 px-4">需要立即处理的逾期情况</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 px-4">
<div class="bg-[#F9ECEC] rounded-lg p-4 border border-[#F0CACA]">
<div class="flex items-center justify-between">
<span class="text-[#999999] text-sm">逾期机构数量</span>
<span class="font-bold text-[#D44643] text-lg">{{ formatMetricValue(currentOverdueInstitutions) }}</span>
</div>
</div>
<div class="bg-[#F9ECEC] rounded-lg p-4 border border-[#F0CACA]">
<div class="flex items-center justify-between">
<span class="text-[#999999] text-sm">逾期金额</span>
<span class="font-bold text-[#D44643] text-lg">{{ formatAmount(currentOverdueAmount) }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 温馨提示 -->
<LRemark
content="贷款行为分析通过多维度数据展示申请人的借贷行为模式,包括机构类型分布、交易金额分析、申请频率统计和风险事件分析。消费金融类、小贷担保类和网络贷款类机构的数据分别统计,有助于了解申请人的借贷偏好。建议重点关注短期内频繁申请和大额借贷行为,这可能暗示资金紧张或过度借贷风险。分析结果可为风险评估提供重要参考。" />
</template>
<script>
import LTitle from '@/components/LTitle.vue'
import LRemark from '@/components/LRemark.vue'
import * as echarts from 'echarts'
export default {
name: 'LoanBehaviorAnalysis',
components: {
LTitle,
LRemark
},
props: {
data: {
type: Object,
default: () => ({})
}
},
data() {
return {
chartInstance: null,
activeTimePeriod: '最近1天'
}
},
computed: {
// 平均成功率
averageSuccessRate() {
if (this.timePeriods.length === 0) return 0
const totalRate = this.timePeriods.reduce((sum, period) => sum + period.successRate, 0)
return totalRate / this.timePeriods.length
},
// 最高成功率
maxSuccessRate() {
if (this.timePeriods.length === 0) return 0
return Math.max(...this.timePeriods.map(period => period.successRate))
},
// 最低成功率
minSuccessRate() {
if (this.timePeriods.length === 0) return 0
return Math.min(...this.timePeriods.map(period => period.successRate))
},
// 机构类型统计
consumerFinanceInstitutions() {
return this.parseIntervalValue(this.data.xyp_cpl0007)
},
smallLoanInstitutions() {
// 通过总机构数减去消费金融和网络贷款推算
const total = this.parseIntervalValue(this.data.xyp_cpl0001)
const consumer = this.consumerFinanceInstitutions
const network = this.networkLoanInstitutions
return Math.max(0, total - consumer - network)
},
networkLoanInstitutions() {
return this.parseIntervalValue(this.data.xyp_cpl0008)
},
// 机构类型比例
totalInstitutions() {
return this.consumerFinanceInstitutions + this.smallLoanInstitutions + this.networkLoanInstitutions || 1
},
consumerFinanceRatio() {
return this.consumerFinanceInstitutions / this.totalInstitutions
},
smallLoanRatio() {
return this.smallLoanInstitutions / this.totalInstitutions
},
networkLoanRatio() {
return this.networkLoanInstitutions / this.totalInstitutions
},
// 还款表现统计
historicalSuccessPayments() {
return this.parseIntervalValue(this.data.xyp_cpl0014)
},
historicalFailurePayments() {
return this.parseIntervalValue(this.data.xyp_cpl0015)
},
overallSuccessRate() {
const total = this.historicalSuccessPayments + this.historicalFailurePayments
if (total === 0) return 0
return Math.round((this.historicalSuccessPayments / total) * 100)
},
// 时间维度分析
timePeriods() {
return [
{
name: '最近1天',
success: this.parseIntervalValue(this.data.xyp_cpl0017),
failure: this.parseIntervalValue(this.data.xyp_cpl0016),
amounts: {
success: this.parseIntervalValue(this.data.xyp_cpl0033),
failure: this.parseIntervalValue(this.data.xyp_cpl0032)
}
},
{
name: '最近7天',
success: this.parseIntervalValue(this.data.xyp_cpl0019),
failure: this.parseIntervalValue(this.data.xyp_cpl0018),
amounts: {
success: this.parseIntervalValue(this.data.xyp_cpl0035),
failure: this.parseIntervalValue(this.data.xyp_cpl0034)
}
},
{
name: '最近14天',
success: this.parseIntervalValue(this.data.xyp_cpl0021),
failure: this.parseIntervalValue(this.data.xyp_cpl0020),
amounts: {
success: this.parseIntervalValue(this.data.xyp_cpl0037),
failure: this.parseIntervalValue(this.data.xyp_cpl0036)
}
},
{
name: '最近21天',
success: this.parseIntervalValue(this.data.xyp_cpl0064),
failure: this.parseIntervalValue(this.data.xyp_cpl0065),
amounts: {
success: this.parseIntervalValue(this.data.xyp_cpl0067),
failure: this.parseIntervalValue(this.data.xyp_cpl0066)
}
},
{
name: '最近30天',
success: this.parseIntervalValue(this.data.xyp_cpl0023),
failure: this.parseIntervalValue(this.data.xyp_cpl0022),
amounts: {
success: this.parseIntervalValue(this.data.xyp_cpl0039),
failure: this.parseIntervalValue(this.data.xyp_cpl0038)
}
},
{
name: '最近90天',
success: this.parseIntervalValue(this.data.xyp_cpl0025),
failure: this.parseIntervalValue(this.data.xyp_cpl0024),
amounts: {
success: this.parseIntervalValue(this.data.xyp_cpl0041),
failure: this.parseIntervalValue(this.data.xyp_cpl0040)
}
},
{
name: '最近180天',
success: this.parseIntervalValue(this.data.xyp_cpl0027),
failure: this.parseIntervalValue(this.data.xyp_cpl0026),
amounts: {
success: this.parseIntervalValue(this.data.xyp_cpl0043),
failure: this.parseIntervalValue(this.data.xyp_cpl0042)
}
}
].map(period => {
const total = period.success + period.failure || 1
return {
...period,
total,
successRate: (period.success / total) * 100,
failureRate: (period.failure / total) * 100
}
})
},
// 逾期时间线
overdueTimeline() {
return [
{
period: '最近1天',
hasOverdue: this.data.xyp_cpl0028 === '1',
description: this.data.xyp_cpl0028 === '1' ? '检测到逾期行为' : '无逾期记录'
},
{
period: '最近7天',
hasOverdue: this.data.xyp_cpl0029 === '1',
description: this.data.xyp_cpl0029 === '1' ? '检测到逾期行为' : '无逾期记录'
},
{
period: '最近14天',
hasOverdue: this.data.xyp_cpl0030 === '1',
description: this.data.xyp_cpl0030 === '1' ? '检测到逾期行为' : '无逾期记录'
},
{
period: '最近30天',
hasOverdue: this.data.xyp_cpl0031 === '1',
description: this.data.xyp_cpl0031 === '1' ? '检测到逾期行为' : '无逾期记录'
}
]
},
// 当前逾期状态
hasCurrentOverdue() {
return this.data.xyp_cpl0044 === '1'
},
currentOverdueInstitutions() {
return this.parseIntervalValue(this.data.xyp_cpl0071)
},
currentOverdueAmount() {
return this.parseIntervalValue(this.data.xyp_cpl0072)
},
// 当前选中的时间段
currentPeriod() {
return this.timePeriods.find(p => p.name === this.activeTimePeriod) || this.timePeriods[0]
}
},
mounted() {
this.initChart()
window.addEventListener('resize', this.handleResize)
},
beforeUnmount() {
if (this.chartInstance) {
this.chartInstance.dispose()
this.chartInstance = null
}
window.removeEventListener('resize', this.handleResize)
},
watch: {
timePeriods() {
this.updateChart()
}
},
methods: {
initChart() {
if (!this.$refs.chartRef) return
this.chartInstance = echarts.init(this.$refs.chartRef)
this.updateChart()
},
updateChart() {
if (!this.chartInstance) return
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
formatter: (params) => {
const data = params[0]
const period = this.timePeriods[data.dataIndex]
return `${data.name}<br/>成功率: ${period.successRate.toFixed(1)}%<br/>${period.success}/${period.total}`
}
},
grid: {
left: '3%',
right: '4%',
bottom: '15%',
top: '5%',
containLabel: true
},
xAxis: {
type: 'category',
data: this.timePeriods.map(p => p.name),
axisLabel: {
rotate: 45,
fontSize: 12,
color: '#666'
},
axisLine: {
lineStyle: {
color: '#e0e0e0'
}
}
},
yAxis: {
type: 'value',
max: 100,
axisLabel: {
formatter: '{value}%',
fontSize: 12,
color: '#666'
},
axisLine: {
lineStyle: {
color: '#e0e0e0'
}
},
splitLine: {
lineStyle: {
color: '#f0f0f0'
}
}
},
series: [
{
name: '还款成功率',
type: 'bar',
data: this.timePeriods.map(p => Math.max(p.successRate, 2)),
barWidth: '25%',
barMinHeight: 2,
itemStyle: {
color: '#10b981',
borderRadius: [4, 4, 0, 0]
},
label: {
show: true,
position: 'top',
formatter: (params) => {
const period = this.timePeriods[params.dataIndex]
return period.successRate > 0 ? `${period.successRate.toFixed(1)}%` : '0%'
},
fontSize: 11,
color: '#333'
}
}
]
}
this.chartInstance.setOption(option)
},
handleResize() {
if (this.chartInstance) {
this.chartInstance.resize()
}
},
getSuccessRateColor() {
const rate = this.currentPeriod.successRate
if (rate >= 80) return '#10b981'
if (rate >= 60) return '#f59e0b'
return '#ef4444'
},
getSuccessRateLightColor() {
const rate = this.currentPeriod.successRate
if (rate >= 80) return '#E8F8F0'
if (rate >= 60) return '#FEF3C7'
return '#FEE2E2'
},
getOverdueTimelineCardClass(hasOverdue) {
if (hasOverdue) return 'bg-[#FFF0F0] border border-red-200'
return 'bg-[#F0FFF0] border border-green-200'
},
getOverdueTimelineTagClass(hasOverdue) {
if (hasOverdue) return 'bg-[#E53935]'
return 'bg-[#4CAF50]'
},
getOverdueTimelineIcon(hasOverdue) {
if (hasOverdue) return new URL('@/assets/images/report/gfx.png', import.meta.url).href
return new URL('@/assets/images/report/zq.png', import.meta.url).href
},
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
}
},
formatMetricValue(value) {
if (value === 0) return '0'
if (value < 5) return `${value}`
return `${value}+`
},
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)}万元`
},
getSliceStyle(ratio, startAngle) {
const angle = ratio * 360
return {
'--start-angle': `${startAngle * 360}deg`,
'--end-angle': `${(startAngle + ratio) * 360}deg`,
'--slice-percent': `${ratio * 100}%`
}
}
}
}
</script>
<style scoped>
.loan-behavior-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;
}
.loan-behavior-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) {
.loan-behavior-analysis {
padding: 16px;
}
.grid {
grid-template-columns: 1fr;
gap: 1rem;
}
}
@media (max-width: 480px) {
.loan-behavior-analysis {
padding: 12px;
}
}
</style>

View File

@@ -0,0 +1,414 @@
<template>
<div class="rounded-lg border border-[#99999933] mb-4">
<!-- 标题栏 -->
<div class="flex items-center p-4">
<div class="w-8 h-8 flex items-center justify-center mr-2">
<img src="@/assets/images/report/zyjy.png" alt="专业建议" class="w-8 h-8 object-contain" />
</div>
<span class="font-bold text-gray-800">专业建议</span>
</div>
<!-- 风险评估结论 -->
<div class="mb-6 px-4">
<div class="rounded-xl p-4 relative border" :class="overallRiskLevel.bgClass">
<!-- 风险分标签 -->
<div
class="absolute top-0 right-0 px-3 py-1 rounded-bl-lg rounded-tr-lg text-sm font-bold text-white whitespace-nowrap"
:class="getRiskBadgeClass()">
风险分{{ overallRiskScore }}
</div>
<div class="flex items-center gap-4 mb-3">
<div class="w-10 h-10 flex-shrink-0">
<img :src="getRiskIcon()" :alt="overallRiskLevel.title" class="w-10 h-10 object-contain" />
</div>
<div class="flex-1">
<h3 class="text-base font-bold text-[#333333]">{{ overallRiskLevel.title }}</h3>
<p class="text-sm text-[#999999]">{{ overallRiskLevel.subtitle }}</p>
</div>
</div>
<div class="text-sm text-[#333333] leading-relaxed">
{{ overallRiskLevel.description }}
</div>
</div>
</div>
<!-- 关键建议 -->
<div class="mb-6">
<LTitle title="关键建议" class="mb-2" />
<div class="space-y-3 px-4">
<div class="rounded-xl p-4 relative" v-for="recommendation in keyRecommendations" :key="recommendation.id"
:class="getRecommendationCardClass(recommendation.priority)">
<!-- 优先级标签 -->
<div class="absolute top-0 right-0 px-2 py-1 rounded-bl-lg rounded-tr-lg text-xs font-bold text-white"
:class="getRecommendationBadgeClass(recommendation.priority)">
{{ recommendation.priorityText }}
</div>
<div class="flex items-center gap-3">
<div class="w-10 h-10 flex-shrink-0">
<img :src="getRecommendationIcon(recommendation.priority)" :alt="recommendation.title"
class="w-10 h-10 object-contain" />
</div>
<div class="flex-1 min-w-0">
<h4 class="text-base font-bold text-[#333333] mb-2">{{ recommendation.title }}</h4>
<p class="text-sm text-[#999999] mb-3 leading-relaxed">{{ recommendation.description }}</p>
<div class="flex flex-wrap gap-2" v-if="recommendation.actions.length > 0">
<span class="inline-flex items-center px-3 py-1 rounded-xl text-sm"
:class="getRecommendationActionClass(recommendation.priority)"
v-for="action in recommendation.actions.slice(0, 3)" :key="action">
{{ action }}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 温馨提示 -->
<!-- <div class="px-4 pb-4">
<LRemark
content="专业建议基于综合风险评估结果,为不同风险等级的申请人提供针对性的审核廊议和风险管控措施。建议内容包括关键建议、风险管控措施、注意事项和后续跟进等方面。系统会根据当前风险等级动态调整建议内容,但最终决策仍需结合具体业务情况和风险政策进行综合判断。建议定期复评风险状况和调整风险管控策略。" />
</div> -->
</div>
</template>
<script>
import LTitle from '@/components/LTitle.vue'
import LRemark from '@/components/LRemark.vue'
export default {
name: 'RiskAdvice',
components: {
LTitle,
LRemark
},
props: {
data: {
type: Object,
default: () => ({})
}
},
computed: {
// 综合风险评估
overallRiskScore() {
const creditScore = parseFloat(this.data.xyp_cpl0081) || 0
const amountIndex = parseFloat(this.data.xyp_cpl0082) || 0
const countIndex = parseFloat(this.data.xyp_cpl0083) || 0
// 风险分数 (0-100, 分数越高风险越低)
const avgRisk = (creditScore + amountIndex + countIndex) / 3
return Math.round((1 - avgRisk) * 100)
},
overallRiskLevel() {
const score = this.overallRiskScore
const hasCurrentOverdue = this.data.xyp_cpl0044 === '1'
const hasRecentOverdue = this.data.xyp_cpl0028 === '1' || this.data.xyp_cpl0029 === '1'
if (hasCurrentOverdue || score < 30) {
return {
title: '高风险用户',
subtitle: '需要立即关注',
description: '当前信用状况较差,建议立即处理逾期问题并暂停新申请。',
bgClass: 'bg-red-50 border-red-200',
iconBg: 'bg-red-500',
iconComponent: 'ExclamationTriangleIcon'
}
} else if (hasRecentOverdue || score < 60) {
return {
title: '中风险用户',
subtitle: '需要改善',
description: '信用状况一般,建议优化还款表现并控制申请频率。',
bgClass: 'bg-yellow-50 border-yellow-200',
iconBg: 'bg-yellow-500',
iconComponent: 'ExclamationCircleIcon'
}
} else {
return {
title: '低风险用户',
subtitle: '状况良好',
description: '信用状况良好,建议继续保持良好的还款习惯。',
bgClass: 'bg-green-50 border-green-200',
iconBg: 'bg-green-500',
iconComponent: 'CheckCircleIcon'
}
}
},
// 关键建议
keyRecommendations() {
const recommendations = []
// 当前逾期处理
if (this.data.xyp_cpl0044 === '1') {
recommendations.push({
id: 'handle_overdue',
title: '立即处理逾期',
description: '尽快联系机构协商还款,避免影响征信。',
priority: 'urgent',
priorityText: '紧急',
iconComponent: 'ExclamationTriangleIcon',
iconBg: 'bg-red-500',
borderClass: 'border-l-red-500',
badgeClass: 'bg-red-100 text-red-800',
actions: [
'联系机构协商',
'优先还小额',
'制定还款计划'
]
})
}
// 高频申请警告
const recent1Day = this.parseIntervalValue(this.data.xyp_cpl0070)
const recent7Day = this.parseIntervalValue(this.data.xyp_cpl0009)
if (recent1Day > 0 || recent7Day > 5) {
recommendations.push({
id: 'reduce_applications',
title: '控制申请频率',
description: '近期申请过频建议暂停新申请3-6个月。',
priority: 'high',
priorityText: '重要',
iconComponent: 'PauseCircleIcon',
iconBg: 'bg-orange-500',
borderClass: 'border-l-orange-500',
badgeClass: 'bg-orange-100 text-orange-800',
actions: [
'暂停新申请',
'整理现有贷款',
'制定资金规划'
]
})
}
// 还款表现改善
const successRate = parseFloat(this.data.xyp_cpl0080) || 0
const recent5SuccessRate = parseFloat(this.data.xyp_cpl0074) || 0
const recent20SuccessRate = parseFloat(this.data.xyp_t0400002) || 0
if (successRate < 0.8 || recent5SuccessRate < 0.8 || recent20SuccessRate < 0.8) {
recommendations.push({
id: 'improve_repayment',
title: '提升还款表现',
description: `还款成功率偏低,建议设置自动还款。`,
priority: 'high',
priorityText: '重要',
iconComponent: 'CalendarIcon',
iconBg: 'bg-blue-500',
borderClass: 'border-l-blue-500',
badgeClass: 'bg-blue-100 text-blue-800',
actions: [
'设置自动还款',
'确保账户余额',
'按时还款'
]
})
}
// 机构数量管理
const totalInstitutions = this.parseIntervalValue(this.data.xyp_cpl0001)
if (totalInstitutions > 10) {
recommendations.push({
id: 'manage_institutions',
title: '优化机构数量',
description: '机构数量较多,建议优先结清小额贷款。',
priority: 'medium',
priorityText: '建议',
iconComponent: 'AdjustmentsIcon',
iconBg: 'bg-purple-500',
borderClass: 'border-l-purple-500',
badgeClass: 'bg-purple-100 text-purple-800',
actions: [
'结清小额贷款',
'合并同类贷款',
'控制新增机构'
]
})
}
// 交易失败后恢复分析
const consumerFailureRecoveryDays = this.parseIntervalValue(this.data.xyp_cpl0054)
const smallLoanFailureRecoveryDays = this.parseIntervalValue(this.data.xyp_cpl0055)
const overallFailureRecoveryDays = this.parseIntervalValue(this.data.xyp_cpl0056)
if (consumerFailureRecoveryDays > 30 || smallLoanFailureRecoveryDays > 30 || overallFailureRecoveryDays > 30) {
recommendations.push({
id: 'improve_recovery_time',
title: '快速恢复能力',
description: '失败后恢复较慢,建议建立应急资金。',
priority: 'medium',
priorityText: '建议',
iconComponent: 'ClockIcon',
iconBg: 'bg-indigo-500',
borderClass: 'border-l-indigo-500',
badgeClass: 'bg-indigo-100 text-indigo-800',
actions: [
'建立应急资金',
'优化资金流',
'快速处理失败'
]
})
}
// 信用修复
const settledInstitutions = this.parseIntervalValue(this.data.xyp_cpl0002)
if (settledInstitutions > 0) {
recommendations.push({
id: 'credit_repair',
title: '继续信用修复',
description: '已有良好结清记录,建议继续保持。',
priority: 'medium',
priorityText: '建议',
iconComponent: 'TrendingUpIcon',
iconBg: 'bg-green-500',
borderClass: 'border-l-green-500',
badgeClass: 'bg-green-100 text-green-800',
actions: [
'保持还款记录',
'结清剩余贷款',
'稳定收入来源'
]
})
}
return recommendations
},
// 改善步骤
improvementSteps() {
const steps = []
if (this.data.xyp_cpl0044 === '1') {
steps.push({
id: 'immediate_action',
title: '立即行动期',
description: '处理逾期问题,停止新申请',
duration: '1-2周',
impact: '高',
badgeClass: 'bg-red-100 text-red-800'
})
}
steps.push({
id: 'stabilization',
title: '稳定期',
description: '建立稳定还款计划,按时还款',
duration: '3-6个月',
impact: '中',
badgeClass: 'bg-yellow-100 text-yellow-800'
})
steps.push({
id: 'optimization',
title: '优化期',
description: '减少机构数量,优化债务结构',
duration: '6-12个月',
impact: '中',
badgeClass: 'bg-yellow-100 text-yellow-800'
})
steps.push({
id: 'recovery',
title: '恢复期',
description: '建立良好信用记录,恢复信用状况',
duration: '12-24个月',
impact: '高',
badgeClass: 'bg-green-100 text-green-800'
})
return steps
}
},
methods: {
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
}
},
getRiskIcon() {
// 根据风险等级返回对应的图标
if (this.overallRiskLevel.iconComponent === 'ExclamationTriangleIcon') {
return new URL('@/assets/images/report/gfx.png', import.meta.url).href
} else if (this.overallRiskLevel.iconComponent === 'ExclamationCircleIcon') {
return new URL('@/assets/images/report/zfx.png', import.meta.url).href
} else {
return new URL('@/assets/images/report/zq.png', import.meta.url).href
}
},
getRiskBadgeClass() {
// 根据风险等级返回徽章样式
if (this.overallRiskLevel.iconComponent === 'ExclamationTriangleIcon') {
return 'bg-[#D44643]'
} else if (this.overallRiskLevel.iconComponent === 'ExclamationCircleIcon') {
return 'bg-[#F5A623]'
} else {
return 'bg-[#5EBC62]'
}
},
getRecommendationCardClass(priority) {
// 根据优先级返回卡片样式
if (priority === 'urgent') {
return 'bg-[#FFF0F0] border border-[#F0CACA]'
} else if (priority === 'high') {
return 'bg-[#ECF2FD] border border-[#CADAF9]'
} else {
return 'bg-[#F0FFF0] border border-green-200'
}
},
getRecommendationIcon(priority) {
// 根据优先级返回图标
if (priority === 'urgent') {
return new URL('@/assets/images/report/gfx.png', import.meta.url).href
} else if (priority === 'high') {
return new URL('@/assets/images/report/wxts_icon.png', import.meta.url).href
} else {
return new URL('@/assets/images/report/zq.png', import.meta.url).href
}
},
getRecommendationBadgeClass(priority) {
// 根据优先级返回徽章样式
if (priority === 'urgent') {
return 'bg-[#D44643]'
} else if (priority === 'high') {
return 'bg-[#5079EA]'
} else {
return 'bg-[#5EBC62]'
}
},
getRecommendationActionClass(priority) {
// 根据优先级返回操作按钮样式
if (priority === 'urgent') {
return 'bg-[#F0CACA] text-[#D44643]'
} else if (priority === 'high') {
return 'bg-[#DBE6FC] text-[#2B79EE]'
} else {
return 'bg-green-200 text-[#5EBC62]'
}
}
}
}
</script>
<style scoped>
/* 组件样式已使用 Tailwind CSS */
</style>

View File

@@ -0,0 +1,638 @@
<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/fxzbxq.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" />
<p class="text-xs text-[#999999] px-4 mb-3">关键风险评估指标汇总</p>
<div class="space-y-3 px-4">
<!-- 警示指标 -->
<div class="bg-[#F9F5ED] border border-[#F0E2CB] rounded-xl">
<h4 class="text-base font-bold text-[#333333] border-b border-[#F0E2CB] px-4 py-2">警示指标</h4>
<div class="space-y-2 p-4">
<div class="flex justify-between items-center">
<span class="text-sm text-[#999999]">最近1天申请机构</span>
<span class="text-base font-bold text-[#333333]">{{ formatMetricValue(recent1DayInstitutions) }}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-[#999999]">最近7天申请机构</span>
<span class="text-base font-bold text-[#333333]">{{ formatMetricValue(recent7DayInstitutions) }}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-[#999999]">履约金额指数</span>
<span class="text-base font-bold text-[#333333]">{{ (amountComplianceIndex * 100).toFixed(0) }}%</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-[#999999]">履约笔数指数</span>
<span class="text-base font-bold text-[#333333]">{{ (countComplianceIndex * 100).toFixed(0) }}%</span>
</div>
</div>
</div>
<!-- 正面指标 -->
<div class="bg-[#ECF9EF] border border-[#CAECD3] rounded-xl">
<h4 class="text-base font-bold text-[#333333] px-4 py-2 border-b border-[#CAECD3]">正面指标</h4>
<div class="space-y-2 p-4">
<div class="flex justify-between items-center">
<span class="text-sm text-[#999999]">已结清机构数</span>
<span class="text-base font-bold text-[#333333]">{{ formatMetricValue(settledInstitutions) }}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-[#999999]">信用贷款时长</span>
<span class="text-base font-bold text-[#333333]">{{ formatDays(creditLoanDuration) }}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-[#999999]">历史成功还款</span>
<span class="text-base font-bold text-[#333333]">{{ formatMetricValue(historicalSuccessPayments)
}}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 交易失败后还款分析 -->
<div class="mb-6">
<LTitle title="交易失败后还款分析" class="mb-2" />
<p class="text-xs text-[#999999] px-4 mb-3">失败后的恢复能力评估</p>
<div class="space-y-3 px-4">
<!-- 失败后还款次数 -->
<div class="bg-[#F9F9F9] border border-[#EEEEEE] rounded-xl">
<h4 class="text-base font-bold text-[#333333] px-4 py-2 border-b border-[#EEEEEE]">失败后还款次数</h4>
<div class="space-y-3 p-4">
<div class="flex items-center gap-3">
<span class="text-sm text-[#999999] min-w-20">已结清机构数</span>
<div class="flex-1 h-2 bg-[#E3EFFD] rounded-full overflow-hidden">
<div class="h-full bg-[#2B79EE] rounded-full transition-all duration-300"
:style="`width: ${Math.max(getRecoveryPercentage(consumerFailureRecovery), 2)}%`"></div>
</div>
<span class="text-base font-bold text-[#333333] min-w-12 text-right">{{
formatMetricValue(consumerFailureRecovery) }}</span>
</div>
<div class="flex items-center gap-3">
<span class="text-sm text-[#999999] min-w-20">信用贷款时长</span>
<div class="flex-1 h-2 bg-[#E3EFFD] rounded-full overflow-hidden">
<div class="h-full bg-[#2B79EE] rounded-full transition-all duration-300"
:style="`width: ${Math.max(getRecoveryPercentage(smallLoanFailureRecovery), 2)}%`"></div>
</div>
<span class="text-base font-bold text-[#333333] min-w-12 text-right">{{
formatMetricValue(smallLoanFailureRecovery) }}</span>
</div>
<div class="flex items-center gap-3">
<span class="text-sm text-[#999999] min-w-20">历史成功还款</span>
<div class="flex-1 h-2 bg-[#E3EFFD] rounded-full overflow-hidden">
<div class="h-full bg-[#2B79EE] rounded-full transition-all duration-300"
:style="`width: ${Math.max(getRecoveryPercentage(overallFailureRecovery), 2)}%`"></div>
</div>
<span class="text-base font-bold text-[#333333] min-w-12 text-right">{{
formatMetricValue(overallFailureRecovery) }}</span>
</div>
</div>
</div>
<!-- 恢复时间分析 -->
<div class="bg-[#F9F9F9] border border-[#EEEEEE] rounded-xl">
<h4 class="text-base font-bold text-[#333333] px-4 py-2 border-b border-[#EEEEEE]">恢复时间分析</h4>
<div class="p-4">
<div class="flex justify-between items-center mb-2">
<span class="text-sm text-[#999999]">平均恢复时间</span>
<span class="text-base font-bold text-[#333333]">{{ formatDays(avgRecoveryTime) }}</span>
</div>
<div class="flex justify-between text-sm text-[#999999]">
<span>最短: {{ formatDays(minRecoveryTime) }}</span>
<span>最长: {{ formatDays(maxRecoveryTime) }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 比例指标分析 -->
<div class="mb-6">
<LTitle title="比例指标分析" class="mb-2" />
<p class="text-xs text-[#999999] px-4 mb-3">各类交易行为的比例统计</p>
<div class="space-y-3 px-4">
<!-- 金额比例指标 -->
<div class="text-base font-bold text-[#333333]">金额比例指标</div>
<div class="space-y-3">
<div v-for="item in amountRatios" :key="item.id">
<div class="bg-[#ECF2FD] border border-[#CADAF9] rounded-xl p-4">
<div class="flex justify-between items-center mb-2">
<span class="text-base font-bold text-[#333333]">{{ item.name }}</span>
<span class="text-base font-bold text-[#333333]">{{ (item.ratio * 100).toFixed(1) }}%</span>
</div>
<div class="h-2 bg-[#DBE6FC] rounded-full overflow-hidden mb-1.5">
<div class="h-full rounded-full transition-all duration-300"
:style="`width: ${Math.max(item.ratio * 100, 2)}%; background-color: #5079EA;`"></div>
</div>
<div class="text-sm text-[#999999]">{{ item.description }}</div>
</div>
</div>
</div>
<!-- 笔数比例指标 -->
<div class="text-base font-bold text-[#333333]">笔数比例指标</div>
<div class="space-y-3">
<div v-for="item in countRatios" :key="item.id">
<div class="rounded-xl p-4" :class="getRatioCardClass(item.id)">
<div class="flex justify-between items-center mb-2">
<span class="text-base font-bold text-[#333333]">{{ item.name }}</span>
<span class="text-base font-bold text-[#333333]">{{ (item.ratio * 100).toFixed(1) }}%</span>
</div>
<div class="h-2 rounded-full overflow-hidden mb-1.5" :class="getRatioBarBgClass(item.id)">
<div class="h-full rounded-full transition-all duration-300"
:style="`width: ${Math.max(item.ratio * 100, 2)}%; background-color: ${getRatioBarColor(item.id)};`">
</div>
</div>
<div class="text-sm text-[#999999]">{{ item.description }}</div>
</div>
</div>
</div>
</div>
</div>
<!-- 特殊指标 -->
<div class="mb-6">
<LTitle title="特殊指标" class="mb-2" />
<p class="text-xs text-[#999999] px-4 mb-3">其他重要风险评估指标</p>
<div class="space-y-3 px-4">
<!-- 时间相关指标 -->
<div class="bg-[#F9F9F9] border border-[#EEEEEE] rounded-xl">
<h4 class="text-base font-bold text-[#333333] px-4 py-2 border-b border-[#EEEEEE]">时间相关指标</h4>
<div class="space-y-2 p-4">
<div class="flex justify-between items-center">
<span class="text-sm text-[#999999]">最近一次交易距今</span>
<span class="text-base font-bold text-[#333333]">{{ formatDays(lastTransactionDays) }}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-[#999999]">最近一次还款距今</span>
<span class="text-base font-bold text-[#333333]">{{ formatDays(lastRepaymentDays) }}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-[#999999]">信用贷款总时长</span>
<span class="text-base font-bold text-[#333333]">{{ formatDays(creditLoanDuration) }}</span>
</div>
</div>
</div>
<!-- 交易失败机构 -->
<div class="bg-[#F9F9F9] border border-[#EEEEEE] rounded-xl">
<h4 class="text-base font-bold text-[#333333] px-4 py-2 border-b border-[#EEEEEE]">交易失败机构</h4>
<div class="p-4">
<div class="space-y-2" v-for="item in failureInstitutionTimeline" :key="item.period">
<div class="flex justify-between items-center">
<span class="text-sm text-[#999999]">{{ item.period }}</span>
<span class="text-base font-bold text-[#333333]">{{ formatMetricValue(item.count) }}</span>
</div>
<div class="h-2 bg-[#E3EFFD] rounded-full overflow-hidden">
<div class="h-full rounded-full transition-all duration-300 bg-[#2B79EE]"
:style="`width: ${Math.max(item.percentage, 2)}%`"></div>
</div>
</div>
</div>
</div>
<!-- 新增机构比例 -->
<div class="bg-[#F9F9F9] border border-[#EEEEEE] rounded-xl">
<h4 class="text-base font-bold text-[#333333] px-4 py-2 border-b border-[#EEEEEE]">新增机构比例</h4>
<div class="space-y-2 p-4">
<div class="flex justify-between items-center" v-for="item in newInstitutionRatios" :key="item.period">
<span class="text-sm text-[#999999]">{{ item.period }}</span>
<span class="text-base font-bold text-[#333333]">{{ (item.ratio * 100).toFixed(0) }}%</span>
</div>
</div>
</div>
<!-- 交易金额统计 -->
<div class="bg-[#F9F9F9] border border-[#EEEEEE] rounded-xl">
<h4 class="text-base font-bold text-[#333333] px-4 py-2 border-b border-[#EEEEEE]">交易金额统计</h4>
<div class="space-y-2 p-4">
<div class="flex justify-between items-center">
<span class="text-sm text-[#999999]">近90天最大交易金额</span>
<span class="text-base font-bold text-[#333333]">{{ formatAmount(parseIntervalValue(data.xyp_t01aafzzz))
}}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-[#999999]">近90天最小交易金额</span>
<span class="text-base font-bold text-[#333333]">{{ formatAmount(parseIntervalValue(data.xyp_t01abfzzz))
}}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-[#999999]">近90天平均交易金额</span>
<span class="text-base font-bold text-[#333333]">{{ formatAmount(parseIntervalValue(data.xyp_t01adfzzz))
}}</span>
</div>
</div>
</div>
<!-- 机构去重统计 -->
<div class="bg-[#F9F9F9] border border-[#EEEEEE] rounded-xl">
<h4 class="text-base font-bold text-[#333333] px-4 py-2 border-b border-[#EEEEEE]">机构去重统计</h4>
<div class="space-y-2 p-4">
<div class="flex justify-between items-center">
<span class="text-sm text-[#999999]">近20次交易还款成功机构</span>
<span class="text-base font-bold text-[#333333]">{{
formatMetricValue(parseIntervalValue(data.xyp_t01dejzzc)) }}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-[#999999]">近50次交易还款成功机构</span>
<span class="text-base font-bold text-[#333333]">{{
formatMetricValue(parseIntervalValue(data.xyp_t01dekzzc)) }}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-[#999999]">近100次交易还款成功机构</span>
<span class="text-base font-bold text-[#333333]">{{
formatMetricValue(parseIntervalValue(data.xyp_t01delzzc)) }}</span>
</div>
</div>
</div>
<!-- 特殊风险指标 -->
<div class="bg-[#F9F9F9] border border-[#EEEEEE] rounded-xl">
<h4 class="text-base font-bold text-[#333333] px-4 py-2 border-b border-[#EEEEEE]">特殊风险指标</h4>
<div class="space-y-2 p-4">
<div class="flex justify-between items-center">
<span class="text-sm text-[#999999]">最近90天交易失败机构</span>
<span class="text-base font-bold text-[#333333]">{{
formatMetricValue(parseIntervalValue(data.xyp_t03td111))
}}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-[#999999]">最近180天交易失败机构</span>
<span class="text-base font-bold text-[#333333]">{{
formatMetricValue(parseIntervalValue(data.xyp_t03td115))
}}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-[#999999]">最近一次交易为失败机构</span>
<span class="text-base font-bold text-[#333333]">{{
formatMetricValue(parseIntervalValue(data.xyp_t03td148))
}}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 温馨提示 -->
<div class="px-4 pb-4">
<LRemark
content="风险指标详情提供全面的风险评估指标分析,包括核心风险指标、警示指标和风险分布统计。核心风险指标包括当前逾期、近期逾期和信用风险评分等严重风险项目。警示指标涵盖申请频率、机构数量等预警信息。建议重点关注严重风险指标,及时评估申请人的还款能力和信用状况。指标数据基于多维度风险模型计算,具有较高的预测准确性。" />
</div>
</template>
<script>
import LTitle from '@/components/LTitle.vue'
import LRemark from '@/components/LRemark.vue'
export default {
name: 'RiskIndicators',
components: {
LTitle,
LRemark
},
props: {
data: {
type: Object,
default: () => ({})
}
},
computed: {
// 核心风险指标
hasCurrentOverdue() {
return this.data.xyp_cpl0044 === '1'
},
currentOverdueInstitutions() {
return this.parseIntervalValue(this.data.xyp_cpl0071)
},
hasRecentOverdue() {
return this.data.xyp_cpl0028 === '1' || this.data.xyp_cpl0029 === '1' || this.data.xyp_cpl0030 === '1'
},
creditRiskScore() {
return parseFloat(this.data.xyp_cpl0081) || 0
},
highCreditRisk() {
return this.creditRiskScore > 0.7
},
hasCriticalRisk() {
return this.hasCurrentOverdue || this.hasRecentOverdue || this.highCreditRisk
},
// 警示指标
recent1DayInstitutions() {
return this.parseIntervalValue(this.data.xyp_cpl0070)
},
recent7DayInstitutions() {
return this.parseIntervalValue(this.data.xyp_cpl0009)
},
amountComplianceIndex() {
return parseFloat(this.data.xyp_cpl0082) || 0
},
countComplianceIndex() {
return parseFloat(this.data.xyp_cpl0083) || 0
},
// 正面指标
settledInstitutions() {
return this.parseIntervalValue(this.data.xyp_cpl0002)
},
creditLoanDuration() {
return this.parseIntervalValue(this.data.xyp_cpl0045)
},
historicalSuccessPayments() {
return this.parseIntervalValue(this.data.xyp_cpl0014)
},
// 交易失败后还款分析
consumerFailureRecovery() {
return this.parseIntervalValue(this.data.xyp_cpl0052)
},
smallLoanFailureRecovery() {
return this.parseIntervalValue(this.data.xyp_cpl0053)
},
overallFailureRecovery() {
return this.parseIntervalValue(this.data.xyp_cpl0069)
},
// 恢复时间分析
avgRecoveryTime() {
return this.parseIntervalValue(this.data.xyp_cpl0062)
},
minRecoveryTime() {
return this.parseIntervalValue(this.data.xyp_cpl0059)
},
maxRecoveryTime() {
return this.parseIntervalValue(this.data.xyp_cpl0056)
},
// 金额比例指标
amountRatios() {
return [
{
id: 'recent_90_amount_ratio',
name: '近90天还款成功金额比例',
ratio: parseFloat(this.data.xyp_t02acfzbc_acfzbz) || 0,
description: '小贷担保类近90天还款成功金额占比'
},
{
id: 'recent_180_amount_ratio',
name: '近180天还款成功金额比例',
ratio: parseFloat(this.data.xyp_t02acgzbc_acgzbz) || 0,
description: '小贷担保类近180天还款成功金额占比'
},
{
id: 'recent_360_amount_ratio',
name: '近360天还款成功金额比例',
ratio: parseFloat(this.data.xyp_t02achzbc_achzbz) || 0,
description: '小贷担保类近360天还款成功金额占比'
},
{
id: 'failure_amount_ratio',
name: '交易失败金额比例',
ratio: parseFloat(this.data.xyp_t02aczzza_aczzzz) || 0,
description: '因交易能力不足导致失败的金额占总交易金额比例'
}
]
},
// 笔数比例指标
countRatios() {
return [
{
id: 'recent_90_count_ratio',
name: '近90天还款成功笔数比例',
ratio: parseFloat(this.data.xyp_t02ccfzbc_ccfzbz) || 0,
description: '小贷担保类近90天还款成功笔数占比'
},
{
id: 'recent_180_count_ratio',
name: '近180天还款成功笔数比例',
ratio: parseFloat(this.data.xyp_t02ccgzbc_ccgzbz) || 0,
description: '小贷担保类近180天还款成功笔数占比'
},
{
id: 'recent_360_count_ratio',
name: '近360天还款成功笔数比例',
ratio: parseFloat(this.data.xyp_t02cchzbc_cchzbz) || 0,
description: '小贷担保类近360天还款成功笔数占比'
},
{
id: 'overall_success_ratio',
name: '总体还款成功率',
ratio: parseFloat(this.data.xyp_t02cczzzc_cczzzz) || 0,
description: '历史总体还款成功笔数占比'
},
{
id: 'recent_5_failure_ratio',
name: '近5次交易失败比例',
ratio: parseFloat(this.data.xyp_t02ccizza_cczzza) || 0,
description: '近5次交易中因交易能力不足导致失败的笔数占比'
},
{
id: 'recent_30_failure_ratio',
name: '近30天交易失败比例',
ratio: parseFloat(this.data.xyp_t02ccezza_cczzza) || 0,
description: '近30天因交易能力不足导致失败的笔数占比'
},
{
id: 'recent_90_failure_ratio',
name: '近90天交易失败比例',
ratio: parseFloat(this.data.xyp_t02ccfzza_cczzza) || 0,
description: '近90天因交易能力不足导致失败的笔数占比'
},
{
id: 'recent_180_failure_ratio',
name: '近180天交易失败比例',
ratio: parseFloat(this.data.xyp_t02ccgzza_ccgzzz) || 0,
description: '近180天因交易能力不足导致失败的笔数占比'
}
]
},
// 特殊指标
lastTransactionDays() {
return this.parseIntervalValue(this.data.xyp_cpl0046)
},
lastRepaymentDays() {
return this.parseIntervalValue(this.data.xyp_cpl0068)
},
// 交易失败机构时间线
failureInstitutionTimeline() {
const items = [
{ period: '7天', count: this.parseIntervalValue(this.data.xyp_cpl0048) },
{ period: '14天', count: this.parseIntervalValue(this.data.xyp_cpl0049) },
{ period: '21天', count: this.parseIntervalValue(this.data.xyp_cpl0050) },
{ period: '30天', count: this.parseIntervalValue(this.data.xyp_cpl0051) },
{ period: '90天', count: this.parseIntervalValue(this.data.xyp_t03td045) },
{ period: '180天', count: this.parseIntervalValue(this.data.xyp_t03td053) }
]
const maxCount = Math.max(...items.map(item => item.count)) || 1
return items.map(item => ({
...item,
percentage: (item.count / maxCount) * 100
}))
},
// 新增机构比例
newInstitutionRatios() {
return [
{
period: '30天',
ratio: parseFloat(this.data.xyp_t02dezezz_dezzzz) || 0
},
{
period: '90天',
ratio: parseFloat(this.data.xyp_t02dezfzz_dezzzz) || 0
},
{
period: '180天',
ratio: parseFloat(this.data.xyp_t02dezgzz_dezzzz) || 0
}
]
}
},
methods: {
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
}
},
formatMetricValue(value) {
if (value === 0) return '0'
if (value < 5) return `${value}`
return `${value}+`
},
formatDays(value) {
if (value === 0) return '无记录'
if (value < 30) return `${value}`
if (value < 365) return `${Math.floor(value / 30)}个月`
return `${Math.floor(value / 365)}`
},
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)}万元`
},
getWarningClass(value) {
if (value === 0) return 'text-green-600'
if (value < 3) return 'text-yellow-600'
return 'text-red-600'
},
getIndexClass(index) {
if (index < 0.3) return 'text-green-600'
if (index < 0.7) return 'text-yellow-600'
return 'text-red-600'
},
getRecoveryPercentage(recovery) {
// 假设最大恢复次数为10次
return Math.min((recovery / 10) * 100, 100)
},
getRatioClass(ratio) {
if (ratio >= 0.8) return 'text-green-600'
if (ratio >= 0.6) return 'text-yellow-600'
return 'text-red-600'
},
getRatioBarClass(ratio) {
if (ratio >= 0.8) return 'bg-green-500'
if (ratio >= 0.6) return 'bg-yellow-500'
return 'bg-red-500'
},
getFailureClass(count) {
if (count === 0) return 'text-green-600'
if (count < 3) return 'text-yellow-600'
return 'text-red-600'
},
getFailureBarClass(count) {
if (count === 0) return 'bg-green-500'
if (count < 3) return 'bg-yellow-500'
return 'bg-red-500'
},
getCircleStyle(ratio) {
let color = '#ef4444'
if (ratio < 0.3) color = '#10b981'
else if (ratio < 0.6) color = '#f59e0b'
// 确保至少显示10度让用户知道是图表
const minDegree = 10
const actualDegree = Math.max(ratio * 360, minDegree)
return {
background: `conic-gradient(${color} ${actualDegree}deg, #e5e7eb 0deg)`
}
},
getRatioCardClass(id) {
// 失败相关的指标使用红色,总体还款成功率使用绿色,其他使用蓝色
if (id === 'overall_success_ratio') {
return 'bg-[#ECF9EF] border border-[#CAECD3]'
} else if (id.includes('failure')) {
return 'bg-[#F9ECEC] border border-[#F0CACA]'
}
return 'bg-[#ECF2FD] border border-[#CADAF9]'
},
getRatioBarBgClass(id) {
// 失败相关的指标使用红色背景,总体还款成功率使用绿色背景,其他使用蓝色背景
if (id === 'overall_success_ratio') {
return 'bg-[#CAECD3]'
} else if (id.includes('failure')) {
return 'bg-[#F0CACA]'
}
return 'bg-[#DBE6FC]'
},
getRatioBarColor(id) {
// 失败相关的指标使用红色,总体还款成功率使用绿色,其他使用蓝色
if (id === 'overall_success_ratio') {
return '#5EBC62'
} else if (id.includes('failure')) {
return '#D44643'
}
return '#5079EA'
}
}
}
</script>
<style scoped>
/* 组件样式已使用 Tailwind CSS */
</style>

View File

@@ -0,0 +1,375 @@
<template>
<div class="">
<div class="rounded-lg border border-[#99999933] pb-2 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/fxgl.png" alt="风险概览" class="w-8 h-8 object-contain" />
</div>
<span class="font-bold text-gray-800">风险概览</span>
</div>
<div class="px-4 pb-4">
<!-- 综合风险等级 -->
<div class="mb-6">
<div class="p-4 rounded-lg" :class="getRiskCardClass()">
<div class="flex items-start">
<div class="mr-3 mt-1">
<img :src="getRiskIconPath()" alt="综合风险等级" class="w-10 h-10 object-contain" />
</div>
<div class="flex-1">
<div class="flex items-center justify-between mb-2">
<h4 class="font-semibold text-gray-800">综合风险等级</h4>
<div class="risk-level-badge" :class="getRiskLevelClass()">
{{ riskLevel }}
</div>
</div>
<p class="text-gray-400 text-sm mb-2">
基于多维度数据分析的风险评估
</p>
<p class=" text-sm" :class="riskLevelTextClass">
{{ riskDescription }}
</p>
</div>
</div>
</div>
</div>
<!-- 当前状态 -->
<div class="mb-6">
<LTitle title="当前状态" class="mb-4" />
<div class="space-y-4">
<!-- 逾期状态 -->
<div class="p-4 rounded-lg" :class="getOverdueCardClass()">
<div class="flex items-start">
<div class="mr-3 mt-1">
<img :src="getOverdueIconPath()" alt="逾期状态" class="w-10 h-10 object-contain" />
</div>
<div class="flex-1">
<div class="flex items-center justify-between mb-2">
<h4 class="font-semibold text-gray-800">逾期状态</h4>
<div class="risk-level-badge" :class="getOverdueStatusClass()">
{{ overdueStatus }}
</div>
</div>
<p class="text-gray-400 text-sm">
当前逾期情况
</p>
</div>
</div>
</div>
<!-- 当前逾期机构 -->
<div class="p-4 rounded-lg" :class="getOverdueInstitutionCardClass()">
<div class="flex items-start">
<div class="mr-3 mt-1">
<img :src="getOverdueInstitutionIconPath()" alt="当前逾期机构" class="w-10 h-10 object-contain" />
</div>
<div class="flex-1">
<div class="flex items-center justify-between mb-2">
<h4 class="font-semibold text-gray-800">当前逾期机构</h4>
<div class="risk-level-badge" :class="getOverdueInstitutionClass()">
{{ currentOverdueInstitutions }}
</div>
</div>
<p class="text-gray-400 text-sm">
逾期机构数量
</p>
</div>
</div>
</div>
</div>
</div>
<!-- 关键指标 -->
<div class="">
<LTitle title="关键指标" />
<div class="space-y-2 p-4">
<div class="flex justify-between items-center text-sm">
<span class="text-[#999999]">贷款总机构数</span>
<span class="text-[#333333] font-bold">{{ formatMetricValue(totalInstitutions) }}</span>
</div>
<div class="flex justify-between items-center text-sm">
<span class="text-[#999999]">已结清机构数</span>
<span class="text-[#333333] font-bold">{{ formatMetricValue(settledInstitutions) }}</span>
</div>
<div class="flex justify-between items-center text-sm">
<span class="text-[#999999]">信用贷款时长</span>
<span class="text-[#333333] font-bold">{{ formatDays(creditLoanDuration) }}</span>
</div>
<div class="flex justify-between items-center text-sm">
<span class="text-[#999999]">最近一次交易距今</span>
<span class="text-[#333333] font-bold">{{ formatDays(lastTransactionDays) }}</span>
</div>
<div class="flex justify-between items-center text-sm">
<span class="text-[#999999]">最近一次还款距今</span>
<span class="text-[#333333] font-bold">{{ formatDays(lastRepaymentDays) }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 温馨提示 -->
<LRemark
content="风险概览提供申请人的整体风险状况总结,包括综合风险等级、当前状态和关键指标。风险等级基于多维度数据分析计算得出,包括但不限于逾期情况、借贷历史、还款表现等。当前状态展示申请人的实时风险指标,包括逾期状态、最近交易情况等。建议结合具体业务场景和风险政策进行综合判断。" />
</template>
<script>
import LTitle from '@/components/LTitle.vue'
import LRemark from '@/components/LRemark.vue'
export default {
name: 'RiskOverview',
components: {
LTitle,
LRemark
},
props: {
data: {
type: Object,
default: () => ({})
}
},
computed: {
// 解析区间化数据的辅助方法
totalInstitutions() {
return this.parseIntervalValue(this.data.xyp_cpl0001)
},
settledInstitutions() {
return this.parseIntervalValue(this.data.xyp_cpl0002)
},
currentOverdueStatus() {
return this.data.xyp_cpl0044 === '1'
},
currentOverdueInstitutions() {
return this.parseIntervalValue(this.data.xyp_cpl0071)
},
creditLoanDuration() {
return this.parseIntervalValue(this.data.xyp_cpl0045)
},
recent1DayInstitutions() {
return this.parseIntervalValue(this.data.xyp_cpl0070)
},
recent7DayInstitutions() {
return this.parseIntervalValue(this.data.xyp_cpl0009)
},
lastTransactionDays() {
return this.parseIntervalValue(this.data.xyp_cpl0046)
},
lastRepaymentDays() {
return this.parseIntervalValue(this.data.xyp_cpl0068)
},
// 综合风险等级计算
riskLevel() {
const creditScore = parseFloat(this.data.xyp_cpl0081) || 0
const overdueIndex = parseFloat(this.data.xyp_cpl0082) || 0
if (creditScore > 0.7 || overdueIndex > 0.7 || this.currentOverdueStatus) {
return '高风险'
} else if (creditScore > 0.4 || overdueIndex > 0.4) {
return '中风险'
} else {
return '低风险'
}
},
riskLevelClass() {
switch (this.riskLevel) {
case '高风险': return 'risk-high'
case '中风险': return 'risk-medium'
default: return 'risk-low'
}
},
riskLevelIconClass() {
switch (this.riskLevel) {
case '高风险': return 'bg-red-500'
case '中风险': return 'bg-yellow-500'
default: return 'bg-green-500'
}
},
riskLevelTextClass() {
switch (this.riskLevel) {
case '高风险': return 'text-red-600'
case '中风险': return 'text-yellow-600'
default: return 'text-green-600'
}
},
overdueStatusIconClass() {
return this.hasOverdue ? 'bg-red-500' : 'bg-green-500'
},
overdueStatusTextClass() {
return this.hasOverdue ? 'text-red-600' : 'text-green-600'
},
riskDescription() {
switch (this.riskLevel) {
case '高风险': return '存在较高信用风险,建议谨慎放贷'
case '中风险': return '信用风险适中,需要进一步评估'
default: return '信用风险较低,具备良好还款能力'
}
},
overdueStatus() {
return this.currentOverdueStatus ? '存在逾期' : '无逾期'
},
overdueStatusClass() {
return this.currentOverdueStatus ? 'status-danger' : 'status-success'
},
hasRecentActivity() {
return this.recent1DayInstitutions > 0 || this.recent7DayInstitutions > 0
},
hasOverdue() {
return this.currentOverdueStatus
}
},
methods: {
// 获取综合风险等级卡片样式
getRiskCardClass() {
switch (this.riskLevel) {
case '高风险':
return 'bg-red-50 border border-red-200'
case '中风险':
return 'bg-yellow-50 border border-yellow-200'
default:
return 'bg-green-50 border border-green-200'
}
},
// 获取综合风险等级图标路径
getRiskIconPath() {
switch (this.riskLevel) {
case '高风险':
return new URL('@/assets/images/report/gfx.png', import.meta.url).href
case '中风险':
return new URL('@/assets/images/report/zfx.png', import.meta.url).href
default:
return new URL('@/assets/images/report/zq.png', import.meta.url).href
}
},
// 获取综合风险等级标签样式
getRiskLevelClass() {
switch (this.riskLevel) {
case '高风险':
return 'bg-red-600 text-white'
case '中风险':
return 'bg-yellow-600 text-white'
default:
return 'bg-green-600 text-white'
}
},
// 获取逾期状态卡片样式
getOverdueCardClass() {
return this.hasOverdue
? 'bg-red-50 border border-red-200'
: 'bg-green-50 border border-green-200'
},
// 获取逾期状态图标路径
getOverdueIconPath() {
return this.hasOverdue
? new URL('@/assets/images/report/gfx.png', import.meta.url).href
: new URL('@/assets/images/report/zq.png', import.meta.url).href
},
// 获取逾期状态标签样式
getOverdueStatusClass() {
return this.hasOverdue
? 'bg-red-600 text-white'
: 'bg-green-600 text-white'
},
// 获取当前逾期机构卡片样式
getOverdueInstitutionCardClass() {
return this.currentOverdueInstitutions > 0
? 'bg-red-50 border border-red-200'
: 'bg-green-50 border border-green-200'
},
// 获取当前逾期机构图标路径
getOverdueInstitutionIconPath() {
return this.currentOverdueInstitutions > 0
? new URL('@/assets/images/report/gfx.png', import.meta.url).href
: new URL('@/assets/images/report/zq.png', import.meta.url).href
},
// 获取当前逾期机构标签样式
getOverdueInstitutionClass() {
return this.currentOverdueInstitutions > 0
? 'bg-red-600 text-white'
: 'bg-green-600 text-white'
},
// 解析区间化数值
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
}
},
formatMetricValue(value) {
if (value === 0) return '0'
if (value < 5) return `${value}`
if (value < 10) return `${value}`
return `${value}+`
},
formatDays(value) {
if (value === 0) return '无记录'
if (value < 30) return `${value}`
if (value < 365) return `${Math.floor(value / 30)}个月`
return `${Math.floor(value / 365)}`
},
getMetricClass(value) {
if (value > 10) return 'text-red-600'
if (value > 5) return 'text-orange-600'
return 'text-green-600'
}
}
}
</script>
<style scoped>
/* 风险等级标签 */
.risk-level-badge {
position: absolute;
top: 0;
right: 0;
padding: 2px 6px;
border-radius: 0 8px 0 8px;
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
z-index: 1;
}
/* 卡片需要相对定位 */
.p-4.rounded-lg {
position: relative;
}
</style>

File diff suppressed because it is too large Load Diff

122
src/ui/CJRZQ5E9F/index.vue Normal file
View File

@@ -0,0 +1,122 @@
<template>
<div class="card">
<!-- 风险概览 -->
<RiskOverview :data="riskData" />
<!-- 信用评分 -->
<CreditScores :data="riskData" />
<!-- 贷款行为分析 -->
<LoanBehaviorAnalysis :data="riskData" />
<!-- 机构分析 -->
<InstitutionAnalysis :data="riskData" />
<!-- 时间趋势分析 -->
<TimeTrendAnalysis :data="riskData" />
<!-- 风险指标详情 -->
<RiskIndicators :data="riskData" />
<!-- 专业建议 -->
<RiskAdvice :data="riskData" />
</div>
</template>
<script>
import RiskOverview from './components/RiskOverview.vue'
import CreditScores from './components/CreditScores.vue'
import LoanBehaviorAnalysis from './components/LoanBehaviorAnalysis.vue'
import InstitutionAnalysis from './components/InstitutionAnalysis.vue'
import TimeTrendAnalysis from './components/TimeTrendAnalysis.vue'
import RiskIndicators from './components/RiskIndicators.vue'
import RiskAdvice from './components/RiskAdvice.vue'
export default {
name: 'LoanRiskReport',
components: {
RiskOverview,
CreditScores,
LoanBehaviorAnalysis,
InstitutionAnalysis,
TimeTrendAnalysis,
RiskIndicators,
RiskAdvice
},
props: {
data: {
type: Object,
default: () => ({})
},
apiId: {
type: String,
default: '',
},
index: {
type: Number,
default: 0,
},
notifyRiskStatus: {
type: Function,
default: () => { },
},
},
computed: {
riskData() {
return this.data || {}
},
hasRisk() {
return Object.keys(this.data || {}).length > 0;
},
riskScore() {
const d = this.riskData;
// 检查是否有数据
if (!d || Object.keys(d).length === 0) {
return 100; // 无数据视为最安全
}
// 根据风险概览数据计算评分
// 假设数据中有风险评分字段
const riskLevel = d.riskLevel || d.risk_level || '';
const riskScore = d.riskScore || d.risk_score || 0;
// 如果有风险评分,直接使用
if (riskScore > 0) {
// 风险评分转换为安全评分(分数越高越安全)
// 假设风险评分是0-100分数越高风险越大
return Math.max(0, 100 - riskScore);
}
// 根据风险等级计算
if (riskLevel === 'high' || riskLevel === 'HIGH' || riskLevel === '高风险') {
return 20; // 高风险
}
if (riskLevel === 'medium' || riskLevel === 'MEDIUM' || riskLevel === '中风险') {
return 60; // 中等风险
}
if (riskLevel === 'low' || riskLevel === 'LOW' || riskLevel === '低风险') {
return 80; // 低风险
}
// 默认中等风险
return 70;
}
},
watch: {
riskScore(newValue) {
if (this.apiId && this.notifyRiskStatus) {
this.notifyRiskStatus(this.apiId, this.index, newValue);
}
}
},
mounted() {
// 立即通知一次
if (this.apiId && this.notifyRiskStatus) {
this.notifyRiskStatus(this.apiId, this.index, this.riskScore);
}
}
}
</script>
<style scoped></style>

View File

@@ -0,0 +1,317 @@
/**
* 贷款风险报告(CJRZQ5E9F)数据拆分工具
* 将完整的贷款风险报告数据拆分成多个独立的模块用于在不同的tab中显示
*/
/**
* 将CJRZQ5E9F数据拆分为多个独立的tab模块
* @param {Array} reportData - 原始报告数据数组
* @returns {Array} 拆分后的模块数组
*/
export function splitCJRZQ5E9FForTabs(reportData) {
// 查找CJRZQ5E9F数据
const cjrzq5e9fData = reportData.find(
(item) => item.data?.apiID === "JRZQ5E9F"
);
if (!cjrzq5e9fData || !cjrzq5e9fData.data?.data) {
return reportData; // 如果没有找到CJRZQ5E9F数据返回原数据
}
const originalData = cjrzq5e9fData.data.data;
const baseTimestamp = cjrzq5e9fData.data.timestamp;
// 创建拆分后的模块数组
const splitModules = [];
// 1. 风险概览
if (originalData && Object.keys(originalData).length > 0) {
splitModules.push({
data: {
apiID: "CJRZQ5E9F_RiskOverview",
data: originalData,
success: true,
timestamp: baseTimestamp,
},
});
}
// 2. 信用评分
if (originalData && Object.keys(originalData).length > 0) {
splitModules.push({
data: {
apiID: "CJRZQ5E9F_CreditScores",
data: originalData,
success: true,
timestamp: baseTimestamp,
},
});
}
// 3. 贷款行为分析
if (originalData && Object.keys(originalData).length > 0) {
splitModules.push({
data: {
apiID: "CJRZQ5E9F_LoanBehaviorAnalysis",
data: originalData,
success: true,
timestamp: baseTimestamp,
},
});
}
// 4. 机构分析
if (originalData && Object.keys(originalData).length > 0) {
splitModules.push({
data: {
apiID: "CJRZQ5E9F_InstitutionAnalysis",
data: originalData,
success: true,
timestamp: baseTimestamp,
},
});
}
// 5. 时间趋势分析
if (originalData && Object.keys(originalData).length > 0) {
splitModules.push({
data: {
apiID: "CJRZQ5E9F_TimeTrendAnalysis",
data: originalData,
success: true,
timestamp: baseTimestamp,
},
});
}
// 6. 风险指标详情
if (originalData && Object.keys(originalData).length > 0) {
splitModules.push({
data: {
apiID: "CJRZQ5E9F_RiskIndicators",
data: originalData,
success: true,
timestamp: baseTimestamp,
},
});
}
// 7. 专业建议
if (originalData && Object.keys(originalData).length > 0) {
splitModules.push({
data: {
apiID: "CJRZQ5E9F_RiskAdvice",
data: originalData,
success: true,
timestamp: baseTimestamp,
},
});
}
// 移除原始的JRZQ5E9F数据添加拆分后的模块
const otherData = reportData.filter(
(item) => item.data?.apiID !== "JRZQ5E9F"
);
return [...otherData, ...splitModules];
}
/**
* 解析区间化数值
* @param {string|number} value - 原始值
* @returns {number} 解析后的数值
*/
export function 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;
}
}
/**
* 格式化指标值显示
* @param {number} value - 数值
* @returns {string} 格式化后的显示文本
*/
export function formatMetricValue(value) {
if (value === 0) return "0";
if (value < 5) return `${value}`;
return `${value}+`;
}
/**
* 格式化天数显示
* @param {number} value - 天数
* @returns {string} 格式化后的显示文本
*/
export function formatDays(value) {
if (value === 0) return "无记录";
if (value < 30) return `${value}`;
if (value < 365) return `${Math.floor(value / 30)}个月`;
return `${Math.floor(value / 365)}`;
}
/**
* 格式化金额显示
* @param {number} value - 金额
* @returns {string} 格式化后的显示文本
*/
export function 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)}万元`;
}
/**
* 计算风险等级
* @param {number} creditScore - 信用风险评分
* @param {number} overdueIndex - 逾期指数
* @param {boolean} currentOverdue - 当前是否逾期
* @returns {object} 包含等级、颜色和描述的对象
*/
export function calculateRiskLevel(creditScore, overdueIndex, currentOverdue) {
if (creditScore > 0.7 || overdueIndex > 0.7 || currentOverdue) {
return {
level: "高风险",
color: "text-red-600",
bgColor: "bg-red-100",
iconColor: "bg-red-500",
description: "存在较高信用风险,建议谨慎放贷",
};
} else if (creditScore > 0.4 || overdueIndex > 0.4) {
return {
level: "中风险",
color: "text-yellow-600",
bgColor: "bg-yellow-100",
iconColor: "bg-yellow-500",
description: "信用风险适中,需要进一步评估",
};
} else {
return {
level: "低风险",
color: "text-green-600",
bgColor: "bg-green-100",
iconColor: "bg-green-500",
description: "信用风险较低,具备良好还款能力",
};
}
}
/**
* 计算信用评分显示
* @param {number} creditRiskScore - 信用风险评分
* @param {number} amountComplianceIndex - 履约金额综合指数
* @param {number} countComplianceIndex - 履约笔数综合指数
* @returns {object} 包含评分、进度和颜色的对象
*/
export function calculateCreditScore(
creditRiskScore,
amountComplianceIndex,
countComplianceIndex
) {
const avgRisk =
(creditRiskScore + amountComplianceIndex + countComplianceIndex) / 3;
// 风险越高,信用分越低
const score = Math.round((1 - avgRisk) * 850 + 150);
const progress = (score / 1000) * 283;
let color = "#ef4444";
if (score >= 750) color = "#10b981";
else if (score >= 650) color = "#f59e0b";
return {
score,
progress,
color,
};
}
/**
* 获取信用等级描述
* @param {number} score - 信用评分
* @returns {string} 等级描述
*/
export function getCreditScoreLevel(score) {
if (score >= 800) return "优秀";
if (score >= 700) return "良好";
if (score >= 600) return "一般";
if (score >= 500) return "较差";
return "很差";
}
/**
* 获取信用等级样式类
* @param {number} score - 信用评分
* @returns {string} 样式类名
*/
export function getCreditScoreBadgeClass(score) {
if (score >= 800) return "bg-green-100 text-green-800";
if (score >= 700) return "bg-blue-100 text-blue-800";
if (score >= 600) return "bg-yellow-100 text-yellow-800";
if (score >= 500) return "bg-orange-100 text-orange-800";
return "bg-red-100 text-red-800";
}
/**
* 获取评分样式类
* @param {number} score - 评分
* @returns {string} 样式类名
*/
export function getScoreClass(score) {
if (score === null) return "text-gray-400";
if (score >= 750) return "text-green-600";
if (score >= 650) return "text-yellow-600";
return "text-red-600";
}
/**
* 获取圆形进度样式
* @param {number} ratio - 比例值 (0-1)
* @returns {object} 样式对象
*/
export function getCircleStyle(ratio) {
let color = "#ef4444";
if (ratio >= 0.8) color = "#10b981";
else if (ratio >= 0.6) color = "#f59e0b";
// 确保至少显示10度让用户知道是图表
const minDegree = 10;
const actualDegree = Math.max(ratio * 360, minDegree);
return {
background: `conic-gradient(${color} ${actualDegree}deg, #e5e7eb 0deg)`,
};
}
/**
* 检查是否有风险数据
* @param {Object} data - 数据对象
* @returns {boolean} 是否有风险
*/
export function hasRiskData(data) {
if (!data) return false;
// 检查对象中是否有非0值
return Object.values(data).some((value) => {
if (typeof value === "number") return value > 0;
if (typeof value === "string")
return value !== "0" && value !== "-" && value !== "";
return false;
});
}

908
src/ui/CJRZQ8203.vue Normal file
View File

@@ -0,0 +1,908 @@
<script setup>
import * as echarts from 'echarts' // 引入 ECharts
import LTable from '@/components/LTable.vue'
import LTitle from '@/components/LTitle.vue'
import { ref, onMounted, watch, computed } from 'vue'
import { useRiskNotifier } from '@/composables/useRiskNotifier'
const props = defineProps({
data: {
type: Object,
required: true,
},
mode: {
type: String,
default: 'idOnly', // 'full' 或 'idOnly'
validator: value => ['full', 'idOnly'].includes(value),
},
apiId: {
type: String,
default: '',
},
index: {
type: Number,
default: 0,
},
notifyRiskStatus: {
type: Function,
default: () => { },
},
})
const { data, mode } = props
// 数据类型切换
const dataType = ref('id') // 'id' 或 'cell'
// 监听mode变化如果是idOnly模式强制selectedDataSource为"id"
watch(
() => props.mode,
newMode => {
if (newMode === 'idOnly') {
dataType.value = 'id'
}
},
{ immediate: true }
)
// 图表实例
const borrowChartRef = ref(null)
const repayChartRef = ref(null)
const trendChartRef = ref(null)
const borrowChartInstance = ref(null)
const repayChartInstance = ref(null)
const trendChartInstance = ref(null)
// 表格数据
const borrowTable = ref([])
const institutionTable = ref([])
// 获取参考日期
const getReferenceDate = computed(() => {
const prefix = `tl_${dataType.value}`
const dateStr = data[`${prefix}_eletail_lasttime`]
if (dateStr) {
// 将字符串转为日期对象
return new Date(dateStr)
}
// 如果没有日期信息,则使用当前日期
return new Date()
})
// 根据相对月份获取实际年月
function getActualMonthYear(monthsBack) {
const month = monthsBack.replace('m', '')
if (month === '12') {
return '近1年'
}
return `${month}`
// const refDate = new Date(getReferenceDate.value);
// refDate.setMonth(refDate.getMonth() - monthsBack);
// const year = refDate.getFullYear();
// const month = refDate.getMonth() + 1; // JavaScript月份从0开始
// return `${year}年${month}月`;
}
function getActualMonthYearT(monthsBack) {
if (monthsBack === 't0') {
return '1年内'
}
return `${monthsBack.replace('t', '')}`
}
// 金额转换函数
function getLevelAmount(level) {
const levelNum = Number(level) || 1
const baseAmount = 3000
return baseAmount * (levelNum - 1)
}
// 等级范围转换
function getLevelRange(level) {
const levelNum = Number(level) || 1
const baseAmount = 3000
const lowerLimit = baseAmount * (levelNum - 1)
const upperLimit = baseAmount * levelNum
return `${lowerLimit}元 - ${upperLimit}`
}
// 计算借贷金额数据(按月)
const monthlyBorrowData = computed(() => {
const months = ['m1', 'm3', 'm6', 'm9', 'm12']
const prefix = `tl_${dataType.value}`
return months
.map((month, index) => {
const borrowKey = `${prefix}_${month}_nbank_passlendamt`
const borrowAmount = getLevelAmount(data[borrowKey])
console.log(borrowKey, borrowAmount)
return {
month: getActualMonthYear(month),
amount: borrowAmount,
displayAmount: formatAmount(borrowAmount),
level: data[borrowKey] || '0',
levelRange: getLevelRange(data[borrowKey]),
}
})
.reverse()
})
// 计算应还金额数据(按月)
const monthlyRepayData = computed(() => {
const months = ['m1', 'm3', 'm6', 'm9', 'm12']
const prefix = `tl_${dataType.value}`
return months
.map((month, index) => {
const repayKey = `${prefix}_${month}_nbank_reamt`
const repayAmount = getLevelAmount(data[repayKey])
return {
month: getActualMonthYear(month),
amount: repayAmount,
displayAmount: formatAmount(repayAmount),
level: data[repayKey] || '0',
levelRange: getLevelRange(data[repayKey]),
}
})
.reverse()
})
// 计算机构数和借还差值(按月)
const monthlyInstitutionData = computed(() => {
const months = ['m1', 'm3', 'm6', 'm9', 'm12']
const prefix = `tl_${dataType.value}`
return months
.map((month, index) => {
const orgKey = `${prefix}_${month}_nbank_passorg`
const numKey = `${prefix}_${month}_nbank_passnum`
const borrowKey = `${prefix}_${month}_nbank_passlendamt`
const repayKey = `${prefix}_${month}_nbank_reamt`
const orgCount = Number(data[orgKey] || 0)
const loanCount = Number(data[numKey] || 0)
const borrowAmount = getLevelAmount(data[borrowKey])
const repayAmount = getLevelAmount(data[repayKey])
let ratio = 0
if (borrowAmount > 0) {
ratio = ((repayAmount / borrowAmount) * 100).toFixed(2)
}
return {
month: getActualMonthYear(month),
orgCount,
loanCount,
borrowAmount: formatAmount(borrowAmount),
repayAmount: formatAmount(repayAmount),
ratio: `${ratio}%`,
}
})
.reverse()
})
// 计算近期借贷趋势数据3月、6月、9月、12月
const recentBorrowTrends = computed(() => {
const months = ['t0']
const prefix = `tl_${dataType.value}`
return months
.map((month, index) => {
const orgKey = `${prefix}_${month}_nbank_org`
const numKey = `${prefix}_${month}_nbank_num`
const borrowKey = `${prefix}_${month}_nbank_lendamt`
const repayKey = `${prefix}_${month}_nbank_reamt`
const orgCount = Number(data[orgKey] || 0)
const loanCount = Number(data[numKey] || 0)
const borrowAmount = getLevelAmount(data[borrowKey])
const repayAmount = getLevelAmount(data[repayKey])
let ratio = 0
if (borrowAmount > 0) {
ratio = ((repayAmount / borrowAmount) * 100).toFixed(2)
}
return {
month: getActualMonthYearT(month),
orgCount,
loanCount,
borrowAmount: formatAmount(borrowAmount),
repayAmount: formatAmount(repayAmount),
ratio: `${ratio}%`,
}
})
.reverse()
})
// 获取最近一次借贷信息
const lastLoanInfo = computed(() => {
const prefix = `tl_${dataType.value}`
return {
time: data[`${prefix}_eletail_lasttime`] || '--',
type: getLoanTypeDesc(data[`${prefix}_eletail_lasttype`]),
count: Number(data[`${prefix}_eletail_num`] || 0),
orgCount: Number(data[`${prefix}_eletail_org`] || 0),
}
})
// 获取借贷类型描述
function getLoanTypeDesc(type) {
const typeMap = {
a: '传统银行',
b: '网络零售银行',
c: '持牌网络小贷',
d: '持牌小贷',
e: '持牌消费金融',
f: '持牌融资租赁',
g: '持牌汽车金融',
h: '其他',
}
return typeMap[type] || '未知'
}
// 金额格式化
function formatAmount(amount) {
return amount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}
// 借贷行为总结
const behaviorSummary = computed(() => {
const prefix = `tl_${dataType.value}`
// 获取近一年数据
const yearData = recentBorrowTrends.value[0]
// 计算平均月申请次数
const avgMonthlyApplications = (yearData.loanCount / 12).toFixed(1)
// 计算平均月审批额度
const totalBorrowAmount = Number(yearData.borrowAmount.replace(/,/g, ''))
const avgMonthlyAmount = (totalBorrowAmount / 12).toFixed(0)
// 计算平均月应还金额
const totalRepayAmount = Number(yearData.repayAmount.replace(/,/g, ''))
const avgMonthlyRepay = (totalRepayAmount / 12).toFixed(0)
// 计算还款比例
const repayRatio = totalBorrowAmount > 0 ? ((totalRepayAmount / totalBorrowAmount) * 100).toFixed(1) : 0
// 风险评估
let riskLevel = '低'
let riskDesc = '借贷行为健康,借贷金额合理'
// 基于机构数评估
if (yearData.orgCount > 5) {
riskLevel = '高'
riskDesc = '多头借贷风险较高,借贷机构过多'
} else if (yearData.orgCount > 3) {
riskLevel = '中'
riskDesc = '存在多头借贷风险,借贷机构较多'
}
// 基于月均申请次数评估
if (avgMonthlyApplications > 3) {
riskLevel = riskLevel === '低' ? '中' : '高'
riskDesc += ',月均申请次数较多'
}
// 基于还款比例评估
if (repayRatio < 50) {
riskLevel = riskLevel === '低' ? '中' : '高'
riskDesc += ',还款比例较低'
}
return {
totalApplications: yearData.loanCount,
totalOrgs: yearData.orgCount,
totalAmount: formatAmount(totalBorrowAmount),
avgMonthlyApplications,
avgMonthlyAmount: formatAmount(avgMonthlyAmount),
avgMonthlyRepay: formatAmount(avgMonthlyRepay),
repayRatio: `${repayRatio}%`,
riskLevel,
riskDesc,
}
})
// 绘制借贷金额图表
function drawBorrowChart() {
if (!borrowChartRef.value) return
if (!borrowChartInstance.value) {
borrowChartInstance.value = echarts.init(borrowChartRef.value)
}
const chartData = monthlyBorrowData.value
const option = {
title: {
text: '月度审批额度(元)',
left: 'center',
textStyle: {
fontWeight: 'bold',
fontSize: 16,
},
},
tooltip: {
trigger: 'axis',
formatter: function (params) {
const data = params[0].data
return `${params[0].name}<br/>${params[0].seriesName}: ${data.displayAmount}<br/>等级: ${data.level} (${data.levelRange})`
},
backgroundColor: 'rgba(255, 255, 255, 0.8)',
borderColor: '#5470C6',
borderWidth: 1,
textStyle: {
color: '#333',
},
shadowBlur: 10,
shadowColor: 'rgba(0, 0, 0, 0.2)',
},
grid: {
left: '5%',
right: '5%',
bottom: '0%',
containLabel: true,
},
xAxis: {
type: 'category',
data: chartData.map(item => item.month),
axisLabel: {
interval: 0,
rotate: 45,
fontWeight: 'bold',
margin: 15,
},
axisLine: {
lineStyle: {
color: '#999',
},
},
},
yAxis: {
type: 'value',
name: '金额(元)',
nameTextStyle: {
fontWeight: 'bold',
},
splitLine: {
lineStyle: {
type: 'dashed',
opacity: 0.6,
},
},
},
series: [
{
name: '审批额度',
type: 'bar',
data: chartData.map(item => ({
value: item.amount,
displayAmount: item.displayAmount,
level: item.level,
levelRange: item.levelRange,
})),
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#83bff6' },
{ offset: 0.5, color: '#5470C6' },
{ offset: 1, color: '#4662a4' },
]),
borderRadius: [5, 5, 0, 0],
},
emphasis: {
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#5470C6' },
{ offset: 0.7, color: '#4662a4' },
{ offset: 1, color: '#3c5390' },
]),
},
},
barWidth: '60%',
barMinHeight: 3,
showBackground: true,
backgroundStyle: {
color: 'rgba(180, 180, 180, 0.1)',
},
},
],
animation: true,
}
borrowChartInstance.value.setOption(option)
}
// 绘制应还金额图表
function drawRepayChart() {
if (!repayChartRef.value) return
if (!repayChartInstance.value) {
repayChartInstance.value = echarts.init(repayChartRef.value)
}
const chartData = monthlyRepayData.value
const option = {
title: {
text: '月度应还金额(元)',
left: 'center',
textStyle: {
fontWeight: 'bold',
fontSize: 16,
},
},
tooltip: {
trigger: 'axis',
formatter: function (params) {
const data = params[0].data
return `${params[0].name}<br/>${params[0].seriesName}: ${data.displayAmount}<br/>等级: ${data.level} (${data.levelRange})`
},
backgroundColor: 'rgba(255, 255, 255, 0.8)',
borderColor: '#91CC75',
borderWidth: 1,
textStyle: {
color: '#333',
},
shadowBlur: 10,
shadowColor: 'rgba(0, 0, 0, 0.2)',
},
grid: {
left: '5%',
right: '5%',
bottom: '15%',
containLabel: true,
},
xAxis: {
type: 'category',
data: chartData.map(item => item.month),
axisLabel: {
interval: 0,
rotate: 45,
fontWeight: 'bold',
margin: 15,
},
axisLine: {
lineStyle: {
color: '#999',
},
},
},
yAxis: {
type: 'value',
name: '金额(元)',
nameTextStyle: {
fontWeight: 'bold',
},
splitLine: {
lineStyle: {
type: 'dashed',
opacity: 0.6,
},
},
},
series: [
{
name: '应还金额',
type: 'bar',
data: chartData.map(item => ({
value: item.amount,
displayAmount: item.displayAmount,
level: item.level,
levelRange: item.levelRange,
})),
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#b8e986' },
{ offset: 0.5, color: '#91CC75' },
{ offset: 1, color: '#7cb362' },
]),
borderRadius: [5, 5, 0, 0],
},
emphasis: {
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#91CC75' },
{ offset: 0.7, color: '#7cb362' },
{ offset: 1, color: '#6a9c53' },
]),
},
},
barWidth: '60%',
barMinHeight: 3,
showBackground: true,
backgroundStyle: {
color: 'rgba(180, 180, 180, 0.1)',
},
},
],
animation: true,
}
repayChartInstance.value.setOption(option)
}
// 绘制借贷应还趋势对比图
function drawTrendChart() {
if (!trendChartRef.value) return
if (!trendChartInstance.value) {
trendChartInstance.value = echarts.init(trendChartRef.value)
}
const borrowData = monthlyBorrowData.value
const repayData = monthlyRepayData.value
const option = {
title: {
text: '审批额度与应还金额趋势对比',
left: 'center',
textStyle: {
fontWeight: 'bold',
fontSize: 16,
},
},
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(255, 255, 255, 0.8)',
borderColor: '#ccc',
borderWidth: 1,
textStyle: {
color: '#333',
},
shadowBlur: 10,
shadowColor: 'rgba(0, 0, 0, 0.2)',
},
legend: {
data: ['审批额度', '应还金额'],
top: 30,
textStyle: {
fontWeight: 'bold',
},
},
grid: {
left: '5%',
right: '5%',
bottom: '15%',
containLabel: true,
},
xAxis: {
type: 'category',
data: borrowData.map(item => item.month),
axisLabel: {
interval: 0,
rotate: 45,
fontWeight: 'bold',
margin: 15,
},
axisLine: {
lineStyle: {
color: '#999',
},
},
},
yAxis: {
type: 'value',
name: '金额(元)',
nameTextStyle: {
fontWeight: 'bold',
},
splitLine: {
lineStyle: {
type: 'dashed',
opacity: 0.6,
},
},
},
series: [
{
name: '审批额度',
type: 'line',
data: borrowData.map(item => item.amount),
smooth: true,
symbol: 'emptyCircle',
symbolSize: 8,
lineStyle: {
width: 3,
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 10,
shadowOffsetY: 8,
},
itemStyle: {
color: '#5470C6',
borderWidth: 2,
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(84, 112, 198, 0.5)' },
{ offset: 1, color: 'rgba(84, 112, 198, 0.1)' },
]),
},
},
{
name: '应还金额',
type: 'line',
data: repayData.map(item => item.amount),
smooth: true,
symbol: 'emptyCircle',
symbolSize: 8,
lineStyle: {
width: 3,
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 10,
shadowOffsetY: 8,
},
itemStyle: {
color: '#91CC75',
borderWidth: 2,
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(145, 204, 117, 0.5)' },
{ offset: 1, color: 'rgba(145, 204, 117, 0.1)' },
]),
},
},
],
animation: true,
}
trendChartInstance.value.setOption(option)
}
// 监听数据类型变化
watch(dataType, () => {
drawBorrowChart()
drawRepayChart()
drawTrendChart()
})
// 初始化所有图表
function initCharts() {
drawBorrowChart()
drawRepayChart()
drawTrendChart()
}
// 窗口大小变化时重绘图表
function handleResize() {
if (borrowChartInstance.value) borrowChartInstance.value.resize()
if (repayChartInstance.value) repayChartInstance.value.resize()
if (trendChartInstance.value) trendChartInstance.value.resize()
}
onMounted(() => {
initCharts()
window.addEventListener('resize', handleResize)
})
// 计算风险评分0-100分分数越高越安全
const riskScore = computed(() => {
// 计算总借贷金额和还款金额
const totalBorrowAmount = monthlyBorrowData.value.reduce((sum, item) => sum + item.totalAmount, 0);
const totalRepayAmount = monthlyRepayData.value.reduce((sum, item) => sum + item.totalAmount, 0);
// 根据借贷金额计算风险评分
// 0元100分最安全
// 1-10万90分较安全
// 10-50万70分中等风险
// 50-100万50分较高风险
// 100万以上30分高风险
const totalAmount = totalBorrowAmount + totalRepayAmount;
if (totalAmount === 0) return 100;
if (totalAmount <= 100000) return 90;
if (totalAmount <= 500000) return 70;
if (totalAmount <= 1000000) return 50;
return 30;
});
// 使用 composable 通知父组件风险评分
useRiskNotifier(props, riskScore);
// 暴露给父组件
defineExpose({
riskScore
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
</script>
<template>
<div class="card">
<div class="flex flex-col gap-y-4">
<!-- 数据类型切换 -->
<div class="p-6 bg-white rounded-lg shadow-sm border border-gray-100 relative overflow-hidden mb-4">
<!-- 背景装饰元素 -->
<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 v-if="mode === 'full'" class="flex flex-wrap gap-6 w-full md:w-auto">
<div class="flex-1 md:flex-none flex rounded-md shadow-sm relative">
<button type="button"
class="flex-1 py-2 px-4 text-sm font-medium rounded-l-md border transition-all duration-200 flex items-center"
:class="[
dataType === '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="dataType = 'id'">
<svg v-if="dataType === '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 type="button"
class="flex-1 py-2 px-4 text-sm font-medium rounded-r-md border transition-all duration-200 flex items-center"
:class="[
dataType === '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="dataType = 'cell'">
<svg v-if="dataType === '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>
</div>
</div>
<!-- 数据类型说明 -->
<div v-if="mode === 'full'" 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>
<!-- 借贷金额图表 -->
<LTitle title="近期审批额度" />
<div ref="borrowChartRef" class="chart-container"></div>
<!-- 借贷机构数和借还差值比例表格 -->
<LTitle title="近期通过借贷审批情况" />
<div class="overflow-x-auto">
<LTable :data="monthlyInstitutionData">
<template #header>
<th class="border px-1 py-2 text-xs min-w-[25%]">时间</th>
<th class="border px-1 py-2 text-xs min-w-[15%]">借贷机构数</th>
<th class="border px-1 py-2 text-xs min-w-[15%]">借贷次数</th>
<th class="border px-1 py-2 text-xs min-w-[15%]">审批额度</th>
</template>
<template #default="{ row }">
<td class="border px-1 py-2 text-xs">
{{ row.month }}
</td>
<td class="border px-1 py-2 text-xs text-center">
{{ row.orgCount }}
</td>
<td class="border px-1 py-2 text-xs text-center">
{{ row.loanCount }}
</td>
<td class="border px-1 py-2 text-xs text-center">
{{ row.borrowAmount }}
</td>
</template>
</LTable>
</div>
<!-- 近期借贷趋势表格 -->
<LTitle title="近1年借贷情况" />
<div class="overflow-x-auto">
<LTable :data="recentBorrowTrends">
<template #header>
<th class="border px-1 py-2 text-xs min-w-[25%]">时间</th>
<th class="border px-1 py-2 text-xs min-w-[15%]">借贷机构数</th>
<th class="border px-1 py-2 text-xs min-w-[15%]">借贷次数</th>
<th class="border px-1 py-2 text-xs min-w-[15%]">审批额度</th>
<th class="border px-1 py-2 text-xs min-w-[15%]">应还金额</th>
<th class="border px-1 py-2 text-xs min-w-[15%]">审批与应还比例</th>
</template>
<template #default="{ row }">
<td class="border px-1 py-2 text-xs">
{{ row.month }}
</td>
<td class="border px-1 py-2 text-xs text-center">
{{ row.orgCount }}
</td>
<td class="border px-1 py-2 text-xs text-center">
{{ row.loanCount }}
</td>
<td class="border px-1 py-2 text-xs text-center">
{{ row.borrowAmount }}
</td>
<td class="border px-1 py-2 text-xs text-center">
{{ row.repayAmount }}
</td>
<td class="border px-1 py-2 text-xs text-center">
{{ row.ratio }}
</td>
</template>
</LTable>
</div>
<!-- 借贷行为总结 -->
<LTitle title="借贷行为总结分析" />
<div class="summary-container bg-blue-50 p-4 rounded-md">
<div class="text-xs text-gray-500 mb-2">数据时间范围: 近1年</div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
<div class="info-card p-3 bg-white rounded shadow-sm">
<div class="text-sm text-gray-500">总申请次数</div>
<div class="text-lg font-semibold">{{ behaviorSummary.totalApplications }}</div>
</div>
<div class="info-card p-3 bg-white rounded shadow-sm">
<div class="text-sm text-gray-500">借贷机构数</div>
<div class="text-lg font-semibold">{{ behaviorSummary.totalOrgs }}</div>
</div>
<div class="info-card p-3 bg-white rounded shadow-sm">
<div class="text-sm text-gray-500">总审批额度</div>
<div class="text-lg font-semibold">{{ behaviorSummary.totalAmount }}</div>
</div>
<div class="info-card p-3 bg-white rounded shadow-sm">
<div class="text-sm text-gray-500">月均申请次数</div>
<div class="text-lg font-semibold">{{ behaviorSummary.avgMonthlyApplications }}</div>
</div>
</div>
<div class="risk-assessment p-4 bg-white rounded-md">
<div class="text-lg font-bold mb-2">
风险评估:
<span :class="{
'text-red-500': behaviorSummary.riskLevel === '高',
'text-yellow-500': behaviorSummary.riskLevel === '中',
'text-green-500': behaviorSummary.riskLevel === '低',
}">{{ behaviorSummary.riskLevel }}风险</span>
</div>
<div class="text-gray-700">
<p>
· 月均审批额度:
{{ behaviorSummary.avgMonthlyAmount }}
</p>
<p>
· 月均应还金额:
{{ behaviorSummary.avgMonthlyRepay }}
</p>
<p>· 还款比例: {{ behaviorSummary.repayRatio }}</p>
<p>· {{ behaviorSummary.riskDesc }}</p>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.chart-container {
width: 100%;
height: 300px;
margin-bottom: 20px;
}
</style>

65
src/ui/CQCXG7A2B.vue Normal file
View File

@@ -0,0 +1,65 @@
<template>
<div class="card">
<!-- 名下车辆信息展示 -->
<div class="bg-yellow-100 text-yellow-700 p-4 rounded-lg">
<h3 class="text-xl font-semibold">名下车辆</h3>
<p class="text-sm">此人名下拥有车辆{{ data?.carNum }} </p>
</div>
<!-- 校验对象展示 -->
</div>
</template>
<script setup>
import { defineProps, watch, computed } from 'vue';
import { useRiskNotifier } from '@/composables/useRiskNotifier';
// 接收父组件传入的 props
const props = defineProps({
data: Object,
params: Object,
apiId: {
type: String,
default: '',
},
index: {
type: Number,
default: 0,
},
notifyRiskStatus: {
type: Function,
default: () => { },
},
});
// 脱敏函数:姓名脱敏(保留首位)
const maskName = (name) => {
if (!name) return '';
return name.length > 1 ? name[0] + "*".repeat(name.length - 1) : "*";
};
// 脱敏函数身份证号脱敏保留前6位和最后4位
const maskIdCard = (idCard) => {
if (!idCard) return '';
return idCard.replace(/^(.{6})(?:\d+)(.{4})$/, "$1****$2");
};
// 计算风险评分0-100分分数越高越安全
const riskScore = computed(() => {
// 名下车辆不算风险始终返回100分最安全
return 100;
});
// 使用 composable 通知父组件风险评分
useRiskNotifier(props, riskScore);
// 暴露给父组件
defineExpose({
riskScore
});
</script>
<style scoped>
/* 自定义样式 */
</style>

View File

@@ -42,7 +42,7 @@
<!-- 说明 -->
<div class="p-4">
<div class="text-xs text-gray-500">
<div class="text-sm text-gray-500">
<div>分数区间为0100分数越高风险越大</div>
<div class="mt-1">风险等级1=低风险2=中风险3=高风险</div>
</div>

View File

@@ -29,7 +29,7 @@
<!-- 说明 -->
<div class="p-4">
<div class="text-xs text-gray-500">
<div class="text-sm text-gray-500">
<div>风险等级1=低风险2=中风险3=高风险</div>
</div>
</div>

View File

@@ -142,7 +142,7 @@
</van-tab>
</van-tabs>
</div>
<div class="text-xs text-gray-500 px-4">白天8-23夜晚0点-7</div>
<div class="text-sm text-gray-500 px-4">白天8-23夜晚0点-7</div>
</div>
<!-- 白天/凌晨申请平台数统计 -->
@@ -257,7 +257,7 @@
</van-tab>
</van-tabs>
</div>
<div class="text-xs text-gray-500 px-4 mt-2">格式为 银行平台/总平台</div>
<div class="text-sm text-gray-500 px-4 mt-2">格式为 银行平台/总平台</div>
</div>
<!-- 查询天数差 -->
@@ -394,7 +394,7 @@ const applicationCountChartOption = computed(() => {
type: 'category',
data: periods,
axisLabel: {
fontSize: 10,
fontSize: 12,
color: '#6b7280',
rotate: 45
},
@@ -407,7 +407,7 @@ const applicationCountChartOption = computed(() => {
yAxis: {
type: 'value',
axisLabel: {
fontSize: 11,
fontSize: 12,
color: '#6b7280',
formatter: '{value} 次'
},

View File

@@ -37,7 +37,7 @@
{{ formatScore(data.longCycle) }}
</span>
</div>
<div class="text-xs text-gray-500 ml-2">长周期指窗口期6个月+</div>
<div class="text-sm text-gray-500 ml-2">长周期指窗口期6个月+</div>
<div class="flex justify-between items-center text-sm">
<span class="text-gray-600">非银行多头共债子分</span>
<span class="text-[#333333] font-bold" :class="getScoreClass(data.nonBank)">
@@ -55,7 +55,7 @@
<!-- 说明 -->
<div class="p-4">
<div class="text-xs text-gray-500">
<div class="text-sm text-gray-500">
<div>分数区间为0100分数越高风险越大</div>
<div class="mt-1">短周期指窗口期7天3个月</div>
</div>

View File

@@ -8,7 +8,7 @@
<span class="font-bold text-gray-800">多头逾期</span>
</div>
<div class="mt-4">
<div class="text-xs text-gray-500 px-4 mb-4">行为推断得到</div>
<div class="text-sm text-gray-500 px-4 mb-4">行为推断得到</div>
<!-- 逾期概览 -->
<div class="mb-6">
@@ -271,7 +271,7 @@ const overduePlatformChartOption = computed(() => {
type: 'category',
data: periods,
axisLabel: {
fontSize: 10,
fontSize: 12,
color: '#6b7280',
rotate: 45
},
@@ -284,7 +284,7 @@ const overduePlatformChartOption = computed(() => {
yAxis: {
type: 'value',
axisLabel: {
fontSize: 11,
fontSize: 12,
color: '#6b7280',
formatter: '{value} 家'
},

View File

@@ -1,5 +1,5 @@
<template>
<div>
<div class="flex flex-col gap-4">
<!-- 多头共债子分 -->
<MultipleDebtScoreSection :data="riskData.multipleDebtScore" />

View File

@@ -1,5 +1,5 @@
/**
* 解析多头借贷行业风险版数据
* 解析多头借贷数据
* @param {Array} riskInfo - riskInfo_report_v3.1 数组
* @returns {Object} 解析后的结构化数据
*/

View File

@@ -358,7 +358,7 @@
<span class="text-base text-[#666666]">后续案件</span>
<span class="text-base font-medium text-[#333333]">
{{ caseData.next.c_ah }}
<span v-if="caseData.next.stage_type" class="ml-2 text-xs px-2 py-0.5 rounded bg-[#EB3C3C1A] text-[#EB3C3C]">
<span v-if="caseData.next.stage_type" class="ml-2 text-sm px-2 py-0.5 rounded bg-[#EB3C3C1A] text-[#EB3C3C]">
{{
caseData.next.stage_type === 2
? "二审"

View File

@@ -30,7 +30,7 @@
<div class="p-4 bg-[#EB3C3C1A] border border-[#EB3C3C4D] rounded-xl text-center">
<div class="text-2xl font-bold text-[#EB3C3C] mb-1">{{ stats.totalRiskItems || 0 }}</div>
<div class="text-sm font-medium text-gray-800 mb-1">风险事项</div>
<div class="text-xs text-gray-500">
<div class="text-sm text-gray-500">
平均{{ stats.totalRiskItems && totalCases > 0 ?
(stats.totalRiskItems / totalCases).toFixed(1) :
'0.0'
@@ -42,12 +42,12 @@
<div class="p-4 bg-[#EB3C3C1A] border border-[#EB3C3C4D] rounded-xl text-center">
<div class="text-2xl font-bold text-[#EB3C3C] mb-1">{{ stats.highRiskItems || 0 }}</div>
<div class="text-sm font-medium text-gray-800 mb-1">高风险案件</div>
<div class="text-xs text-gray-500 mb-1">
<div class="text-sm text-gray-500 mb-1">
占比{{ totalCases > 0 && stats ?
((stats.highRiskItems /
totalCases) * 100).toFixed(1) : '0.0' }}%
</div>
<div class="text-xs text-orange-600">
<div class="text-sm text-orange-600">
<span class="mr-3">失信{{ stats.breachCaseCount || 0 }}</span>
<span style="color: #D6943E;">限高{{ stats.consumptionRestrictionCount || 0 }}</span>
</div>
@@ -57,7 +57,7 @@
<div class="p-4 bg-[#2B79EE1A] border border-[#2B79EE4D] rounded-xl text-center">
<div class="text-2xl font-bold text-[#2B79EE] mb-1">{{ stats.closedCases || 0 }}</div>
<div class="text-sm font-medium text-gray-800 mb-1">已结案件</div>
<div class="text-xs text-gray-500">
<div class="text-sm text-gray-500">
占比{{ totalCases > 0 && stats ?
Math.round((stats.closedCases / totalCases) * 100) :
0
@@ -69,7 +69,7 @@
<div class="p-4 bg-[#2B79EE1A] border border-[#2B79EE4D] rounded-xl text-center">
<div class="text-2xl font-bold text-[#2B79EE] mb-1">{{ stats.caseTypes.length || 0 }}</div>
<div class="text-sm font-medium text-gray-800 mb-1">案件类型</div>
<div class="text-xs text-gray-500">
<div class="text-sm text-gray-500">
涉及多种类型
</div>
</div>
@@ -200,7 +200,7 @@ const caseTypeChartOption = computed(() => {
},
},
axisLabel: {
fontSize: 10,
fontSize: 12,
color: '#666',
},
axisLine: {
@@ -213,7 +213,7 @@ const caseTypeChartOption = computed(() => {
type: 'category',
data: categories,
axisLabel: {
fontSize: 10,
fontSize: 12,
color: '#666',
},
axisLine: {
@@ -242,7 +242,7 @@ const caseTypeChartOption = computed(() => {
label: {
show: true,
position: 'right',
fontSize: 10,
fontSize: 12,
color: '#666',
formatter: function (params) {
// 如果是0.1实际为0显示为0

View File

@@ -54,7 +54,7 @@
<div class="font-bold text-base text-[#333333] mr-2">{{ caseItem.c_ah || caseItem.caseNumber || '暂无案号' }}</div>
<!-- 案件类型标签 -->
<span class="px-2 py-1 text-xs rounded-md font-medium bg-[#F9ECEC] text-[#EB3C3C]">
<span class="px-2 py-1 text-sm rounded-md font-medium bg-[#F9ECEC] text-[#EB3C3C]">
{{ getCaseTypeText(caseItem.type) }}
</span>
</div>
@@ -68,19 +68,19 @@
<!-- 底部区域风险等级和案件状态 -->
<div class="flex items-center gap-2">
<!-- 风险等级标签 -->
<span class="px-2 py-1 text-xs rounded-md font-medium"
<span class="px-2 py-1 text-sm rounded-md font-medium"
:class="getCaseTypeRiskLevel(caseItem.type).color">
{{ getCaseTypeRiskLevel(caseItem.type).text }}
</span>
<!-- 案件状态标签 -->
<span v-if="caseItem.n_ajjzjd" class="px-2 py-1 text-xs rounded-md font-medium"
<span v-if="caseItem.n_ajjzjd" class="px-2 py-1 text-sm rounded-md font-medium"
:class="getCaseStatusClass(caseItem.n_ajjzjd)">
{{ caseItem.n_ajjzjd }}
</span>
</div>
<!-- 展开指示器 -->
<div class="absolute right-4 bottom-3 flex items-center text-xs text-gray-500">
<div class="absolute right-4 bottom-3 flex items-center text-sm text-gray-500">
<img src="@/assets/images/report/zk.png" alt="展开" class="w-4 h-4 container"
:class="{ 'rotate-180': isCaseExpanded(caseItem.id || index, 'case', index) }" />
</div>

View File

@@ -1,337 +1,303 @@
// 案件类型映射表
export const lawsuitTypeMap = {
breachCase: {
text: "失信被执行",
color: "text-red-600 bg-red-50",
darkColor: "bg-red-500",
riskLevel: "high", // 高风险
},
consumptionRestriction: {
text: "限高被执行",
color: "text-orange-600 bg-orange-50",
darkColor: "bg-orange-500",
riskLevel: "high", // 高风险
},
criminal: {
text: "刑事案件",
color: "text-red-600 bg-red-50",
darkColor: "bg-red-500",
riskLevel: "high", // 高风险
},
civil: {
text: "民事案件",
color: "text-blue-600 bg-blue-50",
darkColor: "bg-blue-500",
riskLevel: "medium", // 中风险
},
administrative: {
text: "行政案件",
color: "text-purple-600 bg-purple-50",
darkColor: "bg-purple-500",
riskLevel: "medium", // 中风险
},
implement: {
text: "执行案件",
color: "text-orange-600 bg-orange-50",
darkColor: "bg-orange-500",
riskLevel: "medium", // 中风险
},
bankrupt: {
text: "强制清算与破产案件",
color: "text-rose-600 bg-rose-50",
darkColor: "bg-rose-500",
riskLevel: "high", // 高风险
},
preservation: {
text: "非诉保全审查",
color: "text-amber-600 bg-amber-50",
darkColor: "bg-amber-500",
riskLevel: "low", // 低风险
},
};
breachCase: {
text: '失信被执行',
color: 'text-red-600 bg-red-50',
darkColor: 'bg-red-500',
riskLevel: 'high', // 高风险
},
consumptionRestriction: {
text: '限高被执行',
color: 'text-orange-600 bg-orange-50',
darkColor: 'bg-orange-500',
riskLevel: 'high', // 高风险
},
criminal: {
text: '刑事案件',
color: 'text-red-600 bg-red-50',
darkColor: 'bg-red-500',
riskLevel: 'high', // 高风险
},
civil: {
text: '民事案件',
color: 'text-blue-600 bg-blue-50',
darkColor: 'bg-blue-500',
riskLevel: 'medium', // 中风险
},
administrative: {
text: '行政案件',
color: 'text-purple-600 bg-purple-50',
darkColor: 'bg-purple-500',
riskLevel: 'medium', // 中风险
},
implement: {
text: '执行案件',
color: 'text-orange-600 bg-orange-50',
darkColor: 'bg-orange-500',
riskLevel: 'medium', // 中风险
},
bankrupt: {
text: '强制清算与破产案件',
color: 'text-rose-600 bg-rose-50',
darkColor: 'bg-rose-500',
riskLevel: 'high', // 高风险
},
preservation: {
text: '非诉保全审查',
color: 'text-amber-600 bg-amber-50',
darkColor: 'bg-amber-500',
riskLevel: 'low', // 低风险
},
}
// 案件类型文本
export const getCaseTypeText = (type) => {
return lawsuitTypeMap[type]?.text || "其他案件";
};
export const getCaseTypeText = type => {
return lawsuitTypeMap[type]?.text || '其他案件'
}
// 案件类型颜色
export const getCaseTypeColor = (type) => {
return lawsuitTypeMap[type]?.color || "text-gray-600 bg-gray-50";
};
export const getCaseTypeColor = type => {
return lawsuitTypeMap[type]?.color || 'text-gray-600 bg-gray-50'
}
// 案件类型深色
export const getCaseTypeDarkColor = (type) => {
return lawsuitTypeMap[type]?.darkColor || "bg-gray-500";
};
export const getCaseTypeDarkColor = type => {
return lawsuitTypeMap[type]?.darkColor || 'bg-gray-500'
}
// 格式化日期显示
export const formatDate = (dateStr) => {
if (!dateStr) return "—";
// 转换YYYY-MM-DD为年月日格式
if (dateStr.includes("-")) {
const parts = dateStr.split("-");
if (parts.length === 3) {
return `${parts[0]}${parts[1]}${parts[2]}`;
}
export const formatDate = dateStr => {
if (!dateStr) return '—'
// 转换YYYY-MM-DD为年月日格式
if (dateStr.includes('-')) {
const parts = dateStr.split('-')
if (parts.length === 3) {
return `${parts[0]}${parts[1]}${parts[2]}`
}
return dateStr; // 如果不是标准格式则返回原始字符串
};
}
return dateStr // 如果不是标准格式则返回原始字符串
}
// 格式化金额显示(默认单位:元)
export const formatLawsuitMoney = (money) => {
if (!money) return "—";
// 格式化金额显示(单位:元)
export const formatLawsuitMoney = money => {
if (!money) return '—'
const value = parseFloat(money);
if (isNaN(value)) return "—";
const value = parseFloat(money)
if (isNaN(value)) return '—'
// 超过1亿100000000元显示亿元
if (value >= 100000000) {
return (
(value / 100000000).toLocaleString("zh-CN", {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
}) + " 亿元"
);
}
// 超过1万10000元显示万元
if (value >= 10000) {
return (
(value / 10000).toLocaleString("zh-CN", {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
}) + " 万元"
);
}
// 小于1万直接显示元
// 超过1亿显示亿元
if (value >= 10000) {
return (
value.toLocaleString("zh-CN", {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
}) + " 元"
);
};
(value / 10000).toLocaleString('zh-CN', {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
}) + ' 亿元'
)
}
// 否则显示万元
return (
value.toLocaleString('zh-CN', {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
}) + ' 万元'
)
}
// 获取案件状态样式
export const getCaseStatusClass = (status) => {
if (!status) return "bg-gray-100 text-gray-500";
export const getCaseStatusClass = status => {
if (!status) return 'bg-gray-100 text-gray-500'
if (status.includes("已结") || status.includes("已办结")) {
return "bg-green-50 text-green-600";
} else if (status.includes("执行中") || status.includes("审理中")) {
return "bg-blue-50 text-blue-600";
} else if (status.includes("未执行")) {
return "bg-amber-50 text-amber-600";
} else {
return "bg-gray-100 text-gray-500";
}
};
if (status.includes('已结') || status.includes('已办结')) {
return 'bg-green-50 text-green-600'
} else if (status.includes('执行中') || status.includes('审理中')) {
return 'bg-blue-50 text-blue-600'
} else if (status.includes('未执行')) {
return 'bg-amber-50 text-amber-600'
} else {
return 'bg-gray-100 text-gray-500'
}
}
// 获取企业状态对应的样式
export const getStatusClass = (status) => {
if (!status) return "bg-gray-100 text-gray-500";
export const getStatusClass = status => {
if (!status) return 'bg-gray-100 text-gray-500'
if (status.includes("注销") || status.includes("吊销")) {
return "bg-red-50 text-red-600";
} else if (status.includes("存续") || status.includes("在营")) {
return "bg-green-50 text-green-600";
} else if (status.includes("筹建") || status.includes("新设")) {
return "bg-blue-50 text-blue-600";
} else {
return "bg-yellow-50 text-yellow-600";
}
};
if (status.includes('注销') || status.includes('吊销')) {
return 'bg-red-50 text-red-600'
} else if (status.includes('存续') || status.includes('在营')) {
return 'bg-green-50 text-green-600'
} else if (status.includes('筹建') || status.includes('新设')) {
return 'bg-blue-50 text-blue-600'
} else {
return 'bg-yellow-50 text-yellow-600'
}
}
// 格式化资本金额显示
export const formatCapital = (capital, currency) => {
if (!capital) return "—";
if (!capital) return '—'
// 检查是否包含"万"字或需要显示为万元
let unit = "";
let value = parseFloat(capital);
// 检查是否包含"万"字或需要显示为万元
let unit = ''
let value = parseFloat(capital)
// 处理原始数据中可能带有的单位
if (typeof capital === "string" && capital.includes("万")) {
unit = "万";
// 提取数字部分
const numMatch = capital.match(/[\d.]+/);
value = numMatch ? parseFloat(numMatch[0]) : 0;
} else if (value >= 10000) {
// 大额数字转换为万元显示
value = value / 10000;
unit = "万";
}
// 处理原始数据中可能带有的单位
if (typeof capital === 'string' && capital.includes('万')) {
unit = '万'
// 提取数字部分
const numMatch = capital.match(/[\d.]+/)
value = numMatch ? parseFloat(numMatch[0]) : 0
} else if (value >= 10000) {
// 大额数字转换为万元显示
value = value / 10000
unit = '万'
}
// 格式化数字,保留两位小数(如果有小数部分)
const formattedValue = value.toLocaleString("zh-CN", {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
});
// 格式化数字,保留两位小数(如果有小数部分)
const formattedValue = value.toLocaleString('zh-CN', {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
})
return `${formattedValue}${unit} ${currency || "人民币"}`;
};
return `${formattedValue}${unit} ${currency || '人民币'}`
}
// 获取涉诉风险等级
export const getRiskLevel = (lawsuitInfo) => {
if (!lawsuitInfo) {
return {
level: "low",
text: "低风险",
color: "text-green-600 bg-green-50",
};
}
// 失信被执行人是最高风险
if (lawsuitInfo.breachCaseList && lawsuitInfo.breachCaseList.length > 0) {
return {
level: "high",
text: "高风险",
color: "text-red-600 bg-red-50",
};
}
// 限高被执行人是最高风险
if (
lawsuitInfo.consumptionRestrictionList &&
lawsuitInfo.consumptionRestrictionList.length > 0
) {
return {
level: "high",
text: "高风险",
color: "text-red-600 bg-red-50",
};
}
// 有涉诉数据的风险级别
if (
lawsuitInfo.lawsuitStat &&
Object.keys(lawsuitInfo.lawsuitStat).length > 0
) {
// 检查是否有未结案的案件
const data = lawsuitInfo.lawsuitStat;
if (
data.count &&
data.count.count_wei_total &&
data.count.count_wei_total > 0
) {
return {
level: "medium",
text: "中风险",
color: "text-amber-600 bg-amber-50",
};
}
// 只有已结案的为低中风险
return {
level: "low-medium",
text: "低中风险",
color: "text-yellow-600 bg-yellow-50",
};
}
export const getRiskLevel = lawsuitInfo => {
if (!lawsuitInfo) {
return {
level: "low",
text: "低风险",
color: "text-green-600 bg-green-50",
};
};
level: 'low',
text: '低风险',
color: 'text-green-600 bg-green-50',
}
}
// 失信被执行人是最高风险
if (lawsuitInfo.breachCaseList && lawsuitInfo.breachCaseList.length > 0) {
return {
level: 'high',
text: '高风险',
color: 'text-red-600 bg-red-50',
}
}
// 限高被执行人是最高风险
if (lawsuitInfo.consumptionRestrictionList && lawsuitInfo.consumptionRestrictionList.length > 0) {
return {
level: 'high',
text: '高风险',
color: 'text-red-600 bg-red-50',
}
}
// 有涉诉数据的风险级别
if (lawsuitInfo.lawsuitStat && Object.keys(lawsuitInfo.lawsuitStat).length > 0) {
// 检查是否有未结案的案件
const data = lawsuitInfo.lawsuitStat
if (data.count && data.count.count_wei_total && data.count.count_wei_total > 0) {
return {
level: 'medium',
text: '中风险',
color: 'text-amber-600 bg-amber-50',
}
}
// 只有已结案的为低中风险
return {
level: 'low-medium',
text: '低中风险',
color: 'text-yellow-600 bg-yellow-50',
}
}
return {
level: 'low',
text: '低风险',
color: 'text-green-600 bg-green-50',
}
}
// 获取涉诉案件统计
export const getLawsuitStats = (lawsuitInfo) => {
if (!lawsuitInfo) return null;
export const getLawsuitStats = lawsuitInfo => {
if (!lawsuitInfo) return null
const stats = {
total: 0,
types: [],
};
const stats = {
total: 0,
types: [],
}
// 统计各类型案件数量
Object.keys(lawsuitTypeMap).forEach((type) => {
let count = 0;
// 统计各类型案件数量
Object.keys(lawsuitTypeMap).forEach(type => {
let count = 0
if (type === "breachCase") {
count =
lawsuitInfo.breachCaseList &&
lawsuitInfo.breachCaseList.length > 0
? lawsuitInfo.breachCaseList.length
: 0;
} else if (type === "consumptionRestriction") {
count =
lawsuitInfo.consumptionRestrictionList &&
lawsuitInfo.consumptionRestrictionList.length > 0
? lawsuitInfo.consumptionRestrictionList.length
: 0;
} else if (
lawsuitInfo.lawsuitStat &&
lawsuitInfo.lawsuitStat[type] &&
Object.keys(lawsuitInfo.lawsuitStat[type]).length > 0
) {
const typeData = lawsuitInfo.lawsuitStat[type];
count =
typeData.cases && typeData.cases.length
? typeData.cases.length
: 0;
}
if (type === 'breachCase') {
count = lawsuitInfo.breachCaseList && lawsuitInfo.breachCaseList.length > 0 ? lawsuitInfo.breachCaseList.length : 0
} else if (type === 'consumptionRestriction') {
count = lawsuitInfo.consumptionRestrictionList && lawsuitInfo.consumptionRestrictionList.length > 0 ? lawsuitInfo.consumptionRestrictionList.length : 0
} else if (lawsuitInfo.lawsuitStat && lawsuitInfo.lawsuitStat[type] && Object.keys(lawsuitInfo.lawsuitStat[type]).length > 0) {
const typeData = lawsuitInfo.lawsuitStat[type]
count = typeData.cases && typeData.cases.length ? typeData.cases.length : 0
}
if (count > 0) {
stats.total += count;
stats.types.push({
type,
count,
name: getCaseTypeText(type),
color: getCaseTypeColor(type),
darkColor: getCaseTypeDarkColor(type),
});
}
});
if (count > 0) {
stats.total += count
stats.types.push({
type,
count,
name: getCaseTypeText(type),
color: getCaseTypeColor(type),
darkColor: getCaseTypeDarkColor(type),
})
}
})
return stats;
};
return stats
}
// 获取案件类型优先级顺序
export const getCaseTypePriority = () => {
return [
"breachCase", // 失信被执行人(最高风险)
"consumptionRestriction", // 限高被执行人
"criminal", // 刑事案件
"civil", // 民事案件
"administrative", // 行政案件
"implement", // 执行案件
"bankrupt", // 强制清算与破产案件
"preservation", // 非诉保全审查
];
};
return [
'breachCase', // 失信被执行人(最高风险)
'consumptionRestriction', // 限高被执行人
'criminal', // 刑事案件
'civil', // 民事案件
'administrative', // 行政案件
'implement', // 执行案件
'bankrupt', // 强制清算与破产案件
'preservation', // 非诉保全审查
]
}
// 根据案件类型获取风险等级
export const getCaseTypeRiskLevel = (caseType) => {
const typeInfo = lawsuitTypeMap[caseType];
if (!typeInfo) {
return {
level: "low",
text: "低风险",
color: "text-green-600 bg-green-50",
};
}
const riskLevelMap = {
high: {
text: "高风险",
color: "text-red-600 bg-red-50",
},
medium: {
text: "中风险",
color: "text-amber-600 bg-amber-50",
},
low: {
text: "低风险",
color: "text-green-600 bg-green-50",
},
};
export const getCaseTypeRiskLevel = caseType => {
const typeInfo = lawsuitTypeMap[caseType]
if (!typeInfo) {
return {
level: typeInfo.riskLevel,
...riskLevelMap[typeInfo.riskLevel],
};
};
level: 'low',
text: '低风险',
color: 'text-green-600 bg-green-50',
}
}
const riskLevelMap = {
high: {
text: '高风险',
color: 'text-red-600 bg-red-50',
},
medium: {
text: '中风险',
color: 'text-amber-600 bg-amber-50',
},
low: {
text: '低风险',
color: 'text-green-600 bg-green-50',
},
}
return {
level: typeInfo.riskLevel,
...riskLevelMap[typeInfo.riskLevel],
}
}

696
src/ui/IVYZ3P9M.vue Normal file
View File

@@ -0,0 +1,696 @@
<script setup>
import { computed } from 'vue';
import { useRiskNotifier } from '@/composables/useRiskNotifier';
import xlIcon from '@/assets/images/report/xl.png';
import zymcIcon from '@/assets/images/report/zymc.png';
import xxxsIcon from '@/assets/images/report/xxxs.png';
import xxlxIcon from '@/assets/images/report/xxlx.png';
import bysjIcon from '@/assets/images/report/bysj.png';
import dictionaries from '@/data/ivyz3p9m-dictionary.json';
const props = defineProps({
data: {
type: [Array, Object],
default: () => [],
},
apiId: {
type: String,
default: '',
},
index: {
type: Number,
default: 0,
},
notifyRiskStatus: {
type: Function,
default: () => { },
},
});
const riskScore = computed(() => 100);
useRiskNotifier(props, riskScore);
defineExpose({ riskScore });
const educationLevelMap = dictionaries.educationLevel ?? {};
const learningFormMap = dictionaries.learningForm ?? {};
const schoolDictionary = dictionaries.schools ?? {};
const specialtyDictionary = dictionaries.specialties ?? {};
const educationTagColorMap = {
'1': 'bg-emerald-50 text-emerald-700 border-emerald-200',
'2': 'bg-blue-50 text-blue-700 border-blue-200',
'3': 'bg-indigo-50 text-indigo-700 border-indigo-200',
'4': 'bg-purple-50 text-purple-700 border-purple-200',
'5': 'bg-amber-50 text-amber-700 border-amber-200',
};
const learningFormColorMap = {
'1': 'bg-sky-50 text-sky-700 border-sky-200',
'2': 'bg-blue-50 text-blue-700 border-blue-200',
'3': 'bg-blue-50 text-blue-700 border-blue-200',
'4': 'bg-cyan-50 text-cyan-700 border-cyan-200',
'5': 'bg-orange-50 text-orange-700 border-orange-200',
'6': 'bg-lime-50 text-lime-700 border-lime-200',
'7': 'bg-indigo-50 text-indigo-700 border-indigo-200',
'8': 'bg-violet-50 text-violet-700 border-violet-200',
'9': 'bg-fuchsia-50 text-fuchsia-700 border-fuchsia-200',
};
const normalizeCode = (value) => {
if (value === null || value === undefined) return '';
return String(value).trim();
};
const getEducationLevelText = (code) => {
const normalized = normalizeCode(code);
if (!normalized) return '未知';
return educationLevelMap[normalized] || '未知';
};
const getLearningFormText = (code) => {
const normalized = normalizeCode(code);
if (!normalized) return '未知';
return learningFormMap[normalized] || '未知';
};
const getSchoolNameText = (code, fallback) => {
const normalized = normalizeCode(code);
if (!normalized) return fallback || '未知学校';
return schoolDictionary[normalized] || fallback || '未知学校';
};
const getSpecialtyNameText = (code, fallback) => {
const normalized = normalizeCode(code);
if (!normalized) return fallback || '未知专业';
return specialtyDictionary[normalized] || fallback || '未知专业';
};
const getEducationLevelClass = (code) => {
const normalized = normalizeCode(code);
return educationTagColorMap[normalized] || 'bg-gray-50 text-gray-700 border-gray-200';
};
const getLearningFormClass = (code) => {
const normalized = normalizeCode(code);
return learningFormColorMap[normalized] || 'bg-gray-50 text-gray-700 border-gray-200';
};
const maskIdNumber = (idNumber) => {
if (!idNumber) return '未知';
const normalized = String(idNumber).trim();
if (normalized.length <= 6) {
return `${normalized.slice(0, 1)}****${normalized.slice(-1)}`;
}
return `${normalized.slice(0, 3)}********${normalized.slice(-4)}`;
};
const formatDate = (dateStr) => {
if (!dateStr) return '未知';
const normalized = String(dateStr).trim();
if (!/^\d+$/.test(normalized)) return '未知';
if (normalized.length === 8) {
const year = normalized.substring(0, 4);
const month = normalized.substring(4, 6);
const day = normalized.substring(6, 8);
return `${year}${month}${day}`;
}
if (normalized.length === 6) {
const year = normalized.substring(0, 4);
const month = normalized.substring(4, 6);
return `${year}${month}`;
}
if (normalized.length === 4) {
const shortYear = normalized.substring(0, 2);
const month = normalized.substring(2, 4);
return `20${shortYear}${month}`;
}
return '未知';
};
const parseDate = (value) => {
if (!value) return null;
const normalized = String(value).trim();
if (!/^\d+$/.test(normalized)) return null;
let year;
let month;
let day = 1;
if (normalized.length === 8) {
year = Number(normalized.substring(0, 4));
month = Number(normalized.substring(4, 6));
day = Number(normalized.substring(6, 8));
} else if (normalized.length === 6) {
year = Number(normalized.substring(0, 4));
month = Number(normalized.substring(4, 6));
} else if (normalized.length === 4) {
year = Number(`20${normalized.substring(0, 2)}`);
month = Number(normalized.substring(2, 4));
} else {
return null;
}
if (!year || !month) return null;
return new Date(year, month - 1, day);
};
const calculateStudyDuration = (start, end) => {
const startDate = parseDate(start);
const endDate = parseDate(end);
if (!startDate || !endDate || endDate < startDate) {
return '时长未知';
}
const totalMonths =
(endDate.getFullYear() - startDate.getFullYear()) * 12 +
(endDate.getMonth() - startDate.getMonth());
if (totalMonths <= 0) {
return '不足 1 个月';
}
const years = Math.floor(totalMonths / 12);
const months = totalMonths % 12;
const parts = [];
if (years > 0) parts.push(`${years}`);
if (months > 0) parts.push(`${months}个月`);
return parts.length > 0 ? `${parts.join('')}` : '约 1 个月';
};
const educationRecords = computed(() => {
const source = props.data;
if (Array.isArray(source)) return source;
if (Array.isArray(source?.data)) return source.data;
if (Array.isArray(source?.records)) return source.records;
if (Array.isArray(source?.list)) return source.list;
return [];
});
const enhancedRecords = computed(() =>
educationRecords.value.map((record, index) => {
const educationLevelCode = normalizeCode(record.educationLevel);
const learningFormCode = normalizeCode(record.learningForm);
const schoolCode = normalizeCode(record.schoolName);
const specialtyCode = normalizeCode(record.specialtyName);
const schoolName = getSchoolNameText(schoolCode, record.schoolName);
const specialtyName = getSpecialtyNameText(specialtyCode, record.specialtyName);
return {
index: index + 1,
studentName: record.studentName || '未知',
idNumber: record.idNumber || '',
maskedIdNumber: maskIdNumber(record.idNumber),
schoolName,
specialtyName,
isUnknownSchool: schoolName === '未知学校' || (!schoolCode && !record.schoolName),
isUnknownSpecialty: specialtyName === '未知专业' || (!specialtyCode && !record.specialtyName),
educationLevelCode,
educationLevel: getEducationLevelText(educationLevelCode),
learningFormCode,
learningForm: getLearningFormText(learningFormCode),
enrollmentDate: formatDate(record.enrollmentDate),
graduationDate: formatDate(record.graduationDate),
rawEnrollmentDate: record.enrollmentDate || '',
rawGraduationDate: record.graduationDate || '',
studyDuration: calculateStudyDuration(record.enrollmentDate, record.graduationDate),
};
})
);
const getTimestamp = (value) => {
const date = parseDate(value);
return date ? date.getTime() : null;
};
const orderedRecords = computed(() => {
if (enhancedRecords.value.length <= 1) return enhancedRecords.value;
return [...enhancedRecords.value].sort((a, b) => {
const startA =
getTimestamp(a.rawEnrollmentDate) ??
getTimestamp(a.rawGraduationDate) ??
Number.MAX_SAFE_INTEGER;
const startB =
getTimestamp(b.rawEnrollmentDate) ??
getTimestamp(b.rawGraduationDate) ??
Number.MAX_SAFE_INTEGER;
if (startA === startB) {
const endA = getTimestamp(a.rawGraduationDate) ?? Number.MAX_SAFE_INTEGER;
const endB = getTimestamp(b.rawGraduationDate) ?? Number.MAX_SAFE_INTEGER;
return endA - endB;
}
return startA - startB;
});
});
const educationRankMap = {
'1': 1,
'2': 2,
'3': 3,
'4': 4,
'5': 2.5,
};
const getEducationRank = (code) => {
const normalized = normalizeCode(code);
return educationRankMap[normalized] ?? 0;
};
const summaryRecord = computed(() => {
if (orderedRecords.value.length === 0) return null;
return orderedRecords.value.reduce((best, current) => {
if (!best) return current;
const currentRank = getEducationRank(current.educationLevelCode);
const bestRank = getEducationRank(best.educationLevelCode);
if (currentRank > bestRank) return current;
if (currentRank < bestRank) return best;
const currentGrad = getTimestamp(current.rawGraduationDate) ?? Number.NEGATIVE_INFINITY;
const bestGrad = getTimestamp(best.rawGraduationDate) ?? Number.NEGATIVE_INFINITY;
return currentGrad >= bestGrad
? current
: best;
}, null);
});
const latestGraduationText = computed(() => {
if (orderedRecords.value.length === 0) return '未知';
const latest = orderedRecords.value.reduce((latestRecord, current) => {
if (!latestRecord) return current;
const currentGrad = getTimestamp(current.rawGraduationDate) ?? Number.NEGATIVE_INFINITY;
const latestGrad = getTimestamp(latestRecord.rawGraduationDate) ?? Number.NEGATIVE_INFINITY;
return currentGrad >= latestGrad
? current
: latestRecord;
}, null);
return latest?.graduationDate || '未知';
});
const hasData = computed(() => orderedRecords.value.length > 0);
</script>
<template>
<div v-if="hasData" class="card max-w-4xl mx-auto">
<div class="flex flex-col gap-6">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div class="flex items-center gap-3">
<div class="w-12 h-12 flex items-center justify-center">
<img :src="xlIcon" alt="学历信息" class="w-12 h-12" />
</div>
<div>
<h2 class="text-2xl font-bold text-gray-900">学历信息查询</h2>
</div>
</div>
<div class="flex flex-col items-start md:items-end gap-1">
<div class="text-lg font-semibold text-blue-600">
{{ orderedRecords.length }} 条记录
</div>
<div v-if="summaryRecord" class="summary-meta text-sm text-gray-500">
<span class="summary-meta__item">最高学历{{ summaryRecord.educationLevel }}</span>
<span v-if="latestGraduationText" class="summary-meta__divider">·</span>
<span class="summary-meta__item">最新毕业时间{{ latestGraduationText }}</span>
</div>
</div>
</div>
<div v-if="summaryRecord" class="summary-banner">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
<div class="flex items-center gap-4">
<div class="w-14 h-14 rounded-full bg-white/80 flex items-center justify-center shadow-inner">
<img :src="xlIcon" alt="学历图标" class="w-10 h-10" />
</div>
<div>
<div class="text-xl font-semibold text-gray-900">{{ summaryRecord.studentName }}</div>
<div class="text-sm text-slate-600">身份证{{ summaryRecord.maskedIdNumber }}</div>
<div class="summary-highlight">
<span class="summary-highlight__badge">{{ summaryRecord.educationLevel }}</span>
<div class="summary-highlight__text flex flex-col gap-1">
<span class="flex items-center gap-2">
{{ summaryRecord.schoolName }}
<span v-if="summaryRecord.isUnknownSchool" class="unknown-hint">
<svg class="unknown-icon" xmlns="http://www.w3.org/2000/svg" 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>
</span>
</span>
<span v-if="summaryRecord.isUnknownSchool" class="unknown-text">该学校名称信息未找到可能是学校已改名</span>
</div>
</div>
</div>
</div>
<!-- <div class="flex flex-wrap items-center gap-3">
<div class="summary-chip">
<span class="chip-label">入学时间</span>
<span class="chip-value">{{ summaryRecord.enrollmentDate }}</span>
</div>
<div class="summary-chip">
<span class="chip-label">毕业时间</span>
<span class="chip-value">{{ summaryRecord.graduationDate }}</span>
</div>
<div class="summary-chip">
<span class="chip-label">学习时长</span>
<span class="chip-value">{{ summaryRecord.studyDuration }}</span>
</div>
</div> -->
</div>
</div>
<div class="space-y-5">
<div v-for="record in orderedRecords" :key="record.index" class="record-card">
<div class="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-4">
<div class="record-header">
<div class="record-index">
{{ record.index }}
</div>
<div class="record-title">
<div class="record-title__name flex flex-col gap-1">
<span class="flex items-center gap-2">
{{ record.schoolName }}
<span v-if="record.isUnknownSchool" class="unknown-hint">
<svg class="unknown-icon" xmlns="http://www.w3.org/2000/svg" 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>
</span>
</span>
<span v-if="record.isUnknownSchool" class="unknown-text">该学校名称信息未找到可能是学校已改名</span>
</div>
<div class="record-title__meta" v-if="record.enrollmentDate !== '未知' || record.graduationDate !== '未知'">
<span v-if="record.enrollmentDate !== '未知'">{{ record.enrollmentDate }}</span>
<span v-if="record.enrollmentDate !== '未知' && record.graduationDate !== '未知'"> - </span>
<span v-if="record.graduationDate !== '未知'">{{ record.graduationDate }}</span>
</div>
</div>
</div>
<div class="flex flex-wrap items-center gap-3">
<span :class="['tag', getEducationLevelClass(record.educationLevelCode)]">
{{ record.educationLevel }}学历
</span>
<span :class="['tag', getLearningFormClass(record.learningFormCode)]">
学习形式{{ record.learningForm }}
</span>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6">
<div class="info-block">
<div class="info-icon">
<img :src="zymcIcon" alt="专业名称" class="w-7 h-7" />
</div>
<div>
<div class="info-label">专业名称</div>
<div class="info-value flex flex-col gap-1">
<span class="flex items-center gap-2">
{{ record.specialtyName }}
<span v-if="record.isUnknownSpecialty" class="unknown-hint">
<svg class="unknown-icon" xmlns="http://www.w3.org/2000/svg" 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>
</span>
</span>
<span v-if="record.isUnknownSpecialty" class="unknown-text">该专业名称信息未找到可能是该学校专业已受到变动</span>
</div>
</div>
</div>
<div class="info-block">
<div class="info-icon">
<img :src="xxxsIcon" alt="学习形式" class="w-7 h-7" />
</div>
<div>
<div class="info-label">学习形式</div>
<div class="info-value">
{{ record.learningForm }}
</div>
</div>
</div>
<div class="info-block">
<div class="info-icon">
<img :src="xxlxIcon" alt="入学时间" class="w-7 h-7" />
</div>
<div>
<div class="info-label">入学时间</div>
<div class="info-value">{{ record.enrollmentDate }}</div>
</div>
</div>
<div class="info-block">
<div class="info-icon">
<img :src="bysjIcon" alt="毕业时间" class="w-7 h-7" />
</div>
<div>
<div class="info-label">毕业时间</div>
<div class="info-value">{{ record.graduationDate }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div v-else class="card max-w-3xl mx-auto">
<div class="flex flex-col items-center py-12 text-center">
<div class="w-20 h-20 flex items-center justify-center mb-4">
<img :src="xlIcon" alt="学历图标" class="w-20 h-20 opacity-40" />
</div>
<h3 class="text-lg font-medium text-gray-900 mb-2">暂无学历记录</h3>
<p class="text-sm text-gray-500 max-w-md">
未查询到相关的学历信息可能是数据正在更新或尚未录入建议稍后重试或联系数据提供方确认
</p>
</div>
</div>
</template>
<style lang="scss" scoped>
.card {
padding: 1.5rem;
box-shadow: 0px 0px 24px 0px #3f3f3f0f;
border-radius: 12px;
background: white;
}
.summary-banner {
border-radius: 16px;
padding: 1.5rem;
background: linear-gradient(135deg, rgba(59, 130, 246, 0.1), rgba(79, 70, 229, 0.12));
border: 1px solid rgba(59, 130, 246, 0.25);
}
.summary-chip {
display: inline-flex;
flex-direction: column;
gap: 0.15rem;
padding: 0.5rem 0.75rem;
border-radius: 0.75rem;
background-color: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(148, 163, 184, 0.3);
}
.chip-label {
font-size: 0.7rem;
color: #64748b;
letter-spacing: 0.02em;
text-transform: uppercase;
}
.chip-value {
font-size: 0.95rem;
font-weight: 600;
color: #0f172a;
}
.summary-meta {
display: inline-flex;
align-items: center;
gap: 0.35rem;
flex-wrap: wrap;
}
.summary-meta__divider {
color: #94a3b8;
}
.summary-highlight {
margin-top: 0.35rem;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.summary-highlight__badge {
padding: 0.2rem 0.6rem;
border-radius: 9999px;
background: rgba(59, 130, 246, 0.12);
color: #2563eb;
font-size: 0.75rem;
font-weight: 600;
}
.summary-highlight__text {
font-size: 0.9rem;
color: #1e293b;
font-weight: 500;
}
.record-card {
position: relative;
padding: 1.5rem;
border-radius: 16px;
border: 1px solid rgba(226, 232, 240, 0.9);
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
box-shadow: 0 10px 25px rgba(15, 23, 42, 0.06);
transition: all 0.25s ease;
}
.record-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 4px;
background: linear-gradient(90deg, rgba(59, 130, 246, 0.75), rgba(129, 140, 248, 0.75));
border-radius: 12px 12px 0 0;
}
.record-card:hover {
transform: translateY(-4px);
box-shadow: 0 16px 35px rgba(59, 130, 246, 0.16);
}
.record-header {
display: flex;
align-items: flex-start;
gap: 1rem;
}
.record-index {
width: 3rem;
height: 3rem;
border-radius: 999px;
background: rgba(59, 130, 246, 0.18);
color: #2563eb;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.05rem;
flex-shrink: 0;
}
.record-title {
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.record-title__name {
font-size: 1.125rem;
font-weight: 600;
color: #0f172a;
}
.record-title__meta {
font-size: 0.85rem;
color: #64748b;
}
.tag {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.4rem 0.85rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 600;
border-width: 1px;
border-style: solid;
}
.info-block {
display: flex;
gap: 0.75rem;
padding: 1rem;
border-radius: 12px;
background-color: rgba(248, 250, 252, 0.9);
border: 1px solid rgba(226, 232, 240, 0.8);
transition: border-color 0.2s ease, transform 0.2s ease;
}
.info-block:hover {
border-color: rgba(59, 130, 246, 0.3);
transform: translateY(-2px);
}
.info-icon {
width: 2.5rem;
height: 2.5rem;
border-radius: 0.75rem;
background: rgba(59, 130, 246, 0.12);
display: flex;
align-items: center;
justify-content: center;
}
.info-label {
font-size: 0.75rem;
color: #64748b;
margin-bottom: 0.25rem;
}
.info-value {
font-size: 1rem;
font-weight: 500;
color: #0f172a;
display: flex;
align-items: center;
gap: 0.5rem;
}
.unknown-hint {
display: inline-flex;
align-items: center;
}
.unknown-icon {
width: 1rem;
height: 1rem;
color: #f59e0b;
flex-shrink: 0;
}
.unknown-text {
font-size: 0.75rem;
color: #f59e0b;
line-height: 1.4;
margin-top: 0.125rem;
}
@media (max-width: 768px) {
.record-card {
padding: 1.25rem;
}
.info-block {
padding: 0.85rem;
}
}
</style>

View File

@@ -14,7 +14,7 @@
<div v-for="item in detectionItems" :key="item.key" class="rounded-xl p-4 relative"
:class="getItemResultClass(item.hit)">
<div class="absolute top-0 right-0">
<div class="px-2 py-1 text-xs text-white rounded-bl-xl rounded-tr-xl"
<div class="px-2 py-1 text-sm text-white rounded-bl-xl rounded-tr-xl"
:class="getItemTagClass(item.hit)">
{{ getItemText(item.hit) }}
</div>
@@ -33,6 +33,10 @@
</div>
</div>
</div>
<!-- 温馨提示 -->
<LRemark
content="是否与黑中介有关联:与从事包装客户资料,伪造客户资料,冒用客户资料,套取机构政策等职业的用户或者机构成员有关联。\n是否疑似与异常行业有关联互联网行为疑似涉嫌色情、赌博、毒品等不良行为。\n是否疑似虚假资料在社交平台提供过虚假资料或者有恶意申请/操作记录,或者个人信息疑似泄漏、冒用、伪造等。\n是否疑似羊毛党在网贷、电商、O2O等平台有薅羊毛行为的用户。\n是否身份信息存疑未获取到社交平台中的身份信息或者身份信息身份证、手机号、姓名疑似涉嫌伪造。\n是否严重异常行为疑似有恶意消费的行为。\n是否存在失信行为客户有失信行为。\n是否存在支付异常行为支付行为异常包括支付频次、额度、场景等方面有过异常行为。\n是否存在其他异常行为用户和以下高风险行为可能存在较高关联度被盗风险较高、社交圈子不固定、地理圈子变化较大。\n是否上网环境异常用户上网时有使用虚拟机、代理设备、代理IP、猫池等行为。" />
</div>
</template>

View File

@@ -6,7 +6,7 @@
</div>
<span class="font-bold text-gray-800">申请行为详情</span>
</div>
<span class="text-sm text-gray-500 mb-2 mx-4">0-1之间指数越大用户逾期可能性越高</span>
<div class="p-4">
<!-- 核心指标 -->
<div class="grid grid-cols-2 gap-4 mb-6">
@@ -29,7 +29,7 @@
<!-- 申请命中情况 -->
<LTitle title="申请命中情况" />
<div class="grid grid-cols-2 gap-4 mb-6 mt-3">
<div class="grid grid-cols-1 gap-4 mb-6 mt-3">
<div class="bg-gray-50 rounded-lg p-3">
<div class="text-sm text-gray-600 mb-1">申请命中机构数</div>
<div class="text-lg font-bold text-gray-800">{{ getValue(data.A22160003, '0') }} </div>
@@ -59,17 +59,17 @@
</div>
<div class="grid grid-cols-3 gap-3">
<div class="bg-blue-50 rounded-lg p-3 text-center border border-[#2B79EE8F]">
<div class="text-xl font-bold text-[#2B79EE] mb-1">{{ getValue(data.A22160008, '0') }}</div>
<div class="text-xs text-gray-600">近1个月查询笔数</div>
<div class="bg-blue-50 rounded-lg px-3 py-6 text-center border border-[#2B79EE8F]">
<div class="text-3xl font-bold text-[#2B79EE] mb-1">{{ getValue(data.A22160008, '0') }}</div>
<div class="text-sm text-gray-600">近1个月查询笔数</div>
</div>
<div class="bg-blue-50 rounded-lg p-3 text-center border border-[#2B79EE8F]">
<div class="text-xl font-bold text-[#2B79EE] mb-1">{{ getValue(data.A22160009, '0') }}</div>
<div class="text-xs text-gray-600">近3个月查询笔数</div>
<div class="bg-blue-50 rounded-lg px-3 py-6 text-center border border-[#2B79EE8F]">
<div class="text-3xl font-bold text-[#2B79EE] mb-1">{{ getValue(data.A22160009, '0') }}</div>
<div class="text-sm text-gray-600">近3个月查询笔数</div>
</div>
<div class="bg-blue-50 rounded-lg p-3 text-center border border-[#2B79EE8F]">
<div class="text-xl font-bold text-[#2B79EE] mb-1">{{ getValue(data.A22160010, '0') }}</div>
<div class="text-xs text-gray-600">近6个月查询笔数</div>
<div class="bg-blue-50 rounded-lg px-3 py-6 text-center border border-[#2B79EE8F]">
<div class="text-3xl font-bold text-[#2B79EE] mb-1">{{ getValue(data.A22160010, '0') }}</div>
<div class="text-sm text-gray-600">近6个月查询笔数</div>
</div>
</div>
@@ -197,7 +197,7 @@ const applyScoreGaugeOption = computed(() => {
axisLabel: {
show: true,
distance: -18,
fontSize: 10,
fontSize: 12,
color: '#666',
formatter: function(value) {
if (value === 0) return '0'
@@ -318,7 +318,7 @@ const applyConfidenceGaugeOption = computed(() => {
axisLabel: {
show: true,
distance: -15,
fontSize: 11,
fontSize: 12,
color: '#666',
formatter: function(value) {
// 将百分比映射回置信度值
@@ -413,7 +413,7 @@ const queryTrendChartOption = computed(() => {
yAxis: {
type: 'value',
axisLabel: {
fontSize: 11,
fontSize: 12,
color: '#6b7280',
formatter: '{value} 次'
},
@@ -441,7 +441,7 @@ const queryTrendChartOption = computed(() => {
label: {
show: true,
position: 'top',
fontSize: 11,
fontSize: 12,
color: '#333'
}
}

View File

@@ -237,17 +237,17 @@
<div class="text-sm font-medium text-gray-700 mb-3">M0+逾期</div>
<div class="space-y-2">
<div class="flex justify-between">
<span class="text-xs text-gray-600">近6个月</span>
<span class="text-sm text-gray-600">近6个月</span>
<span class="text-sm font-bold text-[#EB3C3C]">{{ getValue(data.B22170025, '0') }}
</span>
</div>
<div class="flex justify-between">
<span class="text-xs text-gray-600">近12个月</span>
<span class="text-sm text-gray-600">近12个月</span>
<span class="text-sm font-bold text-[#EB3C3C]">{{ getValue(data.B22170026, '0') }}
</span>
</div>
<div class="flex justify-between">
<span class="text-xs text-gray-600">近24个月</span>
<span class="text-sm text-gray-600">近24个月</span>
<span class="text-sm font-bold text-[#EB3C3C]">{{ getValue(data.B22170027, '0') }}
</span>
</div>
@@ -259,17 +259,17 @@
<div class="text-sm font-medium text-gray-700 mb-3">M1+逾期</div>
<div class="space-y-2">
<div class="flex justify-between">
<span class="text-xs text-gray-600">近6个月</span>
<span class="text-sm text-gray-600">近6个月</span>
<span class="text-sm font-bold text-[#EB3C3C]">{{ getValue(data.B22170028, '0') }}
</span>
</div>
<div class="flex justify-between">
<span class="text-xs text-gray-600">近12个月</span>
<span class="text-sm text-gray-600">近12个月</span>
<span class="text-sm font-bold text-[#EB3C3C]">{{ getValue(data.B22170029, '0') }}
</span>
</div>
<div class="flex justify-between">
<span class="text-xs text-gray-600">近24个月</span>
<span class="text-sm text-gray-600">近24个月</span>
<span class="text-sm font-bold text-[#EB3C3C]">{{ getValue(data.B22170030, '0') }}
</span>
</div>
@@ -516,7 +516,7 @@ const behaviorScoreGaugeOption = computed(() => {
axisLabel: {
show: true,
distance: -18,
fontSize: 10,
fontSize: 12,
color: '#666',
formatter: function (value) {
if (value === 0) return '0'
@@ -637,7 +637,7 @@ const behaviorConfidenceGaugeOption = computed(() => {
axisLabel: {
show: true,
distance: -15,
fontSize: 11,
fontSize: 12,
color: '#666',
formatter: function (value) {
// 将百分比映射回置信度值
@@ -722,7 +722,7 @@ const loanTrendChartOption = computed(() => {
type: 'category',
data: periods,
axisLabel: {
fontSize: 11,
fontSize: 12,
color: '#6b7280',
rotate: 15
},
@@ -735,7 +735,7 @@ const loanTrendChartOption = computed(() => {
yAxis: {
type: 'value',
axisLabel: {
fontSize: 11,
fontSize: 12,
color: '#6b7280',
formatter: '{value} 笔'
},
@@ -763,7 +763,7 @@ const loanTrendChartOption = computed(() => {
label: {
show: true,
position: 'top',
fontSize: 11,
fontSize: 12,
color: '#333'
}
}
@@ -829,7 +829,7 @@ const overdueComparisonChartOption = computed(() => {
yAxis: {
type: 'value',
axisLabel: {
fontSize: 11,
fontSize: 12,
color: '#6b7280',
formatter: '{value} 笔'
},
@@ -857,7 +857,7 @@ const overdueComparisonChartOption = computed(() => {
label: {
show: true,
position: 'top',
fontSize: 11,
fontSize: 12,
color: '#333'
}
},
@@ -878,7 +878,7 @@ const overdueComparisonChartOption = computed(() => {
label: {
show: true,
position: 'top',
fontSize: 11,
fontSize: 12,
color: '#333'
}
}
@@ -921,7 +921,7 @@ const amountDistributionChartOption = computed(() => {
type: 'category',
data: categories,
axisLabel: {
fontSize: 11,
fontSize: 12,
color: '#6b7280'
},
axisLine: {
@@ -933,7 +933,7 @@ const amountDistributionChartOption = computed(() => {
yAxis: {
type: 'value',
axisLabel: {
fontSize: 11,
fontSize: 12,
color: '#6b7280',
formatter: '{value} 笔'
},
@@ -961,7 +961,7 @@ const amountDistributionChartOption = computed(() => {
label: {
show: true,
position: 'top',
fontSize: 11,
fontSize: 12,
color: '#333'
}
}

View File

@@ -0,0 +1,456 @@
<template>
<div class="big-data-report-section bg-white rounded-lg border border-gray-200">
<div class="flex items-center p-4">
<div class="w-8 h-8 flex items-center justify-center mr-2">
<img src="@/assets/images/report/gl.png" alt="大数据详情" class="w-8 h-8 object-contain" />
</div>
<span class="font-bold text-gray-800">大数据详情</span>
</div>
<div class="p-4">
<!-- 网络贷款类信用 -->
<LTitle title="网络贷款类信用" />
<div class="mt-3 mb-6">
<div class="space-y-4 mb-4">
<!-- 网贷授信额度 -->
<div class="rounded-lg border border-[#2B79EE8F] bg-[#2B79EE1A] p-4 text-center">
<div class="text-xl font-bold text-[#2B79EE] mb-1">
{{ formatCreditAmount(data.C22180001) }}
</div>
<div class="text-sm font-medium text-gray-700 mb-1">网贷授信额度</div>
</div>
<!-- 网贷额度置信度 -->
<div class="rounded-lg border border-[#2B79EE8F] bg-[#2B79EE1A] p-4">
<div class="text-sm font-medium text-gray-700 mb-2 text-center">网贷额度置信度</div>
<div class="h-40">
<v-chart class="chart-container" :option="p2pConfidenceGaugeOption" autoresize />
</div>
</div>
</div>
<div class="bg-gray-50 rounded-lg p-4 space-y-3">
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600">网络贷款类机构数</span>
<span class="text-lg font-bold text-gray-800">{{ getValue(data.C22180003, '0') }} </span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600">网络贷款类产品数</span>
<span class="text-lg font-bold text-gray-800">{{ getValue(data.C22180004, '0') }} </span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600">网络贷款机构最大授信额度</span>
<span class="text-lg font-bold text-gray-800">{{ formatCreditAmount(data.C22180005) }}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600">网络贷款机构平均授信额度</span>
<span class="text-lg font-bold text-gray-800">{{ formatCreditAmount(data.C22180006) }}</span>
</div>
</div>
<!-- 授信额度对比图 -->
<div class="mb-6">
<LTitle title="授信额度对比" />
<div class="h-64 mt-3">
<v-chart class="chart-container" :option="creditAmountChartOption" autoresize />
</div>
</div>
</div>
<!-- 消金贷款类信用 -->
<LTitle title="消金贷款类信用" />
<div class="mt-3 mb-6">
<div class="space-y-4 mb-4">
<!-- 消金建议授信额度 -->
<div class="rounded-lg border border-[#1FBE5D8F] bg-[#1FBE5D1A] p-4 text-center">
<div class="text-xl font-bold text-[#1FBE5D] mb-1">
{{ formatCreditAmount(data.C22180011) }}
</div>
<div class="text-sm font-medium text-gray-700 mb-1">消金建议授信额度</div>
</div>
<!-- 消金额度置信度 -->
<div class="rounded-lg border border-[#1FBE5D8F] bg-[#1FBE5D1A] p-4">
<div class="text-sm font-medium text-gray-700 mb-2 text-center">消金额度置信度</div>
<div class="h-40">
<v-chart class="chart-container" :option="consumerConfidenceGaugeOption" autoresize />
</div>
</div>
</div>
<div class="bg-gray-50 rounded-lg p-4 space-y-3">
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600">消金贷款类机构数</span>
<span class="text-lg font-bold text-gray-800">{{ getValue(data.C22180007, '0') }} </span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600">消金贷款类产品数</span>
<span class="text-lg font-bold text-gray-800">{{ getValue(data.C22180008, '0') }} </span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600">消金贷款类机构最大授信额度</span>
<span class="text-lg font-bold text-gray-800">{{ formatCreditAmount(data.C22180009) }}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600">消金贷款类机构平均授信额度</span>
<span class="text-lg font-bold text-gray-800">{{ formatCreditAmount(data.C22180010) }}</span>
</div>
</div>
</div>
</div>
<!-- 温馨提示 -->
<Remark
content="大数据详情展示申请人在网络贷款和消金贷款领域的授信情况,包括授信额度、机构数、产品数等指标。授信额度反映了金融机构对申请人信用状况的评估,额度越高通常表示信用状况越好。建议关注授信额度置信度,置信度越高表示评估结果越可靠。同时需要结合申请人的实际借贷行为和还款记录进行综合评估。" />
</div>
</template>
<script setup>
import { computed } from 'vue'
import LTitle from '@/components/LTitle.vue'
import Remark from '@/components/Remark.vue'
import VChart from 'vue-echarts'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { BarChart, GaugeChart } from 'echarts/charts'
import {
TitleComponent,
TooltipComponent,
LegendComponent,
GridComponent
} from 'echarts/components'
import { formatCreditAmount, formatConfidence, getValue } from '../utils/formatUtils'
// 注册ECharts组件
use([
CanvasRenderer,
BarChart,
GaugeChart,
TitleComponent,
TooltipComponent,
LegendComponent,
GridComponent
])
const props = defineProps({
data: {
type: Object,
default: () => ({})
}
})
// 网贷额度置信度仪表盘配置50-100
const p2pConfidenceGaugeOption = computed(() => {
const confidence = parseInt(getValue(props.data.C22180002, '0')) || 0
const percentage = confidence >= 50 ? ((confidence - 50) / 50) * 100 : 0
// 根据置信度确定颜色
let color = '#2B79EE' // 蓝色 - 高置信度
if (confidence < 60) {
color = '#faad14' // 黄色 - 中等置信度
} else if (confidence < 70) {
color = '#fa8c16' // 橙色 - 较低置信度
}
return {
series: [
{
type: 'gauge',
startAngle: 180,
endAngle: 0,
min: 0,
max: 100,
radius: '90%',
center: ['50%', '75%'],
itemStyle: {
color: color,
shadowBlur: 6,
shadowColor: color,
},
progress: {
show: true,
width: 18,
roundCap: true,
},
axisLine: {
roundCap: true,
lineStyle: {
width: 18,
color: [
[percentage / 100, color],
[1, '#e5e7eb']
]
}
},
axisTick: {
show: false
},
splitLine: {
show: false
},
axisLabel: {
show: false
},
pointer: {
show: false
},
detail: {
valueAnimation: true,
fontSize: 24,
fontWeight: 'bold',
color: color,
offsetCenter: [0, '-5%'],
formatter: '{value}%',
rich: {
value: {
fontSize: 24,
fontWeight: 'bold',
color: color,
lineHeight: 28
}
}
},
data: [
{
value: percentage
}
],
title: {
fontSize: 14,
color: '#666',
offsetCenter: [0, '25%'],
formatter: () => `${confidence}%`
}
}
]
}
})
// 消金额度置信度仪表盘配置50-100
const consumerConfidenceGaugeOption = computed(() => {
const confidence = parseInt(getValue(props.data.C22180012, '0')) || 0
const percentage = confidence >= 50 ? ((confidence - 50) / 50) * 100 : 0
// 根据置信度确定颜色
let color = '#1FBE5D' // 绿色 - 高置信度
if (confidence < 60) {
color = '#faad14' // 黄色 - 中等置信度
} else if (confidence < 70) {
color = '#fa8c16' // 橙色 - 较低置信度
}
return {
series: [
{
type: 'gauge',
startAngle: 180,
endAngle: 0,
min: 0,
max: 100,
radius: '90%',
center: ['50%', '75%'],
itemStyle: {
color: color,
shadowBlur: 6,
shadowColor: color,
},
progress: {
show: true,
width: 18,
roundCap: true,
},
axisLine: {
roundCap: true,
lineStyle: {
width: 18,
color: [
[percentage / 100, color],
[1, '#e5e7eb']
]
}
},
axisTick: {
show: false
},
splitLine: {
show: false
},
axisLabel: {
show: false
},
pointer: {
show: false
},
detail: {
valueAnimation: true,
fontSize: 24,
fontWeight: 'bold',
color: color,
offsetCenter: [0, '-5%'],
formatter: '{value}%',
rich: {
value: {
fontSize: 24,
fontWeight: 'bold',
color: color,
lineHeight: 28
}
}
},
data: [
{
value: percentage
}
],
title: {
fontSize: 14,
color: '#666',
offsetCenter: [0, '25%'],
formatter: () => `${confidence}%`
}
}
]
}
})
// 授信额度对比图配置
const creditAmountChartOption = computed(() => {
const categories = ['网贷', '消金']
const maxData = [
parseInt(getValue(props.data.C22180005, '0')) || 0,
parseInt(getValue(props.data.C22180009, '0')) || 0
]
const avgData = [
parseInt(getValue(props.data.C22180006, '0')) || 0,
parseInt(getValue(props.data.C22180010, '0')) || 0
]
return {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
formatter: function (params) {
let result = params[0].name + '<br/>'
params.forEach(item => {
const value = item.value >= 10000
? `${(item.value / 10000).toFixed(2)}万元`
: `${item.value}`
result += `${item.seriesName}: ${value}<br/>`
})
return result
}
},
legend: {
data: ['最大授信额度', '平均授信额度'],
top: '5%',
textStyle: {
fontSize: 12
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
top: '20%',
containLabel: true
},
xAxis: {
type: 'category',
data: categories,
axisLabel: {
fontSize: 12,
color: '#6b7280'
},
axisLine: {
lineStyle: {
color: '#e5e7eb'
}
}
},
yAxis: {
type: 'value',
axisLabel: {
fontSize: 12,
color: '#6b7280',
formatter: function (value) {
if (value >= 10000) {
return `${(value / 10000).toFixed(1)}`
}
return value
}
},
splitLine: {
lineStyle: {
color: '#f3f4f6'
}
}
},
series: [
{
name: '最大授信额度',
type: 'bar',
data: maxData,
barWidth: '30%',
itemStyle: {
color: '#2B79EE',
borderRadius: [4, 4, 0, 0]
},
emphasis: {
itemStyle: {
color: '#1e5bb8'
}
},
label: {
show: true,
position: 'top',
fontSize: 12,
color: '#333',
formatter: function (params) {
const value = params.value
return value >= 10000
? `${(value / 10000).toFixed(1)}`
: value
}
}
},
{
name: '平均授信额度',
type: 'bar',
data: avgData,
barWidth: '30%',
itemStyle: {
color: '#1FBE5D',
borderRadius: [4, 4, 0, 0]
},
emphasis: {
itemStyle: {
color: '#179e4d'
}
},
label: {
show: true,
position: 'top',
fontSize: 12,
color: '#333',
formatter: function (params) {
const value = params.value
return value >= 10000
? `${(value / 10000).toFixed(1)}`
: value
}
}
}
]
}
})
</script>
<style scoped>
.chart-container {
width: 100%;
height: 100%;
}
</style>

View File

@@ -6,8 +6,8 @@
<!-- 放款还款详情 -->
<BehaviorReportSection :data="behaviorData" />
<!-- 信用详情 -->
<CurrentReportSection :data="currentData" />
<!-- 大数据详情 -->
<BigDataReportSection :data="bigDataData" />
</div>
</template>
@@ -16,7 +16,7 @@ import { computed } from 'vue'
import { useRiskNotifier } from '@/composables/useRiskNotifier'
import ApplyReportSection from './components/ApplyReportSection.vue'
import BehaviorReportSection from './components/BehaviorReportSection.vue'
import CurrentReportSection from './components/CurrentReportSection.vue'
import BigDataReportSection from './components/BigDataReportSection.vue'
const props = defineProps({
data: {
@@ -47,17 +47,17 @@ const applyData = computed(() => rawData.value.apply_report_detail || {})
// 放款还款详情
const behaviorData = computed(() => rawData.value.behavior_report_detail || {})
// 信用详情
const currentData = computed(() => rawData.value.current_report_detail || {})
// 大数据详情
const bigDataData = computed(() => rawData.value.current_report_detail || {})
// 计算风险评分0-100分分数越高越安全
const riskScore = computed(() => {
const apply = applyData.value
const behavior = behaviorData.value
const current = currentData.value
const bigData = bigDataData.value
// 检查是否有数据
if (!apply || !behavior || !current || Object.keys(apply).length === 0) {
if (!apply || !behavior || !bigData || Object.keys(apply).length === 0) {
return 100 // 无数据视为最安全
}

View File

@@ -0,0 +1,206 @@
/**
* 全景雷达(JRZQ7F1A) 数据拆分工具
* 将原始的全景雷达报告拆分为申请行为详情、放款还款详情和大数据详情三个独立模块
* 以便在 BaseReport 中按 Tab 展示
*/
/**
* 判断数据对象是否有效(非空且包含至少一个键)
* @param {object} data
* @returns {boolean}
*/
const EMPTY_STRING_MARKERS = new Set(['-', '--', '0', '0.0', '暂无数据', '无数据', 'null', 'NULL'])
function hasData(data, visited = new WeakSet()) {
if (data === null || data === undefined) {
return false
}
if (typeof data === 'number') {
return data !== 0 && !Number.isNaN(data)
}
if (typeof data === 'string') {
const trimmed = data.trim()
if (!trimmed) return false
return !EMPTY_STRING_MARKERS.has(trimmed)
}
if (typeof data === 'boolean') {
return data
}
if (Array.isArray(data)) {
if (data.length === 0) return false
return data.some(item => hasData(item, visited))
}
if (typeof data === 'object') {
if (visited.has(data)) return false
visited.add(data)
const values = Object.values(data)
if (values.length === 0) return false
return values.some(value => hasData(value, visited))
}
return false
}
const createDefaultObject = (fields, defaultValue = '0') =>
fields.reduce((acc, key) => {
acc[key] = defaultValue
return acc
}, {})
const APPLY_REPORT_FIELDS = [
'A22160001',
'A22160002',
'A22160003',
'A22160004',
'A22160005',
'A22160006',
'A22160007',
'A22160008',
'A22160009',
'A22160010'
]
const APPLY_REPORT_DEFAULTS = createDefaultObject(APPLY_REPORT_FIELDS)
const BEHAVIOR_REPORT_FIELDS = [
'B22170001',
'B22170002',
'B22170003',
'B22170004',
'B22170005',
'B22170006',
'B22170007',
'B22170008',
'B22170009',
'B22170010',
'B22170011',
'B22170012',
'B22170013',
'B22170014',
'B22170015',
'B22170016',
'B22170017',
'B22170018',
'B22170019',
'B22170020',
'B22170021',
'B22170022',
'B22170023',
'B22170024',
'B22170025',
'B22170026',
'B22170027',
'B22170028',
'B22170029',
'B22170030',
'B22170031',
'B22170032',
'B22170033',
'B22170034',
'B22170035',
'B22170036',
'B22170037',
'B22170038',
'B22170039',
'B22170040',
'B22170041',
'B22170042',
'B22170043',
'B22170044',
'B22170045',
'B22170046',
'B22170047',
'B22170048',
'B22170049',
'B22170050',
'B22170051',
'B22170052',
'B22170053',
'B22170054'
]
const BEHAVIOR_REPORT_DEFAULTS = createDefaultObject(BEHAVIOR_REPORT_FIELDS)
const BIG_DATA_REPORT_FIELDS = [
'C22180001',
'C22180002',
'C22180003',
'C22180004',
'C22180005',
'C22180006',
'C22180007',
'C22180008',
'C22180009',
'C22180010',
'C22180011',
'C22180012'
]
const BIG_DATA_REPORT_DEFAULTS = createDefaultObject(BIG_DATA_REPORT_FIELDS)
const normalizeModuleData = (source, defaults) => {
const normalized = { ...defaults }
if (!source || typeof source !== 'object') {
return normalized
}
Object.entries(source).forEach(([key, value]) => {
if (value === undefined || value === null) {
return
}
normalized[key] = value
})
return normalized
}
/**
* 拆分 JRZQ7F1A 报告数据
* @param {Array} reportData - 原始报告数据数组
* @returns {Array} 拆分后的报告数据数组
*/
export function splitJRZQ7F1AForTabs(reportData) {
const targetIndex = reportData.findIndex(item => item.data?.apiID === 'JRZQ7F1A');
const target = targetIndex >= 0 ? reportData[targetIndex] : null;
if (!target || !target.data?.data) {
return reportData;
}
const originalData = target.data.data;
const baseTimestamp = target.data.timestamp;
const success = target.data.success ?? true;
const splitModules = [];
const pushModule = (suffix, payload, defaults) => {
splitModules.push({
data: {
apiID: `JRZQ7F1A_${suffix}`,
data: normalizeModuleData(payload, defaults),
success,
timestamp: baseTimestamp,
}
});
};
pushModule('ApplyReport', originalData.apply_report_detail, APPLY_REPORT_DEFAULTS);
pushModule('BehaviorReport', originalData.behavior_report_detail, BEHAVIOR_REPORT_DEFAULTS);
pushModule('BigDataReport', originalData.current_report_detail, BIG_DATA_REPORT_DEFAULTS);
// 未能拆出子模块则直接返回原数据
if (splitModules.length === 0) {
return reportData;
}
const result = [...reportData];
result.splice(targetIndex, 1, ...splitModules);
return result;
}

View File

@@ -24,7 +24,7 @@
<div v-for="rule in hitRules" :key="rule.ruleId"
class="bg-white rounded-xl p-4 border border-gray-200 relative">
<!-- <div class="absolute top-0 right-0">
<div class="px-2 py-1 text-xs text-white rounded-bl-xl rounded-tr-xl bg-orange-500">
<div class="px-2 py-1 text-sm text-white rounded-bl-xl rounded-tr-xl bg-orange-500">
权重: {{ rule.weight }}
</div>
</div> -->
@@ -49,7 +49,7 @@
medium: '严重逾期',
high: '无法收回',
}" :key="key"
class="px-2 py-3 text-center cursor-pointer transition-all duration-300 font-medium text-xs sm:text-sm relative border-b-2"
class="px-2 py-3 text-center cursor-pointer transition-all duration-300 font-medium text-sm sm:text-sm relative border-b-2"
:class="[
key === 'summary'
? activeTab === key
@@ -73,7 +73,7 @@
summaryData.byRiskLevel &&
summaryData.byRiskLevel.find(level => level.id === key && level.triggered > 0)
" :class="[
'absolute -top-1 -right-1 inline-flex items-center justify-center w-4 h-4 text-xs font-bold leading-none text-white rounded-full',
'absolute -top-1 -right-1 inline-flex items-center justify-center w-4 h-4 text-sm font-bold leading-none text-white rounded-full',
key === 'low' ? 'bg-blue-500' : key === 'medium' ? 'bg-orange-500' : 'bg-red-500',
]">
{{summaryData.byRiskLevel.find(level => level.id === key).triggered}}
@@ -97,7 +97,7 @@
]" @click="handleRiskLevelClick(levelSummary.id)">
<div class="absolute top-0 right-0">
<div :class="[
'px-2 py-1 text-xs text-white rounded-bl-xl rounded-tr-xl',
'px-2 py-1 text-sm text-white rounded-bl-xl rounded-tr-xl',
levelSummary.triggered > 0
? levelSummary.id === 'low'
? 'bg-blue-500'
@@ -125,7 +125,7 @@
</div>
<div class="mt-3 flex items-end justify-between">
<div>
<p class="text-xs text-gray-600">命中项</p>
<p class="text-sm text-gray-600">命中项</p>
<p class="text-xl font-bold" :class="levelSummary.triggered > 0
? levelSummary.id === 'low'
? 'text-blue-600'
@@ -138,7 +138,7 @@
</p>
</div>
<button
class="text-xs px-3 py-1.5 rounded-full focus:outline-none transition-all duration-300"
class="text-sm px-3 py-1.5 rounded-full focus:outline-none transition-all duration-300"
:class="levelSummary.id === 'low'
? 'bg-blue-100 text-blue-600 hover:bg-blue-200'
: levelSummary.id === 'medium'
@@ -162,8 +162,8 @@
{{ typeSummary.label }}
</div>
<div class="flex items-center space-x-1">
<span class="text-xs text-gray-500">命中项</span>
<span class="text-xs font-medium px-1.5 py-0.5 rounded-full" :class="[
<span class="text-sm text-gray-500">命中项</span>
<span class="text-sm font-medium px-1.5 py-0.5 rounded-full" :class="[
typeSummary.triggered > 0
? getRateColor(typeSummary.triggered, typeSummary.total) === 'red'
? 'bg-red-100 text-red-700'
@@ -174,8 +174,8 @@
]">
{{ typeSummary.triggered }}
</span>
<span class="text-xs text-gray-500">/</span>
<span class="text-xs text-gray-500">{{ typeSummary.total }}</span>
<span class="text-sm text-gray-500">/</span>
<span class="text-sm text-gray-500">{{ typeSummary.total }}</span>
</div>
</div>
<div class="w-full h-2 bg-gray-100 rounded-full mt-2 overflow-hidden">
@@ -225,10 +225,10 @@
</span>
</div>
<div class="flex flex-col">
<h4 class="text-xs font-medium text-gray-900 truncate max-w-[100px]">
<h4 class="text-sm font-medium text-gray-900 truncate max-w-[100px]">
{{ institutionSummary.label }}
</h4>
<div class="flex items-center mt-1 text-xs text-gray-500">
<div class="flex items-center mt-1 text-sm text-gray-500">
<span>命中项:{{ institutionSummary.triggered }}/{{ institutionSummary.total
}}</span>
</div>
@@ -259,7 +259,7 @@
">
{{ tabConfigs[activeTab].title }}
</h3>
<p class="text-xs text-gray-600 mt-1">
<p class="text-sm text-gray-600 mt-1">
{{ tabConfigs[activeTab].description }}
</p>
</div>
@@ -283,7 +283,7 @@
]">
<div class="absolute top-0 right-0">
<div :class="[
'px-2 py-1 text-xs text-white rounded-bl-xl rounded-tr-xl',
'px-2 py-1 text-sm text-white rounded-bl-xl rounded-tr-xl',
item.isTriggered
? item.levelType === 'low'
? 'bg-blue-500'
@@ -306,7 +306,7 @@
</div>
</div>
</div>
<div class="grid grid-cols-2 gap-2 text-xs mt-3">
<div class="grid grid-cols-2 gap-2 text-sm mt-3">
<div>
<span class="text-gray-500">发生次数:</span>
<span
@@ -349,7 +349,7 @@
]">
<div class="absolute top-0 right-0">
<div :class="[
'px-2 py-1 text-xs text-white rounded-bl-xl rounded-tr-xl',
'px-2 py-1 text-sm text-white rounded-bl-xl rounded-tr-xl',
item.isTriggered
? item.levelType === 'low'
? 'bg-blue-500'
@@ -372,7 +372,7 @@
</div>
</div>
</div>
<div class="grid grid-cols-2 gap-2 text-xs mt-3">
<div class="grid grid-cols-2 gap-2 text-sm mt-3">
<div>
<span class="text-gray-500">发生次数:</span>
<span

View File

@@ -24,7 +24,7 @@
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600">是否转网</span>
<div class="flex items-center">
<span class="px-2 py-1 text-xs text-white rounded-md mr-2"
<span class="px-2 py-1 text-sm text-white rounded-md mr-2"
:class="getPortabilityTagClass(item.result)">
{{ getPortabilityText(item.result) }}
</span>

View File

@@ -108,7 +108,7 @@
<div class="text-sm font-medium" :class="specialStatusTextClass">
{{ specialStatusText }}
</div>
<div class="text-xs mt-1" :class="specialStatusDescClass">
<div class="text-sm mt-1" :class="specialStatusDescClass">
{{ specialStatusDesc }}
</div>
</div>