Compare commits

...

17 Commits

Author SHA1 Message Date
064bf14a5a f 2026-06-15 14:56:45 +08:00
Mrx
7b8bdbb323 f 2026-06-12 14:34:40 +08:00
Mrx
866d811035 f 2026-06-12 14:31:41 +08:00
Mrx
bee67272bb f 2026-06-12 14:25:45 +08:00
Mrx
f069d93d84 f 2026-06-10 21:03:31 +08:00
Mrx
907d277302 f 2026-06-10 12:55:01 +08:00
Mrx
d682a13af9 add 2026-06-10 12:22:43 +08:00
Mrx
70cfe458be ff 2026-05-15 10:45:00 +08:00
Mrx
79a47c6957 f 2026-05-13 11:04:32 +08:00
Mrx
53284b6979 f 2026-05-13 11:01:42 +08:00
Mrx
5c4921b34e f 2026-04-19 16:30:06 +08:00
Mrx
7b472db9d8 注释 2026-03-21 10:58:37 +08:00
Mrx
d617763d2c f 2026-03-20 14:47:54 +08:00
Mrx
b1b301306e f 2026-02-24 11:10:28 +08:00
Mrx
ce06f3a958 add 0s0d 2026-02-13 17:29:12 +08:00
Mrx
6c7169c206 f 2026-01-22 12:18:10 +08:00
Mrx
84ed26f1e0 f 2026-01-22 12:17:57 +08:00
128 changed files with 56158 additions and 3504 deletions

View File

@@ -95,7 +95,13 @@
<script type="module" src="/src/main.js"></script>
<script>
console.log('[index.html] 页面脚本开始执行');
window.onerror = function(msg, url, lineNo, columnNo, error) {
console.error('[index.html] 全局错误捕捉:', msg, 'at', url, 'line:', lineNo);
return false;
};
document.addEventListener('DOMContentLoaded', () => {
console.log('[index.html] DOMContentLoaded 已触发');
const loadingElement = document.getElementById('app-loading');
if (loadingElement) {
loadingElement.style.opacity = '0';

1648
public/DWBG9FB2.json Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1648
public/DWBG9FB2hcl.json Normal file

File diff suppressed because one or more lines are too long

1184
public/DWBG9FB2hzy.json Normal file

File diff suppressed because it is too large Load Diff

1253
public/DWBG9FB3.json Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1253
public/DWBG9FB3hcl.json Normal file

File diff suppressed because one or more lines are too long

1029
public/DWBG9FB3hzy.json Normal file

File diff suppressed because it is too large Load Diff

331
public/desensitize.js Normal file
View File

@@ -0,0 +1,331 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const INPUT_FILES = ['DWBG9FB2.json', 'DWBG9FB3.json'];
const ORG_SUFFIXES = [
'股份有限公司城区支行',
'农村商业银行股份有限公司',
'物业服务有限公司南宁分公司',
'物业服务有限公司',
'房地产开发有限公司',
'国际大酒店有限公司',
'生态旅游家园开发有限公司',
'农业发展有限公司',
'农资有限公司',
'贸易有限公司',
'发展有限公司',
'开发有限公司',
'股份有限公司',
'农村合作银行',
'信用合作联社',
'有限公司',
'分公司',
];
const COMPANY_KEYWORDS = /银行|公司|联社|集团|酒店|有限|股份/;
const NAME_BLACKLIST = new Set([
'当事人', '法律关系', '法律关', '案涉房', '案涉', '房屋', '所有权',
'合同法律', '侵权法律', '物业服务', '诉讼请求', '正当理由',
'民发物业', '民发实', '民发', '广西广为', '广西中硕','广西',
]);
// ── 基础脱敏函数 ──────────────────────────────────────────
function desensitizePersonName(name) {
if (!name || typeof name !== 'string') return name;
if (COMPANY_KEYWORDS.test(name)) return desensitizeCompany(name);
if (name.length <= 1) return name;
return name[0] + '某某';
}
function desensitizeCompany(name) {
if (!name || typeof name !== 'string') return name;
for (const suffix of ORG_SUFFIXES) {
if (!name.endsWith(suffix)) continue;
const core = name.slice(0, -suffix.length);
if (core.startsWith('广西')) return '广西****' + suffix;
if (core.startsWith('湖北')) return '湖北****' + suffix;
if (core.startsWith('桂林市')) return '桂林市****' + suffix;
if (core.startsWith('兴安县')) return '兴安县****' + suffix;
if (core.startsWith('资源县')) return '资源县****' + suffix;
if (/^.+县/.test(core)) return '**县****' + suffix;
if (/^.+市/.test(core)) return '**市****' + suffix;
return core.slice(0, 2) + '****' + suffix;
}
return name.slice(0, 2) + '****';
}
function desensitizeCourt(court) {
if (!court || typeof court !== 'string') return court;
return court
.replace(/^(.+?市)(.+?区)/, '**市**区')
.replace(/^(.+?市)(.+?县)/, '**市**县')
.replace(/^(.+?县)/, '**县')
.replace(/^(.+?市)/, '**市');
}
function desensitizeProvince(text) {
if (!text || typeof text !== 'string') return text;
return text
.replace(/湖北省/g, '**省')
.replace(/辽宁省/g, '**省');
}
function desensitizeAreaStat(text) {
if (!text || typeof text !== 'string') return text;
return desensitizeProvince(text).replace(/辽宁省\(/g, '**省(');
}
function desensitizeIdCard(idCard) {
if (!idCard || typeof idCard !== 'string') return idCard;
if (idCard.length === 18) {
return idCard.substring(0, 6) + '********' + idCard.substring(14);
}
if (/^[0-9A-Z]{15,18}$/.test(idCard)) {
return idCard.substring(0, 4) + '**********' + idCard.substring(idCard.length - 4);
}
return idCard;
}
function desensitizeMobile(mobile) {
if (!mobile || typeof mobile !== 'string' || mobile.length !== 11) return mobile;
return mobile.substring(0, 3) + '****' + mobile.substring(7);
}
function desensitizeAddressText(text) {
if (!text || typeof text !== 'string') return text;
let result = text;
result = result.replace(/统一社会信用代码[:]?\s*[0-9A-Z]{15,18}/g, (m) => {
const code = m.replace(/统一社会信用代码[:]?\s*/, '');
return '统一社会信用代码:' + desensitizeIdCard(code);
});
result = result.replace(/账号[:]?\s*[\d×]{10,}/g, '账号:********');
result = result.replace(/[\u4e00-\u9fa5]{2,6}路\d+号[^,,。;;]*/g, '**路**号****');
result = result.replace(/民发[·・][\u4e00-\u9fa5A-Za-z0-9]{2,15}?(?:小区|会所)/g, '****小区');
result = result.replace(/民发物业服务有限公司南宁分公司/g, '****物业服务有限公司南宁分公司');
result = result.replace(/民发物业服务有限公司/g, '****物业服务有限公司');
result = result.replace(/民发物业南宁分公司/g, '****物业南宁分公司');
result = result.replace(/民发物业公司/g, '****物业公司');
result = result.replace(/广西中硕资产评估有限责任公司/g, '广西****资产评估有限责任公司');
result = result.replace(/民发实业集团\(广西\)房地产开发有限公司/g, '****实业集团(广西)****开发有限公司');
result = result.replace(/[\u4e00-\u9fa5]{2,4}市[\u4e00-\u9fa5]{2,6}区人民法院/g, '**市**区人民法院');
result = result.replace(/上诉于([\u4e00-\u9fa5]{2,4})市中级人民法院/g, '上诉于**市中级人民法院');
result = result.replace(/开户名称[:]([\u4e00-\u9fa5]{2,4})市中级人民法院/g, '开户名称:**市中级人民法院');
result = result.replace(/([\u4e00-\u9fa5]{2,4})县人民法院/g, '**县人民法院');
result = result.replace(/住广西壮族自治区[^,,。;;]{2,20}?[区县]/g, '住广西壮族自治区**市**区');
result = result.replace(/住所地广西壮族自治区[^,,。;;]{2,30}/g, '住所地广西壮族自治区**市**区****');
result = result.replace(/住所地湖北省[^,,。;;]{2,30}/g, '住所地**省**市**区****');
result = result.replace(/位于[\u4e00-\u9fa5]{2,4}市[\u4e00-\u9fa5]{2,6}区/g, '位于**市**区');
result = result.replace(/坐落于[\u4e00-\u9fa5]{2,4}市[\u4e00-\u9fa5]{2,6}区/g, '坐落于**市**区');
result = result.replace(/系[\u4e00-\u9fa5]{2,5}市[\u4e00-\u9fa5]{2,6}区/g, '系**市**区');
result = result.replace(/[\u4e00-\u9fa5·・A-Za-z0-9]+栋\d+单元\d+号/g, '****栋**单元**号');
return result;
}
// ── 从数据中收集替换映射 ──────────────────────────────────
function isValidPersonName(name) {
return (
name &&
name.length >= 2 &&
name.length <= 4 &&
!COMPANY_KEYWORDS.test(name) &&
!NAME_BLACKLIST.has(name) &&
/^[\u4e00-\u9fa5]+$/.test(name)
);
}
function collectMappings(data) {
const personNames = new Set();
const companyNames = new Set();
const courtNames = new Set();
function walk(obj) {
if (!obj || typeof obj !== 'object') return;
if (Array.isArray(obj)) {
obj.forEach(walk);
return;
}
if (obj.c_mc) {
if (obj.n_dsrlx === '企业组织' || COMPANY_KEYWORDS.test(obj.c_mc)) {
companyNames.add(obj.c_mc);
} else if (isValidPersonName(obj.c_mc)) {
personNames.add(obj.c_mc);
}
}
if (obj.n_jbfy) courtNames.add(obj.n_jbfy);
for (const field of ['c_gkws_dsr', 'c_gkws_pjjg']) {
if (obj[field]) extractNamesFromLegalText(obj[field], personNames, companyNames);
}
Object.values(obj).forEach(walk);
}
walk(data);
return { personNames, companyNames, courtNames };
}
function extractNamesFromLegalText(text, personNames, companyNames) {
if (!text || typeof text !== 'string') return;
const rolePatterns = [
/(?:原告|被告|上诉人|被上诉人|原审被告人|被告人|负责人|法定代表人|案外人|委托诉讼代理人|代理人|承租人|出租人)[:]([\u4e00-\u9fa5]{2,4})/g,
/与案外人([\u4e00-\u9fa5]{2,4})签/g,
/([\u4e00-\u9fa5]{2,4})所有的/g,
/向([\u4e00-\u9fa5]{2,4})转账/g,
];
let match;
for (const rolePattern of rolePatterns) {
while ((match = rolePattern.exec(text)) !== null) {
if (isValidPersonName(match[1])) personNames.add(match[1]);
}
}
const companyPattern =
/([\u4e00-\u9fa5()·・]{4,40}?(?:有限公司|股份有限公司|合作银行|信用合作联社))/g;
while ((match = companyPattern.exec(text)) !== null) {
companyNames.add(match[1]);
}
}
function buildReplacementList(personNames, companyNames, courtNames) {
const replacements = [];
for (const name of personNames) {
replacements.push({ from: name, to: desensitizePersonName(name) });
}
for (const name of companyNames) {
replacements.push({ from: name, to: desensitizeCompany(name) });
}
for (const court of courtNames) {
replacements.push({ from: court, to: desensitizeCourt(court) });
}
replacements.sort((a, b) => b.from.length - a.from.length);
return replacements;
}
function desensitizeText(text, replacements) {
if (!text || typeof text !== 'string') return text;
let result = text;
for (const { from, to } of replacements) {
if (from && to && from !== to) {
result = result.split(from).join(to);
}
}
result = desensitizeAddressText(result);
result = desensitizeProvince(result);
return result;
}
// ── 递归脱敏 ──────────────────────────────────────────────
function desensitizeObject(obj, replacements) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (Array.isArray(obj)) {
return obj.map((item) => desensitizeObject(item, replacements));
}
const result = {};
for (const [key, value] of Object.entries(obj)) {
switch (key) {
case 'name':
result[key] = desensitizePersonName(value);
break;
case 'id_card':
result[key] = desensitizeIdCard(value);
break;
case 'mobile':
result[key] = desensitizeMobile(value);
break;
case 'c_mc':
result[key] =
obj.n_dsrlx === '企业组织' || COMPANY_KEYWORDS.test(value)
? desensitizeCompany(value)
: desensitizePersonName(value);
break;
case 'c_gkws_dsr':
case 'c_gkws_pjjg':
result[key] = desensitizeText(value, replacements);
break;
case 'n_jbfy':
result[key] = desensitizeCourt(value);
break;
case 'c_ssdy':
result[key] = value;
break;
case 'area_stat':
result[key] = desensitizeAreaStat(value);
break;
default:
result[key] = desensitizeObject(value, replacements);
break;
}
}
return result;
}
function processFile(filename) {
const inputFile = path.join(__dirname, filename);
const baseName = filename.replace('.json', '');
const outputFile = path.join(__dirname, `${baseName}_desensitized.json`);
const data = JSON.parse(fs.readFileSync(inputFile, 'utf8'));
const { personNames, companyNames, courtNames } = collectMappings(data);
const replacements = buildReplacementList(personNames, companyNames, courtNames);
const desensitizedData = desensitizeObject(data, replacements);
fs.writeFileSync(outputFile, JSON.stringify(desensitizedData, null, 2), 'utf8');
console.log(`\n${filename} 脱敏完成`);
console.log(` 原始文件:${inputFile}`);
console.log(` 输出文件:${outputFile}`);
console.log(` 姓名 ${personNames.size} 个,公司 ${companyNames.size} 个,法院 ${courtNames.size}`);
return { personNames, companyNames, courtNames };
}
// ── 执行 ──────────────────────────────────────────────────
console.log('开始脱敏处理...');
const summary = { person: 0, company: 0, court: 0 };
for (const file of INPUT_FILES) {
const stats = processFile(file);
summary.person += stats.personNames.size;
summary.company += stats.companyNames.size;
summary.court += stats.courtNames.size;
}
console.log('\n脱敏摘要');
console.log('- 姓名:保留姓氏,名字替换为「某某」');
console.log('- 公司/机构:保留地区前缀与组织类型,中间替换为「****」');
console.log('- 法院:市/县/区名称替换为「**」');
console.log('- 省份/地区:次要省份(湖北、辽宁等)脱敏,广西自治区保留');
console.log('- 判决书文本:地址、信用代码、路名等同步脱敏');
console.log('- 身份证号/手机号:按字段规则脱敏');
console.log(`- 合计处理:姓名 ${summary.person} 个,公司 ${summary.company} 个,法院 ${summary.court}`);

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 745 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 803 KiB

View File

@@ -87,24 +87,6 @@ onMounted(async () => {
await loadTrapezoidBackground();
});
// 处理数据拆分支持DWBG8B4D、DWBG6A2C、CJRZQ5E9F和CQYGL3F8E
const processedReportData = computed(() => {
let data = reportData.value;
// 拆分DWBG8B4D数据
data = splitDWBG8B4DForTabs(data);
// 拆分DWBG6A2C数据
data = splitDWBG6A2CForTabs(data);
// 拆分CQYGL3F8E数据
data = splitCQYGL3F8EForTabs(data);
// 过滤掉在featureMap中没有对应的项
return data.filter(item => featureMap[item.data.apiID]);
});
// 牌匾背景图片样式
const trapezoidBgStyle = computed(() => {
if (trapezoidBgImage.value) {
return {
@@ -170,6 +152,10 @@ const featureMap = {
name: "关联风险监督",
component: defineAsyncComponent(() => import("@/ui/DWBG6A2C/components/RiskSupervisionSection.vue")),
},
DWBG6A2C_CourtRiskInfo:{
name:"法院风险信息",
component: defineAsyncComponent(() => import("@/ui/DWBG6A2C/components/CourtRiskInfoSection.vue")),
},
// 司法涉诉
FLXG0V4B: {
@@ -179,6 +165,15 @@ const featureMap = {
),
},
// 法院被执行人高级版
FLXGK5D2: {
name: "法院被执行人高级版",
component: defineAsyncComponent(() =>
import("@/ui/FLXGK5D2/index.vue")
),
remark: '法院被执行人高级版展示申请人作为失信被执行人或限高被执行人的详细情况。数据来源于法院公开信息,包括案号、执行法院、立案时间及风险信号描述等。'
},
// 个人涉诉
FLXG7E8F: {
name: "个人涉诉",
@@ -363,6 +358,12 @@ const featureMap = {
remark: '手机在网时长查询用于检测用户手机号码的在网使用时长。在网时长越长,通常表示用户身份越稳定,信用风险越低。需要注意的是,如果手机号码存在携号转网的情况,那么在网时长会从转网的时候重新计算,转网前的在网时长不计入当前在网时长。建议结合手机携号转网查询结果进行综合评估。'
},
IVYZ0S0D:{
name: "劳动仲裁信息查询(个人版)",
component: defineAsyncComponent(() => import("@/ui/CIVYZ0S0D.vue")),
remark: '劳动仲裁信息查询(个人版)用于查询个人在劳动仲裁方面的信息,包括劳动仲裁案件数量、劳动仲裁案件类型、劳动仲裁案件结果等。',
},
// 谛听多维报告
DWBG8B4D: {
name: "谛听多维报告",
@@ -405,8 +406,204 @@ const featureMap = {
name: "规则风险提示",
component: defineAsyncComponent(() => import("@/ui/CDWBG8B4D/components/RiskWarningTab.vue")),
},
// DWBG3B4DCourtInfo:{
// name: "规则风险提示",
// component: defineAsyncComponent(() => import("@/ui/cDwBG8B4D/components/MultcourtInfosection.vue")),
// }
// 法院被执行人限高版
CFLX3A9B: {
name: "法院被执行人限高版",
component: defineAsyncComponent(() => import("@/ui/CFLX3A9B.vue")),
remark: '法院被执行人限高版用于查询个人在法院的被执行人信息,包括被执行人数量、被执行人类型、被执行人结果等。',
},
CIVYZ6M8P: {
name: "职业资格证书查询",
component: defineAsyncComponent(() => import("@/ui/CIVYZ6M8P.vue")),
remark: '职业资格证书查询展示查询到的职业资格考试名称、级别、专业及通过日期等信息。查询结果来源于相关职业技能鉴定机构,仅供参考。',
},
CIVYZ7F3A: {
name: "学历信息查询B",
component: defineAsyncComponent(() => import("@/ui/CIVYZ7F3A.vue")),
remark: '学历信息查询B展示学生毕业院校排名、专业、学历层次、学习形式、毕业时间、学校属性985/211/双一流)等多维度学历信息。查询结果来源于教育部门等权威机构,仅供参考。',
},
CIVYZ9K7F: {
name: "公安二要素认证即时版",
component: defineAsyncComponent(() => import("@/ui/CIVYZ9K7F.vue")),
remark: '公安二要素认证即时版用于核验姓名与身份证号是否一致,同时返回涉诈风险评估等级。查询结果来源于公安部门等权威机构,仅供参考。',
},
CIVYZA1B3: {
name: "公安三要素即时版",
component: defineAsyncComponent(() => import("@/ui/CIVYZA1B3.vue")),
remark: '公安三要素即时版通过比对人像与身份证信息判断是否为同一人,返回相似度分值及个人基本信息。查询结果来源于公安部门等权威机构,仅供参考。',
},
CJRZQ0B6Y: {
name: "银行卡黑名单(实时)",
component: defineAsyncComponent(() => import("@/ui/CJRZQ0B6Y.vue")),
remark: '银行卡黑名单(实时)查询银行卡是否命中不良持卡人、涉案卡片、交易欺诈卡片、线上/线下卡号黑名单等各类风险名单。查询结果仅供参考,具体信息以相关机构官方记录为准。',
},
CJRZQACAB: {
name: "银行卡四要素验证(详版)",
component: defineAsyncComponent(() => import("@/ui/CJRZQACAB.vue")),
remark: '银行卡四要素验证(详版)核验姓名、身份证号、银行卡号与预留手机号是否匹配,返回详细的验证状态说明。',
},
CQCXG1H7Y: {
name: "车辆过户简版查询",
component: defineAsyncComponent(() => import("@/ui/CQCXG1H7Y.vue")),
remark: '车辆过户简版查询用于查看车辆最近是否发生过户及累计过户次数,数据来源于车辆管理部门等权威机构,仅供参考。',
},
CQCXG1U4U: {
name: "车辆里程记录(混合查询)",
component: defineAsyncComponent(() => import("@/ui/CQCXG1U4U.vue")),
remark: '车辆里程记录(混合查询)综合诊断与维保记录,展示车辆里程变化与是否存在调表嫌疑,数据来源于车辆维保及检测机构,仅供参考。',
},
CQCXG3Y6B: {
name: "车辆维保简版查询",
component: defineAsyncComponent(() => import("@/ui/CQCXG3Y6B.vue")),
remark: '车辆维保简版查询按时间轴展示维保记录,包含保养与更换材料明细,数据来源于车辆维保机构,仅供参考。',
},
CQCXG3Z3L: {
name: "车辆维保详细版查询",
component: defineAsyncComponent(() => import("@/ui/CQCXG3Z3L.vue")),
remark: '车辆维保详细版查询展示品牌、车架号等基本信息及每次维保的详细内容,数据来源于车辆维保机构,仅供参考。',
},
CQCXG4D2E: {
name: "名下车辆数量查询",
component: defineAsyncComponent(() => import("@/ui/CQCXG4D2E.vue")),
remark: '名下车辆数量查询展示查询对象名下登记的车辆数量及车牌、车牌颜色、车辆类型等基本信息,数据来源于车辆管理部门等权威机构,仅供参考。',
},
CQCXG4I1Z: {
name: "车辆过户详版查询",
component: defineAsyncComponent(() => import("@/ui/CQCXG4I1Z.vue")),
remark: '车辆过户详版查询按时间轴展示每一次车辆过户的车牌与地区变更情况,数据来源于车辆管理部门等权威机构,仅供参考。',
},
CQCXG5U0Z: {
name: "车辆静态信息查询",
component: defineAsyncComponent(() => import("@/ui/CQCXG5U0Z.vue")),
remark: '车辆静态信息查询展示车辆生产日期、排放标准、燃料类型、发动机型号、生产企业名称等核心静态信息,数据来源于车辆管理部门等权威机构,仅供参考。',
},
CQCXG6B4E: {
name: "车辆出险记录核验",
component: defineAsyncComponent(() => import("@/ui/CQCXG6B4E.vue")),
remark: '车辆出险记录核验综合车辆出险、脱保、重大事故等信息评估风险等级,并给出二手车价格参考,数据来源于保险行业信息平台,仅供参考。',
},
CQCXGGB2Q: {
name: "车辆二要素核验V1",
component: defineAsyncComponent(() => import("@/ui/CQCXGGB2Q.vue")),
remark: '车辆二要素核验V1用于校验人员姓名与车辆号牌是否匹配数据来源于车辆管理部门等权威机构仅供参考。',
},
CQCXGP00W: {
name: "车辆出险详版查询",
component: defineAsyncComponent(() => import("@/ui/CQCXGP00W.vue")),
remark: '车辆出险详版查询展示多维出险记录、碰撞部位、车辆配件类别及车况信息,辅助评估车辆风险,数据来源于保险行业信息平台,仅供参考。',
},
CQCXGY7F2: {
name: "二手车VIN估值",
component: defineAsyncComponent(() => import("@/ui/CQCXGY7F2.vue")),
remark: '二手车VIN估值基于车型、排量、排放标准等信息给出参考估值实际价格以市场为准仅供参考。',
},
CQCXGYTS2: {
name: "车辆二要素核验V2",
component: defineAsyncComponent(() => import("@/ui/CQCXGYTS2.vue")),
remark: '车辆二要素核验V2展示人员与车辆的详细匹配结果及相关说明数据来源于车辆管理部门等权威机构仅供参考。',
},
CQVehicleGeneric: {
name: "车辆通用查询",
component: defineAsyncComponent(() => import("@/ui/CQVehicleGeneric.vue")),
remark: '车辆通用查询用于展示各类车辆相关接口的原始返回数据,便于调试和查看。',
},
CQYGL2S0W: {
name: "失信被执行人(企业,个人)",
component: defineAsyncComponent(() => import("@/ui/CQYGL2S0W.vue")),
remark: '失信被执行人查询用于识别个人或企业的严重违约风险,展示命中最高法院公布的失信被执行人信息,数据来源于法院公开信息,仅供参考。',
},
CQYGL5F6A: {
name: "名下企业关联",
component: defineAsyncComponent(() => import("@/ui/CQYGL5F6A.vue")),
remark: '名下企业关联展示查询对象作为法人、股东或高管关联的企业信息,包括企业状态、成立日期、注册资本、行业等多维度数据,仅供参考。',
},
CQYGL66SL: {
name: "全国企业司法模型服务查询新详版",
component: defineAsyncComponent(() => import("@/ui/CQYGL66SL.vue")),
remark: '全国企业司法模型服务查询新详版展示企业在全国法院公开信息中的民事、刑事、行政、执行、破产、保全等多类司法案件情况,数据来源于法院公开信息,仅供参考。',
},
CYYSY3M8S: {
name: "运营商二要素V即时版",
component: defineAsyncComponent(() => import("@/ui/CYYSY3M8S.vue")),
remark: '运营商二要素V即时版用于核验手机号与姓名是否一致返回核验结果及计费状态数据来源于运营商系统仅供参考。',
},
CYYSY6F2B: {
name: "手机消费区间验证",
component: defineAsyncComponent(() => import("@/ui/CYYSY6F2B.vue")),
remark: '手机消费区间验证根据运营商数据评估消费能力档位,同时展示运营商及携号转网信息,数据来源于运营商系统,仅供参考。',
},
CYYSY9E4A: {
name: "天远手机号码归属地核验A",
component: defineAsyncComponent(() => import("@/ui/CYYSY9E4A.vue")),
remark: '天远手机号码归属地核验A展示手机号码的省市、运营商、区号及邮编等归属信息数据来源于运营商系统仅供参考。',
},
CYYSYE7V5: {
name: "手机在网状态V即时版",
component: defineAsyncComponent(() => import("@/ui/CYYSYE7V5.vue")),
remark: '手机在网状态V即时版用于判断号码当前是否在网可用返回在网状态及运营商信息数据来源于运营商系统仅供参考。',
},
CYYSYF2T7: {
name: "号码二次放号V即时版",
component: defineAsyncComponent(() => import("@/ui/CYYSYF2T7.vue")),
remark: '号码二次放号V即时版用于判断该手机号是否为二次放号返回核验结果及运营商信息数据来源于运营商系统仅供参考。',
},
CYYSYK8R3: {
name: "手机空号检测V即时版",
component: defineAsyncComponent(() => import("@/ui/CYYSYK8R3.vue")),
remark: '手机空号检测V即时版用于判断号码是空号、实号还是沉默号或风险号返回号码状态及归属地信息数据来源于运营商系统仅供参考。',
},
CYYSYK9R4: {
name: "全网手机三要素验证",
component: defineAsyncComponent(() => import("@/ui/CYYSYK9R4.vue")),
remark: '全网手机三要素验证用于核验手机号、身份证号与姓名是否一致,返回验证结果,数据来源于运营商系统,仅供参考。',
},
CYYSYP0T4: {
name: "手机号码在网时长V即时版",
component: defineAsyncComponent(() => import("@/ui/CYYSYP0T4.vue")),
remark: '手机号码在网时长V即时版展示号码在当前运营商下的在网时长区间数据来源于运营商系统仅供参考。',
},
CYYSYS9W1: {
name: "手机携号转网V即时版",
component: defineAsyncComponent(() => import("@/ui/CYYSYS9W1.vue")),
remark: '手机携号转网V即时版查询号码是否发生携号转网及前后运营商变更情况数据来源于运营商系统仅供参考。',
},
QCXG5F3A: {
name: "名下车辆车牌查询B",
component: defineAsyncComponent(() => import("@/ui/QCXG5F3A.vue")),
remark: '名下车辆车牌查询B展示查询对象名下登记的车辆车牌号、车牌颜色、车辆类型等信息数据来源于车辆管理部门等权威机构仅供参考。',
},
IVYZ4Y27: {
name: "学历信息高级版",
component: defineAsyncComponent(() => import("@/ui/IVYZ4Y27/index.vue")),
remark: '名下车辆车牌查询B展示查询对象名下登记的车辆车牌号、车牌颜色、车辆类型等信息数据来源于车辆管理部门等权威机构仅供参考。',
},
DWBG5SAM: {
name: "天远指谜报告",
component: defineAsyncComponent(() => import("@/ui/DWBG5SAM/index.vue")),
remark: '天远指谜报告综合展示身份核验、信用等级、风险画像与名单、公安不良、逾期与司法案件等维度,数据来源于合作机构,仅供参考。',
},
DWBG9FB3: {
name: "海宇贷前风险档案",
component: defineAsyncComponent(() => import("@/ui/DWBG9FB3/index.vue")),
remark: '海宇贷前风险档案综合展示风险评估、基本信息、借贷画像、逾期黑名单、欺诈黑名单、投诉风险、逾期勘测、借贷意向与司法案件等维度,数据来源于合作机构,仅供参考。',
},
DWBG9FB2: {
name: "海宇个人风险报告",
component: defineAsyncComponent(() => import("@/ui/DWBG9FB2/index.vue")),
remark: '海宇租赁海宇个人风险报告综合展示风险评估、基本信息、借贷画像、欺诈黑名单、逾期勘测、借贷意向、3C租赁申请意向与司法案件等维度数据来源于合作机构仅供参考。',
},
};
const maskValue = computed(() => {
return (type, value) => {
if (!value) return value;
@@ -478,6 +675,7 @@ const maskValue = computed(() => {
const featureRiskLevels = {
// 🔴 高风险类
'FLXG0V4B': 20, // 司法涉诉
'FLXGK5D2': 20, // 法院被执行人高级版
'FLXG7E8F': 20, // 个人涉诉
// 🟠 中高风险类 - 权重 7
@@ -494,10 +692,15 @@ const featureRiskLevels = {
'JRZQ3C9R': 7, // 支付行为指数
'YYSY7D3E': 5, // 手机携号转网
'YYSY8B1C': 5, // 手机在网时长
'CFLX3A9B': 5, // 法院被执行人限高版
'IVYZ4Y27' :3 , //xueli
'DWBG5SAM': 10,
'DWBG9FB3': 10,
'DWBG9FB2': 10,
// 🟡 中风险类 - 权重 5
'QYGL3F8E': 5, // 人企关系加强版
'QCXG9P1C': 5, // 名下车辆
'QCXG9P1C': 5, // 名下车辆贷前
'QCXG7A2B': 3, // 名下车辆(简化版)
'JRZQ09J8': 5, // 收入评估
'JRZQ8B3C': 5, // 个人消费能力等级
@@ -505,6 +708,8 @@ const featureRiskLevels = {
'FLXGDEA9': 15, // 公安不良人员名单(加强版)
'IVYZ81NC': 3, // 单人婚姻查询(登记时间版)
'IVYZ5733': 3, // 单人婚姻状态A
'IVYZ0S0D': 3, // 劳动仲裁信息查询(个人版)
// 📊 复合报告类 - 按子模块动态计算
'DWBG8B4D': 0, // 谛听多维报告(由子模块计算)
@@ -521,6 +726,8 @@ const featureRiskLevels = {
'DWBG8B4D_LeasingRisk': 6,
'DWBG8B4D_RiskSupervision': 8,
'DWBG8B4D_RiskWarningTab': 9,
'DWBG6A2C_CourtRiskInfo':9,
// 司南报告子模块
'DWBG6A2C_StandLiveInfo': 4,
@@ -535,6 +742,7 @@ const featureRiskLevels = {
'DWBG6A2C_CreditDetail': 5,
'DWBG6A2C_RentalBehavior': 5,
'DWBG6A2C_RiskSupervision': 8,
'DWBG6A2C_CourtRiskInfo':9,
// 人企关系加强版子模块
'CQYGL3F8E_Investment': 4,
@@ -545,8 +753,68 @@ const featureRiskLevels = {
'CQYGL3F8E_Punishment': 7,
'CQYGL3F8E_Abnormal': 6,
'CQYGL3F8E_TaxRisk': 7,
// 新增模块 - 默认权重3待调整
'CIVYZ6M8P': 3,
'CIVYZ7F3A': 3,
'CIVYZ9K7F': 3,
'CIVYZA1B3': 3,
'CJRZQ0B6Y': 3,
'CJRZQACAB': 3,
'CQCXG1H7Y': 3,
'CQCXG1U4U': 3,
'CQCXG3Y6B': 3,
'CQCXG3Z3L': 3,
'CQCXG4D2E': 3,
'CQCXG4I1Z': 3,
'CQCXG5U0Z': 3,
'CQCXG6B4E': 3,
'CQCXGGB2Q': 3,
'CQCXGP00W': 3,
'CQCXGY7F2': 3,
'CQCXGYTS2': 3,
'CQVehicleGeneric': 3,
'CQYGL2S0W': 3,
'CQYGL5F6A': 3,
'CQYGL66SL': 3,
'CYYSY3M8S': 3,
'CYYSY6F2B': 3,
'CYYSY9E4A': 3,
'CYYSYE7V5': 3,
'CYYSYF2T7': 3,
'CYYSYK8R3': 3,
'CYYSYK9R4': 3,
'CYYSYP0T4': 3,
'CYYSYS9W1': 3,
'QCXG5F3A': 3,
};
// 处理数据拆分支持DWBG8B4D、DWBG6A2C、CJRZQ5E9F和CQYGL3F8E
const processedReportData = computed(() => {
console.log('[BaseReport.vue] 开始计算 processedReportData...');
let data = reportData.value || [];
// 拆分DWBG8B4D数据
data = splitDWBG8B4DForTabs(data);
// 拆分DWBG6A2C数据
data = splitDWBG6A2CForTabs(data);
// 拆分CQYGL3F8E数据
data = splitCQYGL3F8EForTabs(data);
// 过滤掉在featureMap中没有对应的项
const filtered = data.filter(item => {
const apiID = item?.data?.apiID;
const exists = item && item.data && apiID && featureMap[apiID];
if (!exists && item?.data?.apiID) {
console.warn(`[BaseReport.vue] 未找到 API ID "${apiID}" 的对应组件配置,已过滤。`);
}
return exists;
});
console.log('[BaseReport.vue] 过滤后模块数量:', filtered.length);
return filtered;
});
// 存储每个组件的 ref 引用
const componentRefs = ref({});
@@ -566,6 +834,7 @@ defineExpose({
// 计算综合评分的函数(分数越高越安全)
const calculateScore = () => {
console.log('[BaseReport.vue] 开始计算报告评分...');
// 收集实际存在的 features 及其风险权重
const presentFeatures = [];
@@ -586,6 +855,7 @@ const calculateScore = () => {
});
});
console.log('[BaseReport.vue] 参与评分的特征数量:', presentFeatures.length);
if (presentFeatures.length === 0) return 100; // 无有效特征时返回满分(最安全)
// 累计总风险分数
@@ -622,12 +892,14 @@ const calculateScore = () => {
totalRiskScore += riskContribution;
});
console.log('[BaseReport.vue] 计算得出的总风险分数:', totalRiskScore);
// 将总风险分数限制在 0-90 范围内确保最低分为10分
const finalRiskScore = Math.max(0, Math.min(90, Math.round(totalRiskScore)));
// 转换为安全分数分数越高越安全100 - 风险分数)
// 最终分数范围10-100分
const safetyScore = 100 - finalRiskScore;
console.log('[BaseReport.vue] 最终安全评分:', safetyScore);
return safetyScore;
};
@@ -641,11 +913,11 @@ watch([reportData, componentRiskScores], () => {
timestamp: new Date().toISOString(),
finalScore: reportScore.value,
reportModules: processedReportData.value.map((item, index) => ({
apiID: item.data.apiID,
name: featureMap[item.data.apiID]?.name || '未知',
apiID: item?.data?.apiID || 'unknown',
name: featureMap[item?.data?.apiID]?.name || '未知',
index: index,
riskScore: componentRiskScores.value[`${item.data.apiID}_${index}`] ?? '未上报',
weight: featureRiskLevels[item.data.apiID] ?? 0
riskScore: componentRiskScores.value[`${item?.data?.apiID}_${index}`] ?? '未上报',
weight: featureRiskLevels[item?.data?.apiID] ?? 0
})),
componentScores: componentRiskScores.value,
riskLevels: featureRiskLevels

View File

@@ -1,15 +1,31 @@
console.log('[main.js] 脚本开始执行...');
import "./assets/main.css";
import { createApp } from "vue";
import { createRouter, createWebHistory } from "vue-router";
import App from "./App.vue";
console.log('[main.js] 依赖库导入完成');
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: "/",
name: "Report",
component: () => import("./views/Report.vue"),
component: () => {
console.log('[main.js] 路由正在加载 Report.vue...');
return import("./views/Report.vue");
},
},
{
path: "/DWBG9FB3",
name: "DWBG9FB3",
component: () => import("./views/DWBG9FB3Report.vue"),
},
{
path: "/DWBG9FB2",
name: "DWBG9FB2",
component: () => import("./views/DWBG9FB2Report.vue"),
},
],
});
@@ -17,4 +33,6 @@ const router = createRouter({
const app = createApp(App);
app.use(router);
console.log('[main.js] 准备挂载应用...');
app.mount("#app");
console.log('[main.js] 应用挂载指令已发出');

View File

@@ -0,0 +1,96 @@
<template>
<div class="gamma-grid-3">
<div class="gamma-card info-card">
<div class="gamma-title"><span>👤</span> 用户信息</div>
<div class="info-item">
<span class="info-label">姓名:</span>
<span class="info-value">
{{ maskedName }}
<span v-if="realNameAuth.coincide" class="gamma-tag">身份证姓名一致</span>
</span>
</div>
<div class="info-item"><span class="info-label">性别</span><span>{{ params.sex || '—' }}</span></div>
<div class="info-item"><span class="info-label">年龄</span><span>{{ params.age || '—' }}</span></div>
<div class="info-item"><span class="info-label">身份证号</span><span>{{ maskedIdCard }}</span></div>
<div class="info-item"><span class="info-label">户籍地</span><span>{{ params.location || '—' }}</span></div>
</div>
<div class="gamma-card info-card">
<div class="gamma-title"><span>📱</span> 手机信息</div>
<div class="info-item">
<span class="info-label">手机号:</span>
<span>
{{ maskedMobile }}
<span v-if="mobile3Verify.status === 1" class="gamma-tag">身份证姓名手机号一致</span>
</span>
</div>
<div class="info-item">
<span class="info-label">手机号在网时长:</span>
<span>{{ durationText }} </span>
</div>
<div class="info-item">
<span class="info-label">手机号在网状态:</span>
<span :class="mobile4Verify.status === 2 ? 'gamma-text-danger' : ''">
{{ mobileStatusText(mobile4Verify.status) }}
<span v-if="mobile4Verify.status === 2"> </span>
</span>
</div>
<div class="info-item"><span class="info-label">手机号运营商:</span><span>{{ params.carrier || '—' }}</span></div>
<div class="info-item"><span class="info-label">手机号归属地:</span><span>{{ params.phonePlace || '—' }}</span></div>
</div>
<div class="gamma-card info-card">
<div class="gamma-title"><span></span> 司法风险</div>
<div v-for="item in courtItems" :key="item.key" class="info-item">
<span class="info-label">
<span :class="item.hit ? 'gamma-text-danger' : 'gamma-text-success'">{{ item.hit ? '❗' : '✅' }}</span>
{{ item.label }}
</span>
<span :class="item.hit ? 'gamma-text-danger' : ''">{{ item.hit ? '是' : '否' }}</span>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { maskName, maskIdCard, maskMobile, mobileStatusText, buildCourtRiskItems } from '../reportHelper';
const props = defineProps({
params: { type: Object, default: () => ({}) },
realNameAuth: { type: Object, default: () => ({}) },
mobile3Verify: { type: Object, default: () => ({}) },
mobile4Verify: { type: Object, default: () => ({}) },
mobileDuration: { type: Object, default: () => ({}) },
courtRisk: { type: Object, default: () => ({}) },
personalLawsuit: { type: Object, default: () => ({}) },
});
const maskedName = computed(() => maskName(props.params.name));
const maskedIdCard = computed(() => maskIdCard(props.params.id_card));
const maskedMobile = computed(() => maskMobile(props.params.mobile));
const durationText = computed(() => {
const range = props.mobileDuration.range || '';
if (range.includes('[24')) return '24~36月';
return range || '—';
});
const courtItems = computed(() =>
buildCourtRiskItems(props.courtRisk, props.personalLawsuit),
);
</script>
<style lang="scss" scoped>
.info-card { margin-bottom: 0; }
.info-item {
display: flex;
justify-content: space-between;
margin: 10px 0;
font-size: 14px;
gap: 8px;
}
.info-label { color: #666; flex-shrink: 0; }
</style>

View File

@@ -0,0 +1,87 @@
<template>
<div class="gamma-card">
<div class="gamma-title"><span>📞</span> 投诉风险筛查</div>
<p class="gamma-small" style="margin-bottom: 16px;">风险分: 0-100分数越高投诉风险越高</p>
<div class="complaint-content">
<div class="complaint-score-card">
<div class="gamma-subtitle"><span></span> 筛查结果</div>
<div class="complaint-risk-circle">
<div class="circle" />
<div class="complaint-risk-text">
<div class="score">{{ data.score ?? '—' }}</div>
<div class="gamma-small">风险分</div>
</div>
</div>
</div>
<div>
<table class="gamma-table">
<thead>
<tr><th>规则名称</th><th>权重</th></tr>
</thead>
<tbody>
<tr><td>是否建议拨打电话</td><td>{{ data.is_call ? '是' : '否' }}</td></tr>
<tr><td>是否高频</td><td>{{ data.is_gp ? '是' : '否' }}</td></tr>
<tr><td>是否靓号</td><td>{{ data.is_lh ? '是' : '否' }}</td></tr>
<tr><td>近14天其他类来电次数</td><td>{{ data.other_times?.day_14 ?? '—' }}</td></tr>
<tr><td>近14天金融类来电次数</td><td>{{ data.finance_times?.day_14 ?? '—' }}</td></tr>
<tr><td>用户接受意愿登记</td><td>{{ dncText }}</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { dncRegisterText } from '../reportHelper';
const props = defineProps({
data: { type: Object, default: () => ({}) },
});
const dncText = computed(() => dncRegisterText(props.data.dnc));
</script>
<style lang="scss" scoped>
.complaint-content {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 24px;
}
.complaint-score-card {
display: flex;
flex-direction: column;
align-items: center;
}
.complaint-risk-circle {
width: 120px;
height: 120px;
position: relative;
}
.complaint-risk-circle .circle {
width: 100px;
height: 100px;
border: 6px solid #eee;
border-top-color: #66cc99;
border-radius: 50%;
margin: 0 auto;
}
.complaint-risk-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
}
.score { font-size: 28px; font-weight: 600; }
@media (max-width: 768px) {
.complaint-content { grid-template-columns: 1fr; }
}
</style>

View File

@@ -0,0 +1,462 @@
<template>
<div class="panorama-report">
<div class="panorama-main-title"><span>🛡</span> 信用全景扫描</div>
<!-- 第一行指数 + 机构借贷情况 -->
<div class="panorama-row">
<div class="panorama-card">
<div class="score-grid">
<div v-for="item in CREDIT_PANORAMA_SCORES" :key="item.key" class="score-item">
<div class="score-label">{{ item.label }}</div>
<div class="circle-index">
<span :class="['score-value', { muted: formatModelScore(data[item.key]) === '未命中' }]">
{{ formatModelScore(data[item.key]) }}
</span>
</div>
<div class="score-hint">350-950指数越大逾期率越低</div>
</div>
</div>
</div>
<div class="panorama-card">
<div class="block-title"><span>🏦</span> 机构借贷情况</div>
<div class="mini-grid">
<div v-for="item in CREDIT_PANORAMA_INSTITUTIONS" :key="item.key" class="mini-box">
<div class="mini-box-head">
<span>{{ item.label }}</span>
<span
class="gamma-tag"
:class="{
'gamma-tag--danger': getPanoramaRiskTag(data[item.key]).danger,
'gamma-tag--warn': getPanoramaRiskTag(data[item.key]).warn,
}"
>
{{ getPanoramaRiskTag(data[item.key]).label }}
</span>
</div>
<div class="mini-box-value">{{ formatPanoramaCount(data[item.key], item.unit) }}</div>
</div>
</div>
</div>
</div>
<!-- 第二行近期贷款申请 + 还款历史 -->
<div class="panorama-row">
<div class="panorama-card">
<div class="block-title"><span></span> 近期贷款申请</div>
<div class="list-rows">
<div v-for="item in CREDIT_PANORAMA_RECENT_LOAN" :key="item.key" class="list-row">
<div class="list-row-left">
<span
class="gamma-tag tag-inline"
:class="{
'gamma-tag--danger': getPanoramaRiskTag(data[item.key]).danger,
'gamma-tag--warn': getPanoramaRiskTag(data[item.key]).warn,
}"
>
{{ getPanoramaRiskTag(data[item.key]).label }}
</span>
<span>{{ item.label }}</span>
</div>
<span class="list-row-value">{{ formatPanoramaCount(data[item.key], item.unit) }}</span>
</div>
</div>
</div>
<div class="panorama-card">
<div class="block-title"><span>📋</span> 还款历史</div>
<div class="stat-grid-2">
<div class="stat-box">
<div class="stat-label">历史贷款机构成功还款笔数</div>
<div class="stat-value">{{ formatPanoramaCount(data.xyp_cpl0014, ' 笔') }}</div>
</div>
<div class="stat-box">
<div class="stat-label">历史贷款机构交易失败笔数</div>
<div class="stat-value">{{ formatPanoramaCount(data.xyp_cpl0015, ' 笔') }}</div>
</div>
</div>
<div class="list-rows">
<div class="list-row">
<span>90天还款成功率</span>
<span class="list-row-value">{{ formatPanoramaRatio(data.xyp_cpl0080) }}</span>
</div>
<div class="list-row">
<span>近90天内还款中成功还款总金额比例</span>
<span class="list-row-value">{{ formatPanoramaRatio(data.xyp_cpl0079) }}</span>
</div>
<div class="list-row">
<span>近5次还款中成功还款总金额比例</span>
<span class="list-row-value">{{ formatPanoramaRatio(data.xyp_cpl0073) }}</span>
</div>
<div class="list-row">
<span>近5次还款中还款成功笔数比例</span>
<span class="list-row-value">{{ formatPanoramaRatio(data.xyp_cpl0074) }}</span>
</div>
</div>
</div>
</div>
<!-- 第三行交易失败 + 交易失败后还款 -->
<div class="panorama-row">
<div class="panorama-card">
<div class="block-title"><span></span> 交易失败情况</div>
<div class="list-rows">
<div v-for="item in CREDIT_PANORAMA_FAIL_COUNTS" :key="item.key" class="list-row">
<div class="list-row-left">
<span
class="gamma-tag tag-inline"
:class="{
'gamma-tag--danger': getPanoramaRiskTag(data[item.key]).danger,
'gamma-tag--warn': getPanoramaRiskTag(data[item.key]).warn,
}"
>
{{ getPanoramaRiskTag(data[item.key]).label }}
</span>
<span>{{ item.label }}</span>
</div>
<span class="list-row-value">{{ formatPanoramaCount(data[item.key], item.unit) }}</span>
</div>
</div>
</div>
<div class="panorama-card">
<div class="block-title"><span></span> 交易失败后还款情况</div>
<div class="list-rows">
<div v-for="item in CREDIT_PANORAMA_FAIL_RECOVERY" :key="item.key" class="list-row">
<div class="list-row-left">
<span
class="gamma-tag tag-inline"
:class="{
'gamma-tag--danger': getPanoramaRiskTag(data[item.key]).danger,
'gamma-tag--warn': getPanoramaRiskTag(data[item.key]).warn,
}"
>
{{ getPanoramaRiskTag(data[item.key]).label }}
</span>
<span>{{ item.label }}</span>
</div>
<span class="list-row-value">{{ formatPanoramaCount(data[item.key], item.unit) }}</span>
</div>
</div>
</div>
</div>
<!-- 第四行逾期情况 + 贷款整体情况 -->
<div class="panorama-row">
<div class="panorama-card">
<div class="block-title"><span>🚨</span> 逾期情况</div>
<div class="stat-grid-2">
<div class="stat-box">
<div class="stat-label">当前逾期机构数</div>
<div class="stat-value">{{ formatPanoramaCount(data.xyp_cpl0071, '家') }}</div>
</div>
<div class="stat-box">
<div class="stat-label">当前逾期金额</div>
<div class="stat-value">{{ formatPanoramaAmount(data.xyp_cpl0072) }}</div>
</div>
</div>
<div class="list-rows">
<div v-for="item in CREDIT_PANORAMA_OVERDUE_FLAGS" :key="item.key" class="list-row flag-row">
<div class="list-row-left">
<span class="success-check"></span>
<span>{{ item.label }}</span>
</div>
<span
class="list-row-value"
:class="{ 'gamma-text-danger': data[item.key] === '1' }"
>
{{ formatPanoramaYesNo(data[item.key]) }}
</span>
</div>
</div>
</div>
<div class="panorama-card">
<div class="block-title"><span>📈</span> 贷款整体情况</div>
<div class="list-rows">
<div v-for="item in CREDIT_PANORAMA_LOAN_OVERVIEW" :key="item.key" class="list-row flag-row">
<div class="list-row-left">
<span class="success-check"></span>
<span>{{ item.label }}</span>
</div>
<span class="list-row-value">
{{
item.type === 'amount'
? formatPanoramaAmount(data[item.key])
: formatPanoramaCount(data[item.key], item.unit)
}}
</span>
</div>
</div>
</div>
</div>
<!-- 第五行贷款分周期汇总 -->
<div class="panorama-card panorama-card--full">
<div class="block-title"><span>📊</span> 贷款分周期汇总</div>
<div class="table-wrap">
<table class="gamma-table panorama-table">
<thead>
<tr>
<th>周期</th>
<th>贷款机构数</th>
<th>还款成功笔数</th>
<th>还款成功总金额()</th>
<th>交易失败笔数</th>
<th>交易失败总金额()</th>
</tr>
</thead>
<tbody>
<tr v-for="row in CREDIT_PANORAMA_PERIOD_ROWS" :key="row.label">
<td>{{ row.label }}</td>
<td>{{ panoramaTableCell(data, row.instKey) }}</td>
<td>{{ panoramaTableCell(data, row.successKey) }}</td>
<td>{{ panoramaTableCell(data, row.successAmtKey, 'amount') }}</td>
<td>{{ panoramaTableCell(data, row.failKey) }}</td>
<td>{{ panoramaTableCell(data, row.failAmtKey, 'amount') }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script setup>
import {
CREDIT_PANORAMA_SCORES,
CREDIT_PANORAMA_INSTITUTIONS,
CREDIT_PANORAMA_RECENT_LOAN,
CREDIT_PANORAMA_FAIL_COUNTS,
CREDIT_PANORAMA_FAIL_RECOVERY,
CREDIT_PANORAMA_OVERDUE_FLAGS,
CREDIT_PANORAMA_LOAN_OVERVIEW,
CREDIT_PANORAMA_PERIOD_ROWS,
formatModelScore,
formatPanoramaCount,
formatPanoramaAmount,
formatPanoramaRatio,
formatPanoramaYesNo,
getPanoramaRiskTag,
panoramaTableCell,
} from '../reportHelper';
defineProps({
data: { type: Object, default: () => ({}) },
});
</script>
<style lang="scss" scoped>
.panorama-report {
background: #fff;
border-radius: 8px;
padding: 24px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.panorama-main-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 8px;
color: #333;
padding-left: 10px;
border-left: 4px solid #d4af37;
}
.panorama-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
.panorama-card {
background: #fff;
border: 1px solid #eee;
border-radius: 12px;
padding: 20px;
&--full {
margin-bottom: 0;
}
}
.block-title {
font-size: 15px;
font-weight: 600;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 6px;
color: #333;
}
.score-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.score-item {
text-align: center;
}
.score-label {
font-size: 14px;
font-weight: 500;
color: #444;
margin-bottom: 12px;
}
.circle-index {
width: 120px;
height: 120px;
border: 4px solid #f0f0f0;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 10px;
}
.score-value {
font-size: 20px;
font-weight: 600;
color: #2da44e;
&.muted {
font-size: 16px;
color: #999;
}
}
.score-hint {
font-size: 12px;
color: #999;
line-height: 1.4;
}
.mini-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.mini-box {
border: 1px solid #eee;
border-radius: 8px;
padding: 12px;
}
.mini-box-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
font-size: 13px;
color: #444;
}
.mini-box-value {
margin-top: 6px;
font-size: 13px;
color: #666;
}
.list-rows {
display: flex;
flex-direction: column;
gap: 12px;
}
.list-row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
font-size: 13px;
color: #444;
}
.list-row-left {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
min-width: 0;
}
.tag-inline {
flex-shrink: 0;
}
.list-row-value {
flex-shrink: 0;
color: #666;
}
.stat-grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-bottom: 16px;
}
.stat-box {
border: 1px solid #eee;
border-radius: 8px;
padding: 16px;
text-align: center;
}
.stat-label {
font-size: 12px;
color: #999;
margin-bottom: 6px;
}
.stat-value {
font-size: 16px;
font-weight: 500;
color: #444;
}
.success-check {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
background: #2da44e;
border-radius: 50%;
color: #fff;
font-size: 10px;
flex-shrink: 0;
}
.flag-row .list-row-left span:last-child {
line-height: 1.4;
}
.table-wrap {
overflow-x: auto;
}
.panorama-table {
min-width: 720px;
}
@media (max-width: 992px) {
.panorama-row {
grid-template-columns: 1fr;
}
.score-grid {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,219 @@
<template>
<div class="gamma-card">
<div class="gamma-title"><span>👤</span> 欺诈黑名单</div>
<div class="gamma-subtitle"><span></span> 风险等级</div>
<div class="fraud-risk-circle">
<div class="circle" />
<div class="fraud-risk-text">
<div class="risk-level">{{ gradeText }}</div>
<div class="gamma-small">风险等级</div>
</div>
</div>
<div class="gamma-subtitle"><span></span> 命中统计</div>
<div class="fraud-stats-grid">
<div class="chart-card">
<div class="chart-title">命中总次数</div>
<div class="chart-bars">
<div v-for="bar in hitBars" :key="bar.label" class="bar-group">
<div class="bar-value">{{ bar.value }}</div>
<div class="bar" :style="{ height: bar.height + 'px' }" />
<div class="bar-label">{{ bar.label }}</div>
</div>
</div>
</div>
<div class="chart-card">
<div class="chart-title">命中机构数</div>
<div class="chart-bars">
<div v-for="bar in orgBars" :key="bar.label" class="bar-group">
<div class="bar-value">{{ bar.value }}</div>
<div class="bar" :style="{ height: bar.height + 'px' }" />
<div class="bar-label">{{ bar.label }}</div>
</div>
</div>
</div>
</div>
<div class="gamma-subtitle"><span></span> 命中分布</div>
<div class="chart-card">
<div class="distribution-bars">
<div v-for="(dist, i) in distributions" :key="i" class="distribution-bar-group">
<div class="distribution-bar blue" :style="{ height: dist.d30 + 'px' }" />
<div class="distribution-bar orange" :style="{ height: dist.d90 + 'px' }" />
<div class="distribution-bar yellow" :style="{ height: dist.d180 + 'px' }" />
</div>
</div>
<div class="distribution-labels">
<div v-for="label in FRAUD_DIST_LABELS" :key="label">{{ label }}</div>
</div>
<div class="chart-legend">
<div class="legend-item"><div class="legend-color blue" /><span>近30天</span></div>
<div class="legend-item"><div class="legend-color orange" /><span>近90天</span></div>
<div class="legend-item"><div class="legend-color yellow" /><span>近180天</span></div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { FRAUD_DIST_LABELS, FRAUD_DIST_KEYS, fraudGradeText } from '../reportHelper';
const props = defineProps({
data: { type: Object, default: () => ({}) },
});
const gradeText = computed(() => fraudGradeText(props.data.grade));
function barHeight(val, max) {
if (!max) return 0;
return Math.max(4, Math.round((Number(val) || 0) / max * 120));
}
const hitBars = computed(() => {
const d = props.data;
const values = [
{ label: '近30天', value: d.ha_30d_C ?? 0 },
{ label: '近90天', value: d.ha_90d_C ?? 0 },
{ label: '近180天', value: d.ha_180d_C ?? 0 },
];
const max = Math.max(...values.map((v) => Number(v.value)), 1);
return values.map((v) => ({ ...v, height: barHeight(v.value, max) }));
});
const orgBars = computed(() => {
const d = props.data;
const values = [
{ label: '近30天', value: d.ha_30d_J ?? 0 },
{ label: '近90天', value: d.ha_90d_J ?? 0 },
{ label: '近180天', value: d.ha_180d_J ?? 0 },
];
const max = Math.max(...values.map((v) => Number(v.value)), 1);
return values.map((v) => ({ ...v, height: barHeight(v.value, max) }));
});
const distributions = computed(() => {
const d = props.data;
const max = Math.max(
...FRAUD_DIST_KEYS.flatMap((k) => [d[k.d30], d[k.d90], d[k.d180]].map(Number)),
1,
);
return FRAUD_DIST_KEYS.map((k) => ({
d30: barHeight(d[k.d30], max),
d90: barHeight(d[k.d90], max),
d180: barHeight(d[k.d180], max),
}));
});
</script>
<style lang="scss" scoped>
.fraud-risk-circle {
width: 200px;
height: 200px;
margin: 0 auto 24px;
position: relative;
}
.fraud-risk-circle .circle {
width: 160px;
height: 160px;
border: 8px solid #eee;
border-top-color: #fdd860;
border-right-color: #fdd860;
border-radius: 50%;
margin: 0 auto;
}
.fraud-risk-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
}
.risk-level { font-size: 20px; font-weight: 600; color: #fdd860; }
.fraud-stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
margin-bottom: 16px;
}
.chart-card {
border: 1px solid #eee;
border-radius: 6px;
padding: 16px;
}
.chart-title { text-align: center; margin-bottom: 12px; font-size: 14px; }
.chart-bars {
display: flex;
align-items: flex-end;
justify-content: space-around;
height: 150px;
}
.bar-group { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.bar {
width: 30px;
background: #4a6fd4;
border-radius: 3px 3px 0 0;
}
.bar-label, .bar-value { font-size: 12px; color: #666; }
.distribution-bars {
display: flex;
align-items: flex-end;
justify-content: space-around;
height: 160px;
margin-top: 20px;
}
.distribution-bar-group {
display: flex;
gap: 4px;
align-items: flex-end;
}
.distribution-bar {
width: 20px;
border-radius: 2px 2px 0 0;
}
.distribution-bar.blue { background: #4a6fd4; }
.distribution-bar.orange { background: #f28534; }
.distribution-bar.yellow { background: #f7bc0c; }
.distribution-labels {
display: flex;
justify-content: space-around;
font-size: 10px;
color: #666;
margin-top: 8px;
}
.chart-legend {
display: flex;
justify-content: center;
gap: 16px;
font-size: 12px;
margin-top: 12px;
}
.legend-item { display: flex; align-items: center; gap: 4px; }
.legend-color {
width: 12px;
height: 8px;
border-radius: 2px;
}
.legend-color.blue { background: #4a6fd4; }
.legend-color.orange { background: #f28534; }
.legend-color.yellow { background: #f7bc0c; }
</style>

View File

@@ -0,0 +1,984 @@
<template>
<div class="gamma-card judicial-section">
<div class="gamma-title"><span>🗺</span> 司法案件</div>
<!-- 案件概览 -->
<div class="block-title">案件概览</div>
<div class="case-overview">
<div
v-for="item in caseTypeCards"
:key="item.key"
class="overview-card"
:class="{ criminal: item.count > 0 && item.key === 'criminal' }"
>
<div v-if="item.count > 0 && item.key === 'criminal'" class="badge">!</div>
<div class="card-title">{{ item.label }}</div>
<div class="card-num">{{ item.count }}</div>
</div>
</div>
<!-- 司法风险 -->
<div class="risk-section">
<div class="risk-item">
<div class="label">司法风险<br>被告案件数</div>
<div class="bar-wrap">
<div class="bar-fill" :style="{ width: defendantBarWidth }" />
</div>
<div class="legend">
<span class="unresolved">未结案{{ count.count_wei_beigao ?? 0 }}</span>
<span class="resolved">已经结案{{ count.count_jie_beigao ?? 0 }}</span>
</div>
<div class="total-hint">{{ count.count_beigao ?? 0 }}</div>
</div>
<div class="risk-item">
<div class="label">被告案件金额</div>
<div class="num-row">
<span class="icon-yen">¥</span>
<span>{{ count.money_beigao ?? 0 }}</span>
</div>
</div>
<div class="risk-item">
<div class="label">被执行案件数</div>
<div class="num-row red">
<span>📋</span>
<span>{{ implementCount }}</span>
</div>
</div>
<div class="risk-item">
<div class="label">最近案件年份</div>
<div class="num-row red">
<span>🗓</span>
<span>{{ recentYearText }}</span>
</div>
</div>
</div>
<!-- 案件统计 -->
<div class="block-title">案件统计</div>
<div class="case-stats">
<div class="chart-title">案件数量</div>
<div class="bar-chart">
<div class="chart-legend">
<span class="unresolved">
未结案统计
<em>{{ count.count_wei_total ?? 0 }}</em>
</span>
<span class="resolved">
已结案统计
<em>{{ count.count_jie_total ?? 0 }}</em>
</span>
</div>
<div class="bar-rows">
<div v-for="row in roleBarRows" :key="row.label" class="bar-row">
<div class="row-label">{{ row.label }}</div>
<div class="row-bar-track">
<div class="row-bar" :style="{ width: row.width }" />
</div>
<div class="row-value">{{ row.count }}</div>
</div>
</div>
<div class="axis">
<span v-for="tick in axisTicks" :key="tick">{{ tick }}</span>
</div>
</div>
<div class="chart-title">涉案金额</div>
<div class="amount-section">
<div v-for="card in amountCards" :key="card.title" class="amount-card">
<div class="card-header">
<span>{{ card.icon }}</span>
<span>{{ card.title }}</span>
</div>
<div class="row"><span>总案件</span><span class="value">{{ card.total }}</span></div>
<div class="row"><span>已结案</span><span class="value">{{ card.jie }}</span></div>
<div class="row"><span>未结案</span><span class="value">{{ card.wei }}</span></div>
</div>
</div>
<div class="pie-section">
<div v-for="pie in pieCharts" :key="pie.title" class="pie-card">
<div class="pie-container">
<div class="pie-circle" :class="{ simple: pie.items.length <= 1 }" :style="{ background: pie.gradient }">
<div class="inner">
{{ pie.title }}<br>占比图
</div>
</div>
</div>
<div v-if="pie.items.length" class="pie-legend">
<div v-for="(item, i) in pie.items" :key="i" class="pie-legend-item">
<span class="dot" :style="{ background: pieColors[i % pieColors.length] }" />
{{ item.label }}({{ item.count }})
</div>
</div>
</div>
</div>
</div>
<!-- 案件列表可展开详情 -->
<div v-if="allCases.length" class="case-list-card">
<div
v-for="(c, i) in allCases"
:key="`${c.c_ah}-${i}`"
class="case-list-group"
>
<div
class="case-item"
:class="{ active: expandedIndex === i }"
@click="toggleCase(i)"
>
<span class="case-text">{{ caseListText(c) }}</span>
<span class="case-arrow">{{ expandedIndex === i ? '' : '>' }}</span>
</div>
<div v-if="expandedIndex === i" class="case-block">
<div class="case-info">
<div><span>案件类型:</span><span>{{ c.sectionLabel }}</span></div>
<div><span>诉讼地位:</span><span>{{ c.n_ssdw }}</span></div>
<div><span>所属地域:</span><span>{{ c.c_ssdy }}</span></div>
<div><span>当事人:</span><span>{{ partiesText(c.c_dsrxx) }}</span></div>
<div><span>审理程序:</span><span>{{ c.n_slcx }}</span></div>
<div><span>立案时间:</span><span>{{ c.d_larq }}</span></div>
<div><span>结束时间:</span><span>{{ c.d_jarq }}</span></div>
<div><span>立案案由详情:</span><span>{{ c.n_laay_tree }}</span></div>
<div><span>结案方式:</span><span>{{ c.n_jafs }}</span></div>
<div><span>经办法院:</span><span>{{ c.n_jbfy }}</span></div>
</div>
<div v-if="c.c_gkws_pjjg" class="case-judgment gamma-small">
判决结果<br>{{ c.c_gkws_pjjg }}
</div>
</div>
</div>
</div>
<!-- 失信被执行人 -->
<div class="module-card">
<div class="module-title title-dishonest">失信被执行人</div>
<template v-if="dishonestList.length">
<div class="case-list-card inner-list">
<div v-for="(item, i) in dishonestList" :key="i" class="case-item static">
<span class="case-text">{{ dishonestText(item) }}</span>
</div>
</div>
</template>
<div v-else class="empty-data">
<svg class="empty-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 7C3 5.89543 3.89543 5 5 5H19C20.1046 5 21 5.89543 21 7V17C21 18.1046 20.1046 19 19 19H5C3.89543 19 3 18.1046 3 17V7Z" stroke="#999" stroke-width="1.5" />
<path d="M3 7L12 12L21 7" stroke="#999" stroke-width="1.5" stroke-linecap="round" />
</svg>
<span>暂无数据</span>
</div>
</div>
<!-- 限高被执行人 -->
<div class="module-card">
<div class="module-title title-high-limit">限高被执行人</div>
<table class="data-table">
<thead>
<tr>
<th>案号</th>
<th>企业法人</th>
<th>企业名称</th>
<th>执行法院</th>
<th>发布时间日期</th>
<th>立案时间日期</th>
</tr>
</thead>
<tbody>
<template v-if="limitHighList.length">
<tr v-for="(item, i) in limitHighRows" :key="i">
<td>{{ item.ah }}</td>
<td>{{ item.legalPerson }}</td>
<td>{{ item.entName }}</td>
<td>{{ item.court }}</td>
<td>{{ item.publishDate }}</td>
<td>{{ item.filingDate }}</td>
</tr>
</template>
<tr v-else>
<td colspan="6">
<div class="empty-data">
<svg class="empty-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 7C3 5.89543 3.89543 5 5 5H19C20.1046 5 21 5.89543 21 7V17C21 18.1046 20.1046 19 19 19H5C3.89543 19 3 18.1046 3 17V7Z" stroke="#999" stroke-width="1.5" />
<path d="M3 7L12 12L21 7" stroke="#999" stroke-width="1.5" stroke-linecap="round" />
</svg>
<span>暂无数据</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 报告使用须知 -->
<div class="notice-card">
<div class="notice-title">报告使用须知</div>
<ol class="notice-list">
<li v-for="(line, i) in REPORT_USAGE_NOTICE" :key="i">{{ line }}</li>
</ol>
</div>
</div>
</template>
<script setup>
import { computed, ref } from 'vue';
import {
parseStatDistribution,
buildConicGradient,
toCaseArray,
extractJudicialList,
REPORT_USAGE_NOTICE,
caseListText,
limitHighRow,
} from '../reportHelper';
const props = defineProps({
data: { type: Object, default: () => ({}) },
});
const PIE_COLORS = ['#4E74F6', '#89d079', '#f6c358', '#ff7c7c', '#9b59b6', '#5dade2'];
const pieColors = PIE_COLORS;
const count = computed(() => props.data.count || {});
const caseTypeCards = computed(() => [
{ key: 'administrative', label: '行政案件', count: toCaseArray(props.data.administrative).length },
{ key: 'civil', label: '民事案件', count: toCaseArray(props.data.civil).length },
{ key: 'criminal', label: '刑事案件', count: toCaseArray(props.data.criminal).length },
{ key: 'implement', label: '执行案件', count: toCaseArray(props.data.implement).length },
{ key: 'preservation', label: '非诉保全审查', count: toCaseArray(props.data.preservation).length },
{ key: 'bankrupt', label: '强制清算与破产案件', count: toCaseArray(props.data.bankrupt).length },
]);
const implementCount = computed(() => toCaseArray(props.data.implement).length);
const defendantBarWidth = computed(() => {
const total = Number(count.value.count_beigao) || 0;
const jie = Number(count.value.count_jie_beigao) || 0;
if (!total) return '0%';
return `${Math.round((jie / total) * 100)}%`;
});
const recentYearText = computed(() => {
const stat = count.value.larq_stat || '';
const items = parseStatDistribution(stat);
if (!items.length) return '—';
const latest = items.reduce((a, b) => (a.label > b.label ? a : b));
return `${latest.label}(${latest.count})`;
});
const roleBarRows = computed(() => {
const c = count.value;
const rows = [
{ label: '被告', count: Number(c.count_beigao) || 0 },
{ label: '原告', count: Number(c.count_yuangao) || 0 },
{ label: '第三人', count: Number(c.count_other) || 0 },
];
const max = Math.max(...rows.map((r) => r.count), 1);
return rows.map((r) => ({
...r,
width: `${Math.round((r.count / max) * 100)}%`,
}));
});
const axisTicks = computed(() => {
const max = Math.max(...roleBarRows.value.map((r) => r.count), 2);
const step = max <= 2 ? 0.5 : 1;
const ticks = [];
for (let i = 0; i <= max; i += step) {
ticks.push(Number.isInteger(i) ? i : i.toFixed(1));
}
return ticks.length > 1 ? ticks : [0, 0.5, 1, 1.5, 2, 2.5];
});
const amountCards = computed(() => {
const c = count.value;
return [
{
title: '汇总',
icon: '📊',
total: c.money_total ?? 0,
jie: c.money_jie_total ?? 0,
wei: c.money_wei_total ?? 0,
},
{
title: '被告',
icon: '👤',
total: c.money_beigao ?? 0,
jie: c.money_jie_beigao ?? 0,
wei: c.money_wei_beigao ?? 0,
},
{
title: '原告',
icon: '👤',
total: c.money_yuangao ?? 0,
jie: c.money_jie_yuangao ?? 0,
wei: c.money_wei_yuangao ?? 0,
},
{
title: '第三人',
icon: '👤',
total: c.money_other ?? 0,
jie: c.money_jie_other ?? 0,
wei: c.money_wei_other ?? 0,
},
];
});
const pieCharts = computed(() => {
const c = count.value;
return [
{ title: '地点分布', items: parseStatDistribution(c.area_stat), gradient: '' },
{ title: '案由分布', items: parseStatDistribution(c.ay_stat), gradient: '' },
{ title: '结案分布', items: parseStatDistribution(c.jafs_stat), gradient: '' },
{ title: '时间分布', items: parseStatDistribution(c.larq_stat), gradient: '' },
].map((pie) => ({
...pie,
gradient: buildConicGradient(pie.items),
}));
});
const caseSections = computed(() => {
const sections = [
{ key: 'criminal', label: '刑事案件', items: toCaseArray(props.data.criminal) },
{ key: 'civil', label: '民事案件', items: toCaseArray(props.data.civil) },
{ key: 'administrative', label: '行政案件', items: toCaseArray(props.data.administrative) },
{ key: 'implement', label: '执行案件', items: toCaseArray(props.data.implement) },
];
return sections.filter((s) => s.items.length > 0);
});
const allCases = computed(() => {
const list = [];
for (const section of caseSections.value) {
for (const c of section.items) {
list.push({ ...c, sectionLabel: section.label });
}
}
return list;
});
const expandedIndex = ref(null);
function toggleCase(index) {
expandedIndex.value = expandedIndex.value === index ? null : index;
}
const dishonestList = computed(() =>
extractJudicialList(props.data, [
'dishonest',
'sxbzxr',
'breachCase',
'disinCases',
'dishonestExecutor',
]),
);
const limitHighList = computed(() =>
extractJudicialList(props.data, [
'limitHigh',
'xgbzxr',
'consumptionRestriction',
'limitCases',
'limitExecutor',
]),
);
const limitHighRows = computed(() => limitHighList.value.map(limitHighRow));
function dishonestText(item) {
const ah = item.c_ah || item.ah || item.caseCode || '—';
const court = item.n_jbfy || item.court || item.zxfy || '';
return court ? `${ah}${court}` : ah;
}
function partiesText(dsrxx) {
if (!Array.isArray(dsrxx)) return '—';
return dsrxx.map((d) => `${d.c_mc}${d.n_dsrlx}`).join('; ');
}
</script>
<style lang="scss" scoped>
.judicial-section {
--blue: #4e74f6;
--red: #ff4d4f;
--border: #e8eef7;
}
.block-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 20px 0 16px;
display: flex;
align-items: center;
gap: 6px;
&::before {
content: '';
display: inline-block;
width: 16px;
height: 16px;
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%234E74F6"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/></svg>') no-repeat center;
background-size: contain;
}
}
.case-overview {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.overview-card {
background: #fff;
border-radius: 8px;
padding: 20px 12px;
text-align: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
position: relative;
border: 1px solid var(--border);
&.criminal {
background: #fff5f5;
border-color: #ffd0d0;
.card-num { color: var(--red); }
}
}
.badge {
position: absolute;
top: 8px;
right: 8px;
width: 16px;
height: 16px;
background: var(--red);
color: #fff;
border-radius: 50%;
font-size: 10px;
display: flex;
align-items: center;
justify-content: center;
}
.card-title {
font-size: 14px;
color: #666;
margin-bottom: 12px;
}
.card-num {
font-size: 28px;
font-weight: 700;
color: var(--blue);
}
.risk-section {
background: #fff;
border-radius: 8px;
padding: 20px;
margin-bottom: 30px;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
border: 1px solid var(--border);
}
.risk-item .label {
font-size: 13px;
color: #666;
margin-bottom: 8px;
line-height: 1.5;
}
.bar-wrap {
width: 100%;
height: 8px;
background: var(--border);
border-radius: 4px;
overflow: hidden;
margin-bottom: 4px;
}
.bar-fill {
height: 100%;
background: var(--blue);
border-radius: 4px;
min-width: 0;
}
.legend {
display: flex;
gap: 12px;
font-size: 12px;
color: #888;
margin-top: 4px;
flex-wrap: wrap;
}
.legend span::before {
content: '';
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 4px;
}
.legend .unresolved::before { background: var(--red); }
.legend .resolved::before { background: var(--blue); }
.total-hint {
text-align: right;
font-size: 12px;
color: #666;
margin-top: 4px;
}
.num-row {
display: flex;
align-items: center;
gap: 6px;
font-size: 16px;
font-weight: 600;
color: #333;
&.red { color: var(--red); }
}
.icon-yen { color: var(--blue); font-size: 18px; font-style: normal; }
.chart-title {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
color: #333;
margin-bottom: 16px;
&::before {
content: '';
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--blue);
border: 2px solid var(--border);
}
}
.bar-chart {
background: #fff;
border-radius: 8px;
padding: 20px;
margin-bottom: 24px;
border: 1px solid var(--border);
position: relative;
}
.chart-legend {
display: flex;
gap: 16px;
flex-wrap: wrap;
margin-bottom: 24px;
}
.chart-legend > span {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
padding: 2px 8px;
border-radius: 4px;
}
.chart-legend .unresolved {
background: #ff4d4f20;
color: var(--red);
}
.chart-legend .resolved {
background: #4e74f620;
color: var(--blue);
}
.chart-legend em {
font-style: normal;
padding: 0 6px;
border-radius: 4px;
color: #fff;
}
.chart-legend .unresolved em { background: var(--red); }
.chart-legend .resolved em { background: var(--blue); }
.bar-rows {
display: flex;
flex-direction: column;
gap: 20px;
}
.bar-row {
display: flex;
align-items: center;
gap: 12px;
}
.row-label {
width: 60px;
font-size: 12px;
color: #666;
text-align: right;
flex-shrink: 0;
}
.row-bar-track {
flex: 1;
height: 8px;
background: var(--border);
border-radius: 4px;
overflow: hidden;
}
.row-bar {
height: 100%;
background: var(--blue);
border-radius: 4px;
min-width: 0;
position: relative;
&::after {
content: '';
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
width: 8px;
height: 8px;
background: var(--blue);
border-radius: 50%;
}
}
.row-value {
width: 24px;
font-size: 12px;
color: #666;
text-align: right;
}
.axis {
display: flex;
justify-content: space-between;
margin-top: 8px;
padding: 0 calc(60px + 36px) 0 calc(60px + 12px);
font-size: 11px;
color: #999;
}
.amount-section {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-bottom: 30px;
}
.amount-card {
background: #fff;
border-radius: 8px;
padding: 16px;
border: 1px solid var(--border);
}
.amount-card .card-header {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 12px;
}
.amount-card .row {
display: flex;
justify-content: space-between;
font-size: 13px;
color: #666;
margin-bottom: 8px;
}
.amount-card .value {
color: #333;
font-weight: 500;
}
.pie-section {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-bottom: 24px;
}
.pie-card {
background: #fff;
border-radius: 8px;
padding: 20px;
border: 1px solid var(--border);
display: flex;
flex-direction: column;
align-items: center;
}
.pie-container {
width: 120px;
height: 120px;
}
.pie-circle {
width: 100%;
height: 100%;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
&.simple { background: var(--blue) !important; }
}
.pie-circle .inner {
width: 80px;
height: 80px;
background: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 500;
color: #333;
text-align: center;
line-height: 1.4;
}
.pie-legend {
margin-top: 12px;
width: 100%;
}
.pie-legend-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: #666;
margin-bottom: 4px;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.case-list-card {
background: #fff;
border-radius: 8px;
margin: 24px 0 20px;
overflow: hidden;
border: 1px solid var(--border);
}
.case-list-card.inner-list {
margin: 0;
border: none;
}
.case-list-group {
border-bottom: 1px solid #e8edf5;
&:last-child { border-bottom: none; }
}
.case-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 20px;
background: #f0f7ff;
border-bottom: 1px solid #e8edf5;
cursor: pointer;
user-select: none;
&::before {
content: '';
display: inline-block;
width: 3px;
height: 16px;
background: #4080ff;
margin-right: 10px;
flex-shrink: 0;
}
&.static { cursor: default; }
&.active { background: #e6f0ff; }
}
.case-text {
flex: 1;
font-size: 14px;
color: #333;
}
.case-arrow {
color: #999;
font-size: 16px;
margin-left: 8px;
}
.case-block {
padding: 16px 20px 16px 33px;
background: #fafbfc;
border-top: 1px dashed #e8edf5;
}
.case-info {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.case-info div {
display: flex;
justify-content: space-between;
padding: 6px 0;
border-bottom: 1px dashed #eee;
gap: 8px;
font-size: 13px;
}
.case-judgment {
margin-top: 10px;
line-height: 1.6;
color: #666;
}
.module-card {
background: #fff;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
border: 1px solid var(--border);
}
.module-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 20px;
display: flex;
align-items: center;
&::before {
content: '';
display: inline-block;
width: 18px;
height: 18px;
margin-right: 8px;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
opacity: 0.7;
}
}
.title-dishonest::before {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23666"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>');
}
.title-high-limit::before {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23666"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-7 14h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>');
}
.empty-data {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 0;
color: #999;
font-size: 14px;
}
.empty-icon {
width: 48px;
height: 48px;
margin-bottom: 10px;
opacity: 0.5;
}
.data-table {
width: 100%;
border-collapse: collapse;
}
.data-table th {
background: #fff9e8;
padding: 12px 8px;
text-align: center;
font-size: 13px;
color: #666;
font-weight: 500;
}
.data-table td {
padding: 12px 8px;
text-align: center;
font-size: 13px;
color: #333;
border-top: 1px solid #f0f0f0;
}
.notice-card {
background: #fff;
border-radius: 8px;
padding: 20px;
border: 1px solid #e0e0e0;
margin-top: 4px;
}
.notice-title {
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 12px;
}
.notice-list {
list-style-type: decimal;
padding-left: 20px;
font-size: 13px;
color: #666;
line-height: 1.8;
}
@media (max-width: 1024px) {
.case-overview { grid-template-columns: repeat(3, 1fr); }
.risk-section { grid-template-columns: repeat(2, 1fr); }
.amount-section,
.pie-section { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 640px) {
.case-overview { grid-template-columns: repeat(2, 1fr); }
.risk-section,
.amount-section,
.pie-section { grid-template-columns: 1fr; }
.data-table {
display: block;
overflow-x: auto;
}
}
</style>

View File

@@ -0,0 +1,278 @@
<template>
<div class="intent-report">
<div class="section-title">借贷意向</div>
<p class="desc-text">
借贷意向数据覆盖大部分的金融机构机构类型包括银行改制机构小贷消费类分期现金类分期代偿类分期和非银其它
</p>
<div class="section-title">最终决策结果</div>
<div class="tab-box">
<div
class="tab-item"
:class="{ active: activeTab === 'decision' }"
@click="activeTab = 'decision'"
>
{{ data.Rule_final_decision || '最终决策结果' }}
</div>
<div
class="tab-item"
:class="{ active: activeTab === 'review' }"
@click="activeTab = 'review'"
>
复议
</div>
</div>
<p v-if="data.Rule_name_QJF045" class="rule-hint">
命中规则{{ data.Rule_name_QJF045 }}
<span v-if="data.Rule_final_weight">权重 {{ data.Rule_final_weight }}</span>
</p>
<!-- 本人在本机构借贷意向表现 -->
<div class="section-title">本人在本机构借贷意向表现</div>
<table class="intent-table">
<thead>
<tr>
<th class="left-align">申请次数</th>
<th v-for="p in LOAN_INTENT_PERIOD_PREFIXES" :key="p.prefix">{{ p.label }}</th>
</tr>
</thead>
<tbody>
<tr v-for="row in LOAN_INTENT_INST_ROWS" :key="row.label">
<td class="left-align">{{ row.label }}</td>
<td v-for="p in LOAN_INTENT_PERIOD_PREFIXES" :key="`${row.label}-${p.prefix}`">
{{ formatAlsCount(data, p.prefix, row.suffix) }}
</td>
</tr>
</tbody>
</table>
<div class="note-text" v-html="LOAN_INTENT_NOTE" />
<!-- 本人在各个客户类型借贷意向表现 -->
<div class="section-title">本人在各个客户类型借贷意向表现</div>
<table class="intent-table">
<thead>
<tr>
<th class="left-align" rowspan="2">客户类型</th>
<th v-for="p in LOAN_INTENT_PERIOD_PREFIXES" :key="'h1-' + p.prefix" colspan="2">{{ p.label }}</th>
</tr>
<tr>
<template v-for="p in LOAN_INTENT_PERIOD_PREFIXES" :key="'h2-' + p.prefix">
<th>机构数</th>
<th>次数</th>
</template>
</tr>
</thead>
<tbody>
<tr
v-for="row in LOAN_INTENT_CUSTOMER_ROWS"
:key="row.label"
:class="{ 'light-bg': row.highlight }"
>
<td class="left-align">{{ row.label }}</td>
<template v-for="p in LOAN_INTENT_PERIOD_PREFIXES" :key="`${row.label}-${p.prefix}`">
<td>{{ formatAlsOrg(data, p.prefix, row.suffix) }}</td>
<td>{{ formatAlsCount(data, p.prefix, row.suffix) }}</td>
</template>
</tr>
</tbody>
</table>
<div class="note-text" v-html="LOAN_INTENT_NOTE" />
<!-- 本人在各个业务类型借贷意向表现 -->
<div class="section-title">本人在各个业务类型借贷意向表现</div>
<table class="intent-table">
<thead>
<tr>
<th class="left-align" rowspan="2">业务类型</th>
<th v-for="p in LOAN_INTENT_PERIOD_PREFIXES" :key="'b1-' + p.prefix" colspan="2">{{ p.label }}</th>
</tr>
<tr>
<template v-for="p in LOAN_INTENT_PERIOD_PREFIXES" :key="'b2-' + p.prefix">
<th>机构数</th>
<th>次数</th>
</template>
</tr>
</thead>
<tbody>
<tr v-for="row in LOAN_INTENT_BUSINESS_ROWS" :key="row.label">
<td class="left-align">{{ row.label }}</td>
<template v-for="p in LOAN_INTENT_PERIOD_PREFIXES" :key="`${row.label}-${p.prefix}`">
<td>{{ formatAlsOrg(data, p.prefix, row.suffix) }}</td>
<td>{{ formatAlsCount(data, p.prefix, row.suffix) }}</td>
</template>
</tr>
</tbody>
</table>
<div class="note-text" v-html="LOAN_INTENT_NOTE" />
<!-- 本人在异常时间段借贷意向表现 -->
<div class="section-title">本人在异常时间段借贷意向表现</div>
<table class="intent-table">
<thead>
<tr>
<th class="left-align" rowspan="2">时间-机构</th>
<th v-for="p in LOAN_INTENT_PERIOD_PREFIXES" :key="'a1-' + p.prefix" colspan="2">{{ p.label }}</th>
</tr>
<tr>
<template v-for="p in LOAN_INTENT_PERIOD_PREFIXES" :key="'a2-' + p.prefix">
<th>机构数</th>
<th>次数</th>
</template>
</tr>
</thead>
<tbody>
<tr v-for="row in LOAN_INTENT_ABNORMAL_ROWS" :key="row.label">
<td class="left-align">{{ row.label }}</td>
<template v-for="p in LOAN_INTENT_PERIOD_PREFIXES" :key="`${row.label}-${p.prefix}`">
<td>{{ formatAlsOrg(data, p.prefix, row.suffix) }}</td>
<td>{{ formatAlsCount(data, p.prefix, row.suffix) }}</td>
</template>
</tr>
</tbody>
</table>
<div class="note-text" v-html="LOAN_INTENT_NOTE" />
</div>
</template>
<script setup>
import { ref } from 'vue';
import {
LOAN_INTENT_PERIOD_PREFIXES,
LOAN_INTENT_INST_ROWS,
LOAN_INTENT_CUSTOMER_ROWS,
LOAN_INTENT_BUSINESS_ROWS,
LOAN_INTENT_ABNORMAL_ROWS,
LOAN_INTENT_NOTE,
formatAlsOrg,
formatAlsCount,
} from '../reportHelper';
defineProps({
data: { type: Object, default: () => ({}) },
});
const activeTab = ref('review');
</script>
<style lang="scss" scoped>
.intent-report {
background: #fff;
border-radius: 8px;
padding: 24px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.section-title {
font-size: 18px;
font-weight: bold;
margin: 30px 0 16px;
padding-left: 10px;
border-left: 4px solid #d4af37;
display: flex;
align-items: center;
gap: 8px;
&:first-child {
margin-top: 0;
}
&::before {
content: '';
display: inline-block;
width: 20px;
height: 16px;
background: linear-gradient(135deg, #d4af37 30%, transparent 30%, transparent 60%, #d4af37 60%);
}
}
.desc-text {
color: #666;
font-size: 15px;
margin: 8px 0 16px;
line-height: 1.6;
padding-left: 14px;
border-left: 2px solid #d4af37;
}
.tab-box {
display: inline-flex;
margin: 10px 0;
}
.tab-item {
padding: 8px 24px;
border: 1px solid #eee;
border-right: none;
background: #f5f7fa;
font-size: 15px;
cursor: pointer;
user-select: none;
&.active {
background: #fff;
font-weight: bold;
}
&:last-child {
border-right: 1px solid #eee;
border-radius: 0 4px 4px 0;
}
&:first-child {
border-radius: 4px 0 0 4px;
}
}
.rule-hint {
font-size: 13px;
color: #d89614;
margin: 4px 0 12px;
padding-left: 14px;
}
.intent-table {
width: 100%;
border-collapse: collapse;
margin: 16px 0;
font-size: 14px;
th,
td {
border: 1px solid #eee;
padding: 12px 8px;
text-align: center;
}
th {
background-color: #fdf8e9;
font-weight: 500;
}
}
.left-align {
text-align: left !important;
}
.light-bg {
background-color: #f0f5ff;
}
.note-text {
font-size: 13px;
color: #666;
margin: 8px 0 16px;
line-height: 1.6;
}
@media (max-width: 768px) {
.intent-report {
padding: 16px;
overflow-x: auto;
}
.intent-table {
min-width: 900px;
}
}
</style>

View File

@@ -0,0 +1,80 @@
<template>
<div class="gamma-card">
<div class="gamma-title"><span>🏦</span> 借贷画像</div>
<div class="loan-profile-grid">
<div v-for="item in items" :key="item.label" class="loan-item">
<div class="loan-icon" :class="item.color">{{ item.icon }}</div>
<div>
<div class="loan-text">{{ item.label }}</div>
<div class="loan-value">{{ item.value }}</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { cellText } from '../reportHelper';
const props = defineProps({
loanTotal: { type: Object, default: () => ({}) },
});
const items = computed(() => {
const d = props.loanTotal || {};
return [
{ label: '机构查询总次数', value: cellText(d.queryCount), icon: '📑', color: 'purple' },
{ label: '借贷机构数2年内', value: cellText(d.orgCount), icon: '🏢', color: 'green' },
{ label: '正常还款订单占比', value: cellText(d.repayRate), icon: '🤲', color: 'orange' },
{ label: '预估网贷授信额度', value: cellText(d.creditLimit), icon: '📄', color: 'pink' },
{ label: '网贷产品数', value: cellText(d.productCount), icon: '📦', color: 'red' },
{ label: '已结清订单数', value: cellText(d.settledCount), icon: '👤', color: 'blue' },
{ label: '借款总金额2年内', value: d.loanAmount != null ? `${d.loanAmount}` : '— 元', icon: '💴', color: 'orange' },
{ label: '逾期总金额2年内', value: cellText(d.overdueAmount), icon: '📄', color: 'pink' },
];
});
</script>
<style lang="scss" scoped>
.loan-profile-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
.loan-item {
background: #f9fafc;
border-radius: 6px;
padding: 16px;
display: flex;
align-items: center;
gap: 10px;
}
.loan-icon {
width: 36px;
height: 36px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 18px;
flex-shrink: 0;
}
.loan-icon.purple { background: #6c5ce7; }
.loan-icon.green { background: #00b894; }
.loan-icon.orange { background: #fdcb6e; }
.loan-icon.pink { background: #e84393; }
.loan-icon.red { background: #e74c3c; }
.loan-icon.blue { background: #3498db; }
.loan-text { font-size: 14px; line-height: 1.4; }
.loan-value { font-size: 16px; font-weight: 600; margin-top: 4px; }
@media (max-width: 768px) {
.loan-profile-grid { grid-template-columns: repeat(2, 1fr); }
}
</style>

View File

@@ -0,0 +1,83 @@
<template>
<div class="gamma-card">
<div class="gamma-title"><span>🚫</span> 逾期黑名单</div>
<div class="gamma-subtitle"><span></span> 命中结果</div>
<div class="blacklist-row">
<div class="blacklist-label">是否命中黑名单</div>
<div class="blacklist-value">{{ isHit(data.black_list) ? '是' : '否' }}</div>
</div>
<div class="gamma-subtitle"><span></span> 疑似标签</div>
<div class="tags-grid">
<div v-for="tag in BLACKLIST_TAGS" :key="tag.key" class="tag-item">
<div class="tag-label">{{ tag.label }}</div>
<div class="tag-value">{{ isHit(data[tag.key]) ? '命中' : '未命中' }}</div>
</div>
</div>
</div>
</template>
<script setup>
import { BLACKLIST_TAGS, isHit } from '../reportHelper';
defineProps({
data: { type: Object, default: () => ({}) },
});
</script>
<style lang="scss" scoped>
.blacklist-row {
display: flex;
align-items: center;
max-width: 400px;
margin-bottom: 16px;
}
.blacklist-label {
width: 160px;
font-size: 14px;
color: #666;
background: #f5f7fa;
padding: 8px 12px;
border-radius: 4px 0 0 4px;
}
.blacklist-value {
flex: 1;
padding: 8px 12px;
border: 1px solid #eee;
border-left: none;
border-radius: 0 4px 4px 0;
text-align: center;
}
.tags-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.tag-item { display: flex; align-items: center; }
.tag-label {
width: 200px;
font-size: 14px;
color: #666;
background: #f9fafc;
padding: 8px 12px;
border-radius: 4px 0 0 4px;
}
.tag-value {
flex: 1;
padding: 8px 12px;
border: 1px solid #eee;
border-left: none;
border-radius: 0 4px 4px 0;
text-align: center;
}
@media (max-width: 768px) {
.tags-grid { grid-template-columns: 1fr; }
.tag-label { width: 140px; }
}
</style>

View File

@@ -0,0 +1,28 @@
<template>
<div class="gamma-card">
<div class="gamma-title"><span>🏠</span> 逾期勘测V3</div>
<div class="gamma-grid">
<div v-for="field in fields" :key="field.key" class="gamma-item">
<label>{{ field.label }}</label>
<span>{{ field.display }}</span>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { OVERDUE_SURVEY_FIELDS, cellText, overdueResultText } from '../reportHelper';
const props = defineProps({
data: { type: Object, default: () => ({}) },
});
const fields = computed(() =>
OVERDUE_SURVEY_FIELDS.map((f) => {
let display = cellText(props.data[f.key]);
if (f.key === 'result_code') display = overdueResultText(props.data[f.key]);
return { ...f, display };
}),
);
</script>

View File

@@ -0,0 +1,148 @@
<template>
<div class="gamma-card risk-assessment">
<div class="gamma-grid-2" style="grid-template-columns: 1fr 2fr; gap: 24px;">
<div class="risk-card">
<div class="gamma-title"><span>🛡</span> 风险评估</div>
<div class="risk-desc">
说明:<br>
<span v-for="item in RISK_LEVEL_DESC" :key="item.level">
{{ item.level }}{{ item.label }}: {{ item.desc }}<br>
</span>
</div>
<div class="risk-level-circle">
<div class="circle" :style="{ borderTopColor: levelColor }">
<div class="circle-text">{{ riskLevel }}</div>
</div>
<div class="risk-level-text">风险等级</div>
<div class="risk-stars">{{ riskStars }}</div>
</div>
</div>
<div class="risk-card index-group">
<div class="index-item">
<div class="index-title">信用风险指数</div>
<div class="index-circle">
<div class="index-status">{{ riskScoreDisplay }}</div>
<div class="gamma-small">{{ riskScoreUnit }}</div>
</div>
<div class="index-desc">信用风险评分0-1000分数越高用户信用越高</div>
</div>
<div class="index-item">
<div class="index-title">履约金额综合指数</div>
<div class="index-circle">
<div class="index-status">{{ amountIndexDisplay.value }}</div>
<div class="gamma-small">{{ amountIndexDisplay.unit }}</div>
</div>
<div class="index-desc">履约金额综合指数0-1000指数越大用户逾期可能性越低</div>
</div>
<div class="index-item">
<div class="index-title">履约笔数综合指数</div>
<div class="index-circle">
<div class="index-status">{{ countIndexDisplay.value }}</div>
<div class="gamma-small">{{ countIndexDisplay.unit }}</div>
</div>
<div class="index-desc">履约笔数综合指数0-1000指数越大用户逾期可能性越低</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { RISK_LEVEL_DESC, riskLevelColor, formatComplianceIndex, riskLevelStars } from '../reportHelper';
const props = defineProps({
riskLevel: { type: String, default: '—' },
riskScore: { type: [String, Number], default: '—' },
amountIndex: { type: [String, Number], default: '' },
countIndex: { type: [String, Number], default: '' },
});
const levelColor = computed(() => riskLevelColor(props.riskLevel));
const riskStars = computed(() => riskLevelStars(props.riskLevel));
const riskScoreDisplay = computed(() => {
if (props.riskScore === null || props.riskScore === undefined || props.riskScore === '') {
return '未命中';
}
return String(props.riskScore);
});
const riskScoreUnit = computed(() => (riskScoreDisplay.value === '未命中' ? '未命中' : '分'));
const amountIndexDisplay = computed(() => formatComplianceIndex(props.amountIndex));
const countIndexDisplay = computed(() => formatComplianceIndex(props.countIndex));
</script>
<style lang="scss" scoped>
.risk-assessment .risk-card {
background: #fff;
border-radius: 8px;
padding: 20px;
}
.risk-desc {
font-size: 13px;
color: #555;
line-height: 1.6;
}
.risk-level-circle {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 16px;
}
.circle {
width: 120px;
height: 120px;
border-radius: 50%;
border: 4px solid #eee;
border-top-color: #ff3333;
display: flex;
align-items: center;
justify-content: center;
}
.circle-text {
font-size: 48px;
font-weight: bold;
}
.risk-level-text {
font-size: 14px;
color: #666;
margin-top: 8px;
}
.risk-stars {
margin-top: 4px;
letter-spacing: 2px;
color: #d4af37;
}
.index-group {
display: flex;
justify-content: space-around;
align-items: center;
}
.index-item { text-align: center; }
.index-circle {
width: 100px;
height: 100px;
border-radius: 50%;
border: 3px solid #eee;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 0 auto 8px;
}
.index-title { font-size: 14px; margin-bottom: 4px; }
.index-status { font-size: 18px; font-weight: 600; }
.index-desc { font-size: 12px; color: #666; margin-top: 4px; line-height: 1.4; max-width: 180px; }
</style>

View File

@@ -0,0 +1,70 @@
<template>
<div class="gamma-card">
<div class="gamma-title"><span>🏮</span> 风险汇总</div>
<div class="summary-grid">
<div v-for="cat in RISK_SUMMARY_CATEGORIES" :key="cat.key" class="summary-card">
<div class="summary-card-title">
<span>{{ cat.icon }}</span>
<span>{{ cat.title }}</span>
</div>
<div
v-for="(item, i) in (risks[cat.key] || [])"
:key="i"
class="summary-item"
>
{{ item }}
</div>
<div v-if="!(risks[cat.key] || []).length" class="summary-empty">无风险项</div>
</div>
</div>
</div>
</template>
<script setup>
import { RISK_SUMMARY_CATEGORIES } from '../reportHelper';
defineProps({
risks: { type: Object, default: () => ({}) },
});
</script>
<style lang="scss" scoped>
.summary-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
.summary-card {
border: 1px solid #eee;
border-radius: 6px;
padding: 12px;
}
.summary-card-title {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
}
.summary-item {
font-size: 12px;
color: #d93025;
background: #fef0f0;
padding: 4px 8px;
border-radius: 4px;
margin: 4px 0;
}
.summary-empty {
font-size: 12px;
color: #999;
}
@media (max-width: 768px) {
.summary-grid { grid-template-columns: repeat(2, 1fr); }
}
</style>

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,9 +14,9 @@
<OverdueRiskSection :overdue-risk-product="data.overdueRiskProduct" />
<!-- 法院曝光台信息 - 暂时隐藏 -->
<!-- <MultCourtInfoSection
<MultCourtInfoSection
:mult-court-info="data.multCourtInfo"
/> -->
/>
<!-- 借贷评估 -->
<LoanEvaluationSection :loan-evaluation-verification-detail="data.loanEvaluationVerificationDetail" />

309
src/ui/CFLX3A9B.vue Normal file
View File

@@ -0,0 +1,309 @@
<template>
<div class="card shadow-sm rounded-xl overflow-hidden p-4">
<div class="border border-[#EEEEEE] rounded-xl">
<!-- 标题 -->
<div class="flex items-center mb-3 p-4">
<div class="w-8 h-8 flex items-center justify-center mr-2">
<img src="@/assets/images/report/ssfxztgl.png" alt="限高/失信风险" class="w-8 h-8 object-contain" />
</div>
<span class="font-bold text-gray-800">限高 / 失信风险</span>
</div>
<!-- 风险整体概览 -->
<div class="px-4 mb-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
<div class="p-3 rounded-lg bg-[#EB3C3C1A] border border-[#EB3C3C4D]">
<div class="text-xs text-gray-600 mb-1">规则最终决策</div>
<div class="text-lg font-semibold" :class="finalDecision === 'Accept' ? 'text-green-600' : 'text-red-600'">
{{ finalDecisionText }}
</div>
<div v-if="finalWeight" class="text-xs text-gray-500 mt-1">
风险权重{{ finalWeight }}
</div>
</div>
<div class="p-3 rounded-lg bg-[#D6943E1A] border border-[#D6943E4D]">
<div class="text-xs text-gray-600 mb-1">失信记录</div>
<div class="text-lg font-semibold text-orange-600">
{{ sxList.length }}
</div>
<div class="text-xs text-gray-500 mt-1">
命中失信被执行人公告
</div>
</div>
<div class="p-3 rounded-lg bg-[#2B79EE1A] border border-[#2B79EE4D]">
<div class="text-xs text-gray-600 mb-1">限高记录</div>
<div class="text-lg font-semibold text-blue-600">
{{ xgList.length }}
</div>
<div class="text-xs text-gray-500 mt-1">
{{ recentLimitDesc }}
</div>
</div>
</div>
</div>
<!-- 失信记录 -->
<div v-if="sxList.length" class="px-4 mb-4">
<LTitle title="失信被执行人记录" />
<div class="space-y-3">
<div v-for="(item, index) in sxList" :key="'sx_' + index" class="case-wrapper">
<div class="bg-white rounded-xl overflow-hidden border px-4 pt-3 border-[#DDDDDD]">
<div class="cursor-pointer relative" @click="toggleExpand('sx', index)">
<div class="flex items-center">
<div class="font-bold text-base text-[#333333] mr-2">
{{ item.casecode || '暂无案号' }}
</div>
<span class="px-2 py-1 text-xs rounded-md font-medium bg-[#F9ECEC] text-[#EB3C3C]">
{{ item.datatype || '失信被执行人' }}
</span>
</div>
<div class="pb-2 text-sm">
<span class="text-[#666666]">立案</span>
<span class="text-[#333333]">{{ item.regdate || '-' }}</span>
</div>
<div class="flex items-center justify-between">
<span class="px-2 py-1 text-xs rounded-md font-medium bg-[#EB3C3C1A] text-[#EB3C3C]">
{{ item.signalDesc || '主体存在失信记录' }}
</span>
<div class="flex items-center text-xs text-gray-500">
<span class="mr-1">
{{ isExpanded('sx', index) ? '收起详情' : '展开详情' }}
</span>
<img src="@/assets/images/report/zk.png" alt="展开" class="w-4 h-4"
:class="{ 'rotate-180': isExpanded('sx', index) }" />
</div>
</div>
</div>
<div class="mt-3 overflow-hidden transition-all duration-300 ease-in-out" :class="{
'max-h-0 opacity-0': !isExpanded('sx', index),
'max-h-[500px] opacity-100': isExpanded('sx', index),
}">
<div class="border-t border-dashed border-gray-200 pt-3 pb-2 text-xs">
<div class="flex mb-1">
<span class="w-16 text-gray-500">被执行人</span>
<span class="flex-1 text-gray-800">
{{ item.iname || '-' }}
<span v-if="item.sexname || item.age" class="text-gray-500 ml-1">
{{ [item.sexname, item.age && item.age + ''].filter(Boolean).join('') }}
</span>
</span>
</div>
<div class="flex mb-1">
<span class="w-16 text-gray-500">地域</span>
<span class="flex-1 text-gray-800">{{ item.areaname || '-' }}</span>
</div>
<div class="flex mb-1">
<span class="w-16 text-gray-500">法院</span>
<span class="flex-1 text-gray-800">{{ item.courtname || '-' }}</span>
</div>
<div class="flex mb-1">
<span class="w-16 text-gray-500">执行依据</span>
<span class="flex-1 text-gray-800">{{ item.gistcid || '-' }}</span>
</div>
<div class="flex mb-1">
<span class="w-16 text-gray-500">立案时间</span>
<span class="flex-1 text-gray-800">{{ item.regdate || '-' }}</span>
</div>
<div class="flex mb-1">
<span class="w-16 text-gray-500">发布日期</span>
<span class="flex-1 text-gray-800">{{ item.publishdate || '-' }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 限高记录 -->
<div v-if="xgList.length" class="px-4 mb-4">
<LTitle title="限高被执行人记录" />
<div class="space-y-3">
<div v-for="(item, index) in xgList" :key="'xg_' + index" class="case-wrapper">
<div class="bg-white rounded-xl overflow-hidden border px-4 pt-3 border-[#DDDDDD]">
<div class="cursor-pointer relative" @click="toggleExpand('xg', index)">
<div class="flex items-center">
<div class="font-bold text-base text-[#333333] mr-2">
{{ item.casecode || '暂无案号' }}
</div>
<span class="px-2 py-1 text-xs rounded-md font-medium bg-[#F9ECEC] text-[#EB3C3C]">
{{ item.datatype || '限高被执行人' }}
</span>
</div>
<div class="pb-2 text-sm">
<span class="text-[#666666]">立案</span>
<span class="text-[#333333]">{{ item.regdate || '-' }}</span>
</div>
<div class="flex items-center justify-between">
<span class="px-2 py-1 text-xs rounded-md font-medium bg-[#D6943E1A] text-[#D6943E]">
{{ item.signalDesc || '主体被限制高消费' }}
</span>
<div class="flex items-center text-xs text-gray-500">
<span class="mr-1">
{{ isExpanded('xg', index) ? '收起详情' : '展开详情' }}
</span>
<img src="@/assets/images/report/zk.png" alt="展开" class="w-4 h-4"
:class="{ 'rotate-180': isExpanded('xg', index) }" />
</div>
</div>
</div>
<div class="mt-3 overflow-hidden transition-all duration-300 ease-in-out" :class="{
'max-h-0 opacity-0': !isExpanded('xg', index),
'max-h-[500px] opacity-100': isExpanded('xg', index),
}">
<div class="border-t border-dashed border-gray-200 pt-3 pb-2 text-xs">
<div class="flex mb-1">
<span class="w-16 text-gray-500">被执行人</span>
<span class="flex-1 text-gray-800">
{{ item.iname || '-' }}
<span v-if="item.sexname || item.age" class="text-gray-500 ml-1">
{{ [item.sexname, item.age && item.age + ''].filter(Boolean).join('') }}
</span>
</span>
</div>
<div class="flex mb-1">
<span class="w-16 text-gray-500">地域</span>
<span class="flex-1 text-gray-800">{{ item.areaname || '-' }}</span>
</div>
<div class="flex mb-1">
<span class="w-16 text-gray-500">法院</span>
<span class="flex-1 text-gray-800">{{ item.courtname || '-' }}</span>
</div>
<div class="flex mb-1">
<span class="w-16 text-gray-500">立案时间</span>
<span class="flex-1 text-gray-800">{{ item.regdate || '-' }}</span>
</div>
<div class="flex mb-1">
<span class="w-16 text-gray-500">发布日期</span>
<span class="flex-1 text-gray-800">{{ item.publishdate || '-' }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div v-if="!sxList.length && !xgList.length"
class="text-gray-500 py-10 text-center bg-gray-50 rounded-lg mx-4 mb-4">
<div class="text-gray-300 text-3xl mb-2"></div>
暂未命中失信或限高被执行人记录
</div>
</div>
</div>
</template>
<script setup>
import { computed, ref, watchEffect } from 'vue'
import LTitle from '@/components/LTitle.vue'
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 raw = computed(() => props.data || {})
const finalDecision = computed(() => raw.value.Rule_final_decision || '')
const finalWeight = computed(() => raw.value.Rule_final_weight || '')
const finalDecisionText = computed(() => {
const v = finalDecision.value
if (!v) return '未知'
if (v.toLowerCase() === 'accept') return '通过'
if (v.toLowerCase() === 'reject') return '拒绝'
return v
})
// 解析失信记录el_sx1_*
const sxList = computed(() => {
const d = raw.value
const list = []
if (d.el_sx1_datatype) {
list.push({
datatype: d.el_sx1_datatype,
age: d.el_sx1_age,
areaname: d.el_sx1_areaname,
casecode: d.el_sx1_casecode,
courtname: d.el_sx1_courtname,
gistcid: d.el_sx1_gistcid,
iname: d.el_sx1_iname,
partytypename: d.el_sx1_partytypename,
publishdate: d.el_sx1_publishdate,
regdate: d.el_sx1_regdate,
sexname: d.el_sx1_sexname,
sign: d.el_sx1_sign,
signalDesc: d.el_sx1_signalDesc,
signalRating: d.el_sx1_signalRating,
})
}
return list
})
// 解析限高记录el_xg1_* / el_xg2_* / el_xg3_*
const xgList = computed(() => {
const d = raw.value
const result = []
;[1, 2, 3].forEach((n) => {
const prefix = `el_xg${n}_`
const datatype = d[`${prefix}datatype`]
if (!datatype) return
result.push({
datatype,
age: d[`${prefix}age`],
areaname: d[`${prefix}areaname`],
casecode: d[`${prefix}casecode`],
courtname: d[`${prefix}courtname`],
iname: d[`${prefix}iname`],
publishdate: d[`${prefix}publishdate`],
regdate: d[`${prefix}regdate`],
sexname: d[`${prefix}sexname`],
sign: d[`${prefix}sign`],
signalDesc: d[`${prefix}signalDesc`],
signalRating: d[`${prefix}signalRating`],
})
})
return result
})
const recentLimitDesc = computed(() => {
if (!xgList.value.length) return '暂无限高记录'
const recent = xgList.value[0]
return recent.signalDesc || '主体存在限高被执行人记录'
})
// 展开状态
const expandedMap = ref({})
const keyOf = (type, index) => `${type}_${index}`
const toggleExpand = (type, index) => {
const k = keyOf(type, index)
expandedMap.value[k] = !expandedMap.value[k]
}
const isExpanded = (type, index) => !!expandedMap.value[keyOf(type, index)]
// 上报风险:只要有失信或限高记录即视为有风险
watchEffect(() => {
if (!props.notifyRiskStatus) return
const hasRisk = sxList.value.length > 0 || xgList.value.length > 0
props.notifyRiskStatus(props.apiId || 'FLXG3A9B', props.index || 0, {
hasRisk,
})
})
</script>
<style scoped>
.card {
padding: 0.5rem;
}
.case-wrapper {
width: 100%;
}
</style>

524
src/ui/CIVYZ0S0D.vue Normal file
View File

@@ -0,0 +1,524 @@
<script setup>
import { computed, ref } from 'vue';
import { useRiskNotifier } from '@/composables/useRiskNotifier';
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 periodTab = ref('threeYears'); // 近三年 | 近五年
// 支持 data 或 data.result接口返回的 result 对象)
const result = computed(() => props.data?.result ?? props.data ?? {});
const getStatusText = (value) => {
if (value === 1) return '未命中';
if (value === 2) return '命中';
return '—';
};
// 获取通知函期间描述文本(支持数字或字符串如 "2"
const getNoticeLetterPeriodText = (period) => {
const p = Number(period);
const periodMap = { 0: '没有被发送通知函', 1: '近2年内', 2: '2-4年', 3: '5年以上' };
return periodMap[p] ?? '—';
};
// 检查是否至少有一个数据类别有内容
const hasAnyData = computed(() => {
const r = result.value;
return Object.keys(r).length > 0;
});
// 汇总数据 - 按分类分组 { key, title, rows }
const summaryGroups = computed(() => {
const groups = [];
const basic = result.value.basic_info;
if (basic?.risk_flag !== undefined) {
groups.push({ key: 'basic', title: '基础风险', rows: [{ label: '该人员是否有风险', value: basic.risk_flag }] });
}
const dishonesty = result.value.dishonesty?.dishonesty;
const highConsumption = result.value.high_consumption?.high_consumption;
if (dishonesty !== undefined || highConsumption !== undefined) {
const rows = [];
if (dishonesty !== undefined) rows.push({ label: '失信人员风险', value: dishonesty });
if (highConsumption !== undefined) rows.push({ label: '限制高消费人员风险', value: highConsumption });
groups.push({ key: 'credit', title: '失信限高', rows });
}
const labor = result.value.labor_disputes;
if (labor) {
const items = [['劳动争议', labor.labor_disputes], ['劳动合同纠纷', labor.labor_contract], ['劳动关系纠纷', labor.labor_relation], ['追索劳动报酬纠纷', labor.wage_claim], ['经济补偿金纠纷', labor.compensation], ['集体合同纠纷', labor.collective_contract], ['劳务派遣合同纠纷', labor.dispatch_contract], ['非全日制用工纠纷', labor.part_time], ['竞业限制纠纷', labor.non_compete]];
const rows = items.filter((item) => item[1] !== undefined).map((item) => ({ label: item[0], value: item[1] }));
if (rows.length) groups.push({ key: 'labor', title: '劳动争议', rows });
}
const social = result.value.social_insurance;
if (social) {
const items = [['社会保险纠纷', social.social_insurance], ['养老保险待遇纠纷', social.pension], ['工伤保险待遇纠纷', social.injury_insurance], ['医疗保险待遇纠纷', social.medical_insurance], ['生育保险待遇纠纷', social.maternity_insurance], ['商业保险待遇纠纷', social.commercial_insurance]];
const rows = items.filter((item) => item[1] !== undefined).map((item) => ({ label: item[0], value: item[1] }));
if (rows.length) groups.push({ key: 'social', title: '社会保险', rows });
}
if (result.value.welfare_disputes?.welfare !== undefined) {
groups.push({ key: 'welfare', title: '福利待遇', rows: [{ label: '福利待遇纠纷', value: result.value.welfare_disputes.welfare }] });
}
const personnel = result.value.personnel_disputes;
if (personnel) {
const items = [['人事争议类纠纷', personnel.personnel_dispute], ['辞职争议纠纷', personnel.resignation_dispute], ['辞退争议纠纷', personnel.dismissal_dispute], ['聘用合同争议纠纷', personnel.employment_contract]];
const rows = items.filter((item) => item[1] !== undefined).map((item) => ({ label: item[0], value: item[1] }));
if (rows.length) groups.push({ key: 'personnel', title: '人事争议', rows });
}
const arb = result.value.arbitration;
if (arb && (arb.arbitration_confirmation !== undefined || arb.arbitration_revocation !== undefined)) {
const rows = [];
if (arb.arbitration_confirmation !== undefined) rows.push({ label: '申请仲裁确认', value: arb.arbitration_confirmation });
if (arb.arbitration_revocation !== undefined) rows.push({ label: '撤销仲裁裁决', value: arb.arbitration_revocation });
groups.push({ key: 'arbitration', title: '仲裁流程', rows });
}
const notice = result.value.notice_letter;
if (notice?.notice_letter !== undefined) {
const rows = [{ label: '通知函触达', value: notice.notice_letter }];
if (notice.notice_letter_period !== undefined && notice.notice_letter === 2) rows.push({ label: '通知函发送时间', value: null, period: notice.notice_letter_period });
groups.push({ key: 'notice', title: '通知函触达', rows });
}
return groups;
});
const summaryRows = computed(() => summaryGroups.value.flatMap((g) => g.rows));
// 真正的风险项(文档:失信限高、劳动争议、社会保险、福利待遇、人事争议、仲裁流程、通知函触达)
// 排除 basic_info.risk_flag汇总结论和 notice_letter_period非风险项
const riskItemRows = computed(() =>
summaryGroups.value
.filter((g) => g.key !== 'basic')
.flatMap((g) => g.rows)
.filter((r) => r.value === 1 || r.value === 2)
);
// 近三年/近五年 - 按分类分组
const periodGroups = computed(() => {
const suffix = periodTab.value === 'threeYears' ? '_3y' : '_5y';
const groups = [];
const labor = result.value.labor_disputes;
if (labor) {
const keys = ['labor_disputes', 'labor_relation', 'wage_claim', 'compensation', 'collective_contract', 'dispatch_contract', 'part_time', 'non_compete'];
const labels = ['劳动争议', '劳动关系', '追索劳动报酬', '经济补偿金', '集体合同', '劳务派遣', '非全日制用工', '竞业限制'];
const rows = keys.map((k, i) => ({ label: labels[i], value: labor[k + suffix] })).filter((r) => r.value === 2).map((r) => ({ label: r.label, value: 2 }));
if (rows.length) groups.push({ key: 'labor', title: '劳动争议', rows });
}
const social = result.value.social_insurance;
if (social) {
const keys = ['pension', 'injury_insurance', 'medical_insurance', 'maternity_insurance', 'commercial_insurance'];
const labels = ['养老保险', '工伤保险', '医疗保险', '生育保险', '商业保险'];
const rows = keys.map((k, i) => ({ label: labels[i], value: social[k + suffix] })).filter((r) => r.value === 2).map((r) => ({ label: r.label, value: 2 }));
if (rows.length) groups.push({ key: 'social', title: '社会保险', rows });
}
const personnel = result.value.personnel_disputes;
if (personnel) {
const keys = ['resignation_dispute', 'dismissal_dispute', 'employment_contract'];
const labels = ['辞职争议', '辞退争议', '聘用合同'];
const rows = keys.map((k, i) => ({ label: labels[i], value: personnel[k + suffix] })).filter((r) => r.value === 2).map((r) => ({ label: r.label, value: 2 }));
if (rows.length) groups.push({ key: 'personnel', title: '人事争议', rows });
}
const arb = result.value.arbitration;
if (arb) {
const rows = [];
if (arb[`arbitration_confirmation${suffix}`] === 2) rows.push({ label: '申请仲裁确认', value: 2 });
if (arb[`arbitration_revocation${suffix}`] === 2) rows.push({ label: '撤销仲裁裁决', value: 2 });
if (rows.length) groups.push({ key: 'arbitration', title: '仲裁流程', rows });
}
return groups;
});
const periodRows = computed(() => periodGroups.value.flatMap((g) => g.rows));
// 用于 riskScore需要近三年+近五年全部数据
const recentThreeYearsRows = computed(() => {
const r = result.value;
const rows = [];
const labor = r.labor_disputes;
if (labor) {
[['labor_disputes_3y'], ['labor_relation_3y'], ['wage_claim_3y'], ['compensation_3y'], ['collective_contract_3y'], ['dispatch_contract_3y'], ['part_time_3y'], ['non_compete_3y']].forEach(([k]) => { if (labor[k] === 2) rows.push({ value: 2 }); });
}
const social = r.social_insurance;
if (social) {
['pension_3y', 'injury_insurance_3y', 'medical_insurance_3y', 'maternity_insurance_3y', 'commercial_insurance_3y'].forEach((k) => { if (social[k] === 2) rows.push({ value: 2 }); });
}
const personnel = r.personnel_disputes;
if (personnel) {
['resignation_dispute_3y', 'dismissal_dispute_3y', 'employment_contract_3y'].forEach((k) => { if (personnel[k] === 2) rows.push({ value: 2 }); });
}
const arb = r.arbitration;
if (arb) {
if (arb.arbitration_confirmation_3y === 2) rows.push({ value: 2 });
if (arb.arbitration_revocation_3y === 2) rows.push({ value: 2 });
}
return rows;
});
const recentFiveYearsRows = computed(() => {
const r = result.value;
const rows = [];
const labor = r.labor_disputes;
if (labor) {
[['labor_disputes_5y'], ['labor_relation_5y'], ['wage_claim_5y'], ['compensation_5y'], ['collective_contract_5y'], ['dispatch_contract_5y'], ['part_time_5y'], ['non_compete_5y']].forEach(([k]) => { if (labor[k] === 2) rows.push({ value: 2 }); });
}
const social = r.social_insurance;
if (social) {
['pension_5y', 'injury_insurance_5y', 'medical_insurance_5y', 'maternity_insurance_5y', 'commercial_insurance_5y'].forEach((k) => { if (social[k] === 2) rows.push({ value: 2 }); });
}
const personnel = r.personnel_disputes;
if (personnel) {
['resignation_dispute_5y', 'dismissal_dispute_5y', 'employment_contract_5y'].forEach((k) => { if (personnel[k] === 2) rows.push({ value: 2 }); });
}
const arb = r.arbitration;
if (arb) {
if (arb.arbitration_confirmation_5y === 2) rows.push({ value: 2 });
if (arb.arbitration_revocation_5y === 2) rows.push({ value: 2 });
}
return rows;
});
// 头部风险总结:风险类型、建议、命中统计(仅真正的风险项)
const riskSummary = computed(() => {
const basic = result.value.basic_info;
const hasRisk = basic?.risk_flag === 2;
// 真正的风险项:总项数、命中数(文档:失信限高、劳动争议、社会保险、福利待遇、人事争议、仲裁流程、通知函触达)
const totalItems = riskItemRows.value.length;
const hitItems = riskItemRows.value.filter((r) => r.value === 2).length;
// 命中的风险分类(汇总中 value=2 的 group
const riskCategories = summaryGroups.value
.filter((g) => g.key !== 'basic' && g.rows.some((r) => r.value === 2))
.map((g) => g.title);
// 精简建议(取主要类型合并为一句)
const suggestionMap = {
'失信限高': '征信修复与限高事项',
'劳动争议': '劳动纠纷与薪酬离职',
'社会保险': '社保缴纳与补缴',
'福利待遇': '福利待遇合规',
'人事争议': '辞职辞退与聘用合同',
'仲裁流程': '仲裁案件进展',
'通知函触达': '仲裁调解涉诉通知',
};
const suggestionParts = riskCategories.map((c) => suggestionMap[c]).filter(Boolean);
const suggestion = suggestionParts.length ? `建议关注${suggestionParts.slice(0, 3).join('、')}` : '';
return {
hasRisk,
label: hasRisk ? '有风险' : '无风险',
riskCategories,
suggestion,
totalItems,
hitItems,
};
});
// 风险评分 0-100越高越安全供 BaseReport 分析指数)
// 基于真正的风险项riskItemRows与展示逻辑一致通过 useRiskNotifier 递交 BaseReport
const riskScore = computed(() => {
const basic = result.value.basic_info;
if (!basic || basic.risk_flag === 1) return 100;
const hitItems = riskItemRows.value.filter((r) => r.value === 2).length;
const score = 100 - hitItems * 2; // 每命中 1 项扣 2 分
return Math.max(0, Math.min(100, score));
});
useRiskNotifier(props, riskScore);
defineExpose({ riskScore });
// 借鉴司法涉诉概览:风险图标与背景样式
const getRiskIcon = () => {
if (riskSummary.value.hasRisk) return new URL('@/assets/images/report/gfx.png', import.meta.url).href;
return new URL('@/assets/images/report/zq.png', import.meta.url).href;
};
</script>
<template>
<div class="report-wrap">
<!-- 头部风险总结底色固定白色命中项用内嵌 card 展现 -->
<div v-if="hasAnyData" class="risk-summary card">
<div class="flex items-center mb-4">
<div class="w-12 h-12 mr-3 flex-shrink-0">
<img :src="getRiskIcon()" alt="风险" class="w-12 h-12 object-contain" />
</div>
<div class="text-gray-700 text-[15px] leading-relaxed">
<template v-if="riskSummary.hasRisk">
<span v-if="riskSummary.riskCategories.length">涉及{{ riskSummary.riskCategories.join('')
}}</span>
<span v-if="riskSummary.suggestion">{{ riskSummary.suggestion }}</span>
</template>
<template v-else>未检测到相关风险</template>
</div>
</div>
<!-- 命中项仅显示总项数 / 命中数真正的风险项失信限高劳动争议社会保险福利待遇人事争议仲裁流程通知函触达 -->
<div v-if="riskSummary.totalItems > 0" class="inner-card p-4 rounded-xl text-center"
:class="riskSummary.hitItems > 0 ? 'inner-card-risk' : 'inner-card-safe'">
<div class="text-2xl font-bold mb-1"
:class="riskSummary.hitItems > 0 ? 'text-[#EB3C3C]' : 'text-[#10b981]'">
{{ riskSummary.hitItems }}/{{ riskSummary.totalItems }}
</div>
<div class="text-sm font-medium text-gray-800">命中项</div>
</div>
</div>
<!-- Card 1: 汇总 -->
<section class="card">
<header class="card-header">
<span class="card-title">风险概览</span>
<span class="card-subtitle">综合评估结果</span>
</header>
<div v-if="hasAnyData" class="group-list">
<div v-for="(group, gi) in summaryGroups" :key="group.key" class="group-box">
<div class="group-header">
<span class="group-title">{{ group.title }}</span>
</div>
<div class="group-body">
<div v-for="(row, ri) in group.rows" :key="ri" class="data-row">
<span class="data-label">{{ row.label }}</span>
<span v-if="row.period !== undefined" class="data-value data-value-text">{{
getNoticeLetterPeriodText(row.period) }}</span>
<span v-else
:class="['data-value', 'data-badge', row.value === 2 ? 'badge-risk' : 'badge-safe']">
<span class="badge-dot" :class="row.value === 2 ? 'dot-risk' : 'dot-safe'" />
{{ getStatusText(row.value) }}
</span>
</div>
</div>
</div>
</div>
<div v-else class="empty-state">
<span class="empty-icon"></span>
<span class="empty-text">暂无相关风险数据</span>
</div>
</section>
<!-- Card 2: 近三年 / 近五年 -->
<section class="card">
<header class="card-header">
<span class="card-title">时间维度</span>
<span class="card-subtitle">按周期查看命中情况</span>
</header>
<div class="period-tabs">
<button type="button" :class="['period-tab', periodTab === 'threeYears' && 'active']"
@click="periodTab = 'threeYears'">近三年</button>
<button type="button" :class="['period-tab', periodTab === 'fiveYears' && 'active']"
@click="periodTab = 'fiveYears'">近五年</button>
</div>
<div v-if="periodGroups.length" class="group-list">
<div v-for="(group, gi) in periodGroups" :key="group.key" class="group-box">
<div class="group-header">
<span class="group-title">{{ group.title }}</span>
</div>
<div class="group-body">
<div v-for="(row, ri) in group.rows" :key="ri" class="data-row">
<span class="data-label">{{ row.label }}</span>
<span class="data-value data-badge badge-risk">
<span class="badge-dot dot-risk" />
{{ getStatusText(row.value) }}
</span>
</div>
</div>
</div>
</div>
<div v-else class="empty-state">
<span class="empty-icon"></span>
<span class="empty-text">暂无{{ periodTab === 'threeYears' ? '近三年' : '近五年' }}命中数据</span>
</div>
</section>
</div>
</template>
<style lang="scss" scoped>
.report-wrap {
display: flex;
flex-direction: column;
gap: 16px;
}
.card {
background: #fff;
border-radius: 12px;
padding: 16px;
border: 1px solid rgba(0, 0, 0, 0.06);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.inner-card {
border: 1px solid #e2e8f0;
}
.inner-card-risk {
background: rgba(235, 60, 60, 0.1);
border-color: rgba(235, 60, 60, 0.3);
}
.inner-card-safe {
background: rgba(16, 185, 129, 0.1);
border-color: rgba(16, 185, 129, 0.3);
}
.card-header {
display: flex;
flex-direction: column;
gap: 2px;
margin-bottom: 14px;
padding-bottom: 12px;
border-bottom: 1px solid #f1f5f9;
}
.card-title {
font-size: 16px;
font-weight: 600;
color: #1e293b;
letter-spacing: 0.02em;
}
.card-subtitle {
font-size: 13px;
color: #94a3b8;
}
.period-tabs {
display: flex;
gap: 8px;
margin-bottom: 14px;
padding: 4px;
background: #f8fafc;
border-radius: 10px;
}
.period-tab {
flex: 1;
padding: 10px 16px;
font-size: 15px;
color: #64748b;
background: transparent;
border: none;
border-radius: 8px;
cursor: pointer;
}
.period-tab.active {
color: #1e293b;
font-weight: 500;
background: #fff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
.group-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.group-box {
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 10px;
overflow: hidden;
}
.group-header {
padding: 12px 16px;
background: #f8fafc;
border-bottom: 1px solid #e2e8f0;
}
.group-title {
font-size: 16px;
font-weight: 600;
color: #475569;
letter-spacing: 0.03em;
}
.group-body {
display: flex;
flex-direction: column;
padding: 0 16px;
}
.data-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
font-size: 16px;
}
.data-row:not(:last-child) {
border-bottom: 1px solid #f1f5f9;
}
.data-label {
color: #475569;
flex: 1;
margin-right: 12px;
font-size: 16px;
line-height: 1.5;
}
.data-value {
flex-shrink: 0;
}
.data-value-text {
color: #64748b;
font-size: 15px;
}
.data-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 12px;
border-radius: 12px;
font-size: 14px;
font-weight: 500;
}
.badge-dot {
width: 6px;
height: 6px;
border-radius: 50%;
}
.badge-risk {
background: #fff1f2;
color: #be123c;
}
.dot-risk {
background: #e11d48;
}
.badge-safe {
background: #f0fdf4;
color: #047857;
}
.dot-safe {
background: #10b981;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 28px 16px;
}
.empty-icon {
font-size: 20px;
color: #cbd5e1;
font-weight: 300;
}
.empty-text {
font-size: 15px;
color: #94a3b8;
}
</style>

104
src/ui/CIVYZ6M8P.vue Normal file
View File

@@ -0,0 +1,104 @@
<template>
<div class="card">
<div class="header-box">
<h3 class="header-title">职业资格证书查询</h3>
<p class="header-desc">展示查询到的职业资格考试及等级信息</p>
</div>
<div v-if="hasData" class="result-section">
<div class="info-row">
<span class="info-label">考试名称</span>
<span class="info-value">{{ examName || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">级别</span>
<span class="info-value">{{ level || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">专业</span>
<span class="info-value">{{ major || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">通过日期</span>
<span class="info-value">{{ passDate || '-' }}</span>
</div>
</div>
<div v-if="hasData && !examName && !major && !level && !passDate" class="empty-tip">
已返回结果但格式不在预期范围内请联系技术支持查看原始数据
</div>
<div v-if="!hasData" class="empty-tip">暂无查询结果</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
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 hasData = computed(() => props.data && Object.keys(props.data).length > 0);
// ED0001: 考试名称, ED0002: 级别, ED0003: 专业, ED0004: 通过年月
const examName = computed(() => props.data?.ED0001 || '');
const level = computed(() => props.data?.ED0002 || '');
const major = computed(() => props.data?.ED0003 || '');
const passDate = computed(() => props.data?.ED0004 || '');
</script>
<style scoped>
.card {
padding: 1rem;
}
.header-box {
margin-bottom: 1rem;
}
.header-title {
font-size: 1.125rem;
font-weight: 600;
}
.header-desc {
font-size: 0.875rem;
color: #6b7280;
margin-top: 0.25rem;
}
.result-section {
background: #f9fafb;
padding: 0.75rem 0.875rem;
border-radius: 0.75rem;
}
.info-row {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
padding: 0.25rem 0;
}
.info-label {
color: #6b7280;
}
.info-value {
color: #111827;
margin-left: 1rem;
text-align: right;
}
.empty-tip {
color: #9ca3af;
font-size: 0.875rem;
padding: 1rem 0;
text-align: center;
}
</style>

296
src/ui/CIVYZ7F3A.vue Normal file
View File

@@ -0,0 +1,296 @@
<script setup>
import { computed, onMounted } 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: () => { },
},
});
// 导入图片图标
import rkpmIcon from '@/assets/images/report/rkpm.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 xlIcon from '@/assets/images/report/xl.png';
// 计算风险评分0-100分分数越高越安全
const riskScore = computed(() => {
// 学历信息不算风险始终返回100分最安全
return 100;
});
// 使用 composable 通知父组件风险评分
useRiskNotifier(props, riskScore);
// 暴露给父组件
defineExpose({
riskScore
});
// 格式化日期从YYYYMM格式转换为YYYY年MM月格式
const formatDate = (dateStr) => {
if (!dateStr || dateStr.length !== 6) return "未知";
const year = dateStr.substring(0, 4);
const month = dateStr.substring(4, 6);
return `${year}${month}`;
};
// 获取学历等级的描述
const getEducationLevelDesc = (level) => {
const descriptions = {
"专科": "专科学历是高等教育的重要组成部分,培养具有专业知识和技能的应用型人才。",
"本科": "本科学历是高等教育的基础学位,培养具有系统专业知识和基本技能的高级人才。",
"硕士": "硕士学位是较高层次的学位,培养具有较深厚理论基础和专业技能的高级专门人才。",
"博士": "博士学位是最高学位,培养能够独立从事科学研究工作、具有创新能力的高级专门人才。",
};
return descriptions[level] || "";
};
// 根据学校属性获取徽章配置
const getSchoolBadges = () => {
const badges = [];
if (props.data.isProject985 === "是") {
badges.push({
text: "985院校",
class: "bg-amber-500 text-white",
icon: "🏆"
});
}
if (props.data.isProject211 === "是") {
badges.push({
text: "211院校",
class: "bg-blue-500 text-white",
icon: "⭐"
});
}
if (props.data.isDoubleFirstClass === "是") {
badges.push({
text: "双一流",
class: "bg-green-500 text-white",
icon: "✨"
});
}
return badges;
};
// 获取学校类型徽章样式
const getSchoolTypeBadgeClass = (type) => {
const typeClasses = {
"综合": "bg-blue-50 text-blue-700 border-blue-300",
"理工": "bg-purple-50 text-purple-700 border-purple-300",
"农业": "bg-green-50 text-green-700 border-green-300",
"医学": "bg-red-50 text-red-700 border-red-300",
"师范": "bg-yellow-50 text-yellow-700 border-yellow-300",
"财经": "bg-cyan-50 text-cyan-700 border-cyan-300",
"艺术": "bg-pink-50 text-pink-700 border-pink-300",
};
return typeClasses[type] || "bg-gray-50 text-gray-700 border-gray-300";
};
// 获取软科排名样式
const getRankingClass = () => {
if (!props.data.Ranking) return "bg-gray-50 text-gray-700";
// 解析排名范围 (例如: "151-155")
const match = props.data.Ranking.match(/(\d+)-(\d+)/);
if (match) {
const minRank = parseInt(match[1]);
if (minRank <= 50) return "bg-green-50 text-green-700 border-green-300";
if (minRank <= 100) return "bg-blue-50 text-blue-700 border-blue-300";
if (minRank <= 200) return "bg-yellow-50 text-yellow-700 border-yellow-300";
}
return "bg-gray-50 text-gray-700 border-gray-300";
};
// 判断是否有数据
const hasData = computed(() => {
return props.data && Object.keys(props.data).length > 0;
});
// 获取徽章列表
const schoolBadges = computed(() => getSchoolBadges());
</script>
<template>
<div v-if="hasData" class="card max-w-4xl mx-auto">
<!-- 头部区域 -->
<div class="mb-6">
<!-- 学历等级标题 -->
<div class="flex items-center justify-between mb-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">{{ data.educationLevel }}学历</h2>
<p class="text-sm text-gray-500">Education Information</p>
</div>
</div>
</div>
</div>
<!-- 主要内容区域 -->
<div class="space-y-4">
<!-- 软科排名 -->
<div v-if="data.Ranking"
class="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-lg p-4 border border-blue-200">
<div class="flex items-center gap-3">
<div class="w-8 h-8 flex items-center justify-center flex-shrink-0">
<img :src="rkpmIcon" alt="软科排名" class="w-8 h-8" />
</div>
<div class="flex-1">
<div class="text-sm text-gray-600 mb-1">软科排名</div>
<div
:class="['inline-block px-3 py-1 rounded-full text-sm font-medium border', getRankingClass()]">
{{ data.Ranking }}
</div>
</div>
</div>
</div>
<!-- 学校徽章 -->
<div v-if="schoolBadges.length > 0" class="flex gap-2 flex-wrap">
<div v-for="(badge, index) in schoolBadges" :key="index"
:class="['inline-flex items-center gap-1 px-4 py-2 rounded-lg text-sm font-medium shadow-sm', badge.class]">
<span>{{ badge.icon }}</span>
<span>{{ badge.text }}</span>
</div>
</div>
<!-- 详细信息卡片 -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- 专业信息 -->
<div v-if="data.major"
class="bg-gray-50 rounded-lg p-4 border border-gray-200 hover:border-blue-300 transition-colors">
<div class="flex items-start gap-3">
<div class="w-8 h-8 flex items-center justify-center flex-shrink-0 mt-1">
<img :src="zymcIcon" alt="专业名称" class="w-8 h-8" />
</div>
<div class="flex-1">
<div class="text-sm text-gray-600 mb-1">专业名称</div>
<div class="text-base font-medium text-gray-900">{{ data.major }}</div>
</div>
</div>
</div>
<!-- 学校类型 -->
<div v-if="data.schoolType"
class="bg-gray-50 rounded-lg p-4 border border-gray-200 hover:border-blue-300 transition-colors">
<div class="flex items-start gap-3">
<div class="w-8 h-8 flex items-center justify-center flex-shrink-0 mt-1">
<img :src="xxlxIcon" alt="学校类型" class="w-8 h-8" />
</div>
<div class="flex-1">
<div class="text-sm text-gray-600 mb-1">学校类型</div>
<div
:class="['inline-block px-3 py-1 rounded text-sm font-medium border', getSchoolTypeBadgeClass(data.schoolType)]">
{{ data.schoolType }}类院校
</div>
</div>
</div>
</div>
<!-- 学习形式 -->
<div v-if="data.educationType"
class="bg-gray-50 rounded-lg p-4 border border-gray-200 hover:border-blue-300 transition-colors">
<div class="flex items-start gap-3">
<div class="w-8 h-8 flex items-center justify-center flex-shrink-0 mt-1">
<img :src="xxxsIcon" alt="学习形式" class="w-8 h-8" />
</div>
<div class="flex-1">
<div class="text-sm text-gray-600 mb-1">学习形式</div>
<div class="text-base font-medium text-gray-900">{{ data.educationType }}</div>
</div>
</div>
</div>
<!-- 毕业时间 -->
<div v-if="data.graduationDate"
class="bg-gray-50 rounded-lg p-4 border border-gray-200 hover:border-blue-300 transition-colors">
<div class="flex items-start gap-3">
<div class="w-8 h-8 flex items-center justify-center flex-shrink-0 mt-1">
<img :src="bysjIcon" alt="毕业时间" class="w-8 h-8" />
</div>
<div class="flex-1">
<div class="text-sm text-gray-600 mb-1">毕业时间</div>
<div class="text-base font-medium text-gray-900">{{ formatDate(data.graduationDate) }}</div>
</div>
</div>
</div>
</div>
<!-- 学历描述 -->
<div v-if="data.educationLevel"
class="mt-6 bg-gradient-to-r from-blue-50 to-indigo-50 rounded-lg p-5 border border-blue-200">
<div class="flex items-start gap-3">
<div class="w-10 h-10 rounded-full bg-blue-500 flex items-center justify-center flex-shrink-0 mt-1">
<span class="text-white text-xl">💡</span>
</div>
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<h3 class="text-lg font-semibold text-gray-900">学历说明</h3>
<span class="px-2 py-1 bg-blue-500 text-white text-xs rounded">信息</span>
</div>
<p class="text-gray-700 leading-relaxed">
{{ getEducationLevelDesc(data.educationLevel) }}
</p>
</div>
</div>
</div>
</div>
</div>
<!-- 无数据状态 -->
<div v-else class="card max-w-2xl 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;
}
/* 添加一些动画效果 */
.bg-gray-50 {
transition: all 0.3s ease;
}
.bg-gray-50:hover {
transform: translateY(-2px);
}
</style>

View File

@@ -63,16 +63,18 @@ const currentStatus = !actualData
: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>
<!-- <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> -->
<style lang="scss" scoped>
.status-info {

227
src/ui/CIVYZ9K7F.vue Normal file
View File

@@ -0,0 +1,227 @@
<template>
<div class="card">
<div class="header-box">
<h3 class="header-title">公安二要素认证</h3>
<p class="header-desc">核验姓名与身份证号是否一致</p>
</div>
<div v-if="hasData" class="result-section" :class="resultSectionClass">
<div class="result-main">
<div class="result-label">核验结果</div>
<div class="result-value" :class="resultClass">
{{ resultText }}
</div>
</div>
<div v-if="desc" class="result-desc">
{{ desc }}
</div>
</div>
<div v-if="hasBaseInfo" class="info-block">
<div class="block-title">被核验人信息</div>
<div class="info-row">
<span class="info-label">姓名</span>
<span class="info-value">{{ maskedName }}</span>
</div>
<div class="info-row">
<span class="info-label">性别</span>
<span class="info-value">{{ sex || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">生日</span>
<span class="info-value">{{ birthdayDisplay }}</span>
</div>
<div class="info-row">
<span class="info-label">户籍地址</span>
<span class="info-value">{{ address || '-' }}</span>
</div>
</div>
<div v-if="!hasData" class="empty-tip">暂无核验结果</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
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 hasData = computed(() => props.data && Object.keys(props.data).length > 0);
const resultCode = computed(() => {
const v = props.data?.result;
if (v === 0 || v === '0') return 0;
if (v === 1 || v === '1') return 1;
if (v === 2 || v === '2') return 2;
return null;
});
const resultText = computed(() => {
switch (resultCode.value) {
case 0:
return '一致';
case 1:
return '不一致';
case 2:
return '无记录';
default:
return '暂无结果';
}
});
const resultClass = computed(() => {
if (resultCode.value === 0) return 'result-ok';
if (resultCode.value === 1) return 'result-bad';
if (resultCode.value === 2) return 'result-unknown';
return 'result-unknown';
});
const resultSectionClass = computed(() => {
if (resultCode.value === 0) return 'result-section ok';
if (resultCode.value === 1) return 'result-section bad';
if (resultCode.value === 2) return 'result-section unknown';
return 'result-section unknown';
});
const desc = computed(() => props.data?.desc || '');
const sex = computed(() => props.data?.sex || '');
const birthday = computed(() => props.data?.birthday || '');
const address = computed(() => props.data?.address || '');
const birthdayDisplay = computed(() => {
const b = birthday.value;
if (!b || b.length !== 8) return b || '-';
const y = b.slice(0, 4);
const m = b.slice(4, 6);
const d = b.slice(6, 8);
return `${y}-${m}-${d}`;
});
const maskedName = computed(() => {
const name = props.params?.name || '';
if (!name) return '-';
return name.length > 1 ? name[0] + '*'.repeat(name.length - 1) : '*';
});
const hasBaseInfo = computed(() => sex.value || birthday.value || address.value || props.params?.name);
</script>
<style scoped>
.card {
padding: 1rem;
}
.header-box {
margin-bottom: 1rem;
}
.header-title {
font-size: 1.125rem;
font-weight: 600;
}
.header-desc {
font-size: 0.875rem;
color: #6b7280;
margin-top: 0.25rem;
}
.result-section {
padding: 0.75rem 0.875rem;
border-radius: 0.75rem;
margin-bottom: 1rem;
border: 1px solid #e5e7eb;
}
.result-section.ok {
background: #ecfdf3;
border-color: #22c55e33;
}
.result-section.bad {
background: #fef2f2;
border-color: #ef444433;
}
.result-section.unknown {
background: #f9fafb;
}
.result-main {
display: flex;
align-items: center;
justify-content: space-between;
}
.result-label {
font-size: 0.875rem;
color: #6b7280;
}
.result-value {
font-size: 1rem;
font-weight: 600;
}
.result-ok {
color: #16a34a;
}
.result-bad {
color: #dc2626;
}
.result-unknown {
color: #6b7280;
}
.result-desc {
margin-top: 0.5rem;
font-size: 0.875rem;
color: #4b5563;
}
.info-block {
margin-top: 1rem;
padding: 0.75rem 0.875rem;
border-radius: 0.75rem;
background: #f9fafb;
}
.block-title {
font-size: 0.875rem;
font-weight: 600;
color: #374151;
margin-bottom: 0.5rem;
}
.info-row {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
padding: 0.15rem 0;
}
.info-label {
color: #6b7280;
}
.info-value {
color: #111827;
margin-left: 1rem;
text-align: right;
}
.empty-tip {
color: #9ca3af;
font-size: 0.875rem;
padding: 1rem 0;
text-align: center;
}
</style>

239
src/ui/CIVYZA1B3.vue Normal file
View File

@@ -0,0 +1,239 @@
<template>
<div class="card">
<div class="header-box">
<h3 class="header-title">公安三要素</h3>
<p class="header-desc">比对人像与身份证信息是否为同一人</p>
</div>
<div v-if="hasData" class="result-section" :class="scoreLevelClass">
<div class="result-main">
<div class="result-label">系统判断</div>
<div class="result-value" :class="scoreTextClass">
{{ msg || scoreConclusion }}
</div>
</div>
<div class="result-score">
相似度分值<span class="font-semibold">{{ scoreDisplay }}</span>
<span class="text-xs text-gray-500 ml-1">(01越高越相似)</span>
</div>
</div>
<div v-if="hasBaseInfo" class="info-block">
<div class="block-title">被核验人信息</div>
<div class="info-row">
<span class="info-label">姓名</span>
<span class="info-value">{{ maskedName }}</span>
</div>
<div class="info-row">
<span class="info-label">性别</span>
<span class="info-value">{{ sex || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">生日</span>
<span class="info-value">{{ birthdayDisplay }}</span>
</div>
<div class="info-row">
<span class="info-label">户籍地址</span>
<span class="info-value">{{ address || '-' }}</span>
</div>
</div>
<div v-if="!hasData" class="empty-tip">暂无核验结果</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
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 hasData = computed(() => props.data && Object.keys(props.data).length > 0);
const score = computed(() => {
const v = props.data?.score;
if (typeof v === 'number') return v;
const n = Number(v);
return Number.isFinite(n) ? n : null;
});
const msg = computed(() => props.data?.msg || '');
const incorrect = computed(() => props.data?.incorrect ?? null);
const sex = computed(() => props.data?.sex || '');
const birthday = computed(() => props.data?.birthday || '');
const address = computed(() => props.data?.address || '');
const scoreDisplay = computed(() => {
if (score.value == null) return '-';
return score.value.toFixed(2);
});
const scoreConclusion = computed(() => {
const s = score.value;
if (s == null) return '暂无结论';
if (s < 0.4) return '系统判断为不同人';
if (s < 0.45) return '不能确定是否为同一人';
return '系统判断为同一人';
});
const scoreLevelClass = computed(() => {
const s = score.value;
if (s == null) return 'result-section unknown';
if (s < 0.4) return 'result-section bad';
if (s < 0.45) return 'result-section warn';
return 'result-section ok';
});
const scoreTextClass = computed(() => {
const s = score.value;
if (s == null) return 'result-unknown';
if (s < 0.4) return 'result-bad';
if (s < 0.45) return 'result-warn';
return 'result-ok';
});
const birthdayDisplay = computed(() => {
const b = birthday.value;
if (!b || b.length !== 8) return b || '-';
const y = b.slice(0, 4);
const m = b.slice(4, 6);
const d = b.slice(6, 8);
return `${y}-${m}-${d}`;
});
const maskedName = computed(() => {
const name = props.params?.name || '';
if (!name) return '-';
return name.length > 1 ? name[0] + '*'.repeat(name.length - 1) : '*';
});
const hasBaseInfo = computed(() => sex.value || birthday.value || address.value || props.params?.name);
</script>
<style scoped>
.card {
padding: 1rem;
}
.header-box {
margin-bottom: 1rem;
}
.header-title {
font-size: 1.125rem;
font-weight: 600;
}
.header-desc {
font-size: 0.875rem;
color: #6b7280;
margin-top: 0.25rem;
}
.result-section {
padding: 0.75rem 0.875rem;
border-radius: 0.75rem;
margin-bottom: 1rem;
border: 1px solid #e5e7eb;
}
.result-section.ok {
background: #ecfdf3;
border-color: #22c55e33;
}
.result-section.bad {
background: #fef2f2;
border-color: #ef444433;
}
.result-section.warn {
background: #fffbeb;
border-color: #f9731633;
}
.result-section.unknown {
background: #f9fafb;
}
.result-main {
display: flex;
align-items: center;
justify-content: space-between;
}
.result-label {
font-size: 0.875rem;
color: #6b7280;
}
.result-value {
font-size: 1rem;
font-weight: 600;
}
.result-ok {
color: #16a34a;
}
.result-bad {
color: #dc2626;
}
.result-warn {
color: #d97706;
}
.result-unknown {
color: #6b7280;
}
.result-score {
margin-top: 0.5rem;
font-size: 0.875rem;
color: #4b5563;
}
.info-block {
margin-top: 1rem;
padding: 0.75rem 0.875rem;
border-radius: 0.75rem;
background: #f9fafb;
}
.block-title {
font-size: 0.875rem;
font-weight: 600;
color: #374151;
margin-bottom: 0.5rem;
}
.info-row {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
padding: 0.15rem 0;
}
.info-label {
color: #6b7280;
}
.info-value {
color: #111827;
margin-left: 1rem;
text-align: right;
}
.empty-tip {
color: #9ca3af;
font-size: 0.875rem;
padding: 1rem 0;
text-align: center;
}
</style>

235
src/ui/CJRZQ0B6Y.vue Normal file
View File

@@ -0,0 +1,235 @@
<template>
<div class="card">
<div class="header-box">
<h3 class="header-title">银行卡黑名单(实时)</h3>
<p class="header-desc">查询银行卡是否命中各类黑名单/风险名单</p>
</div>
<div v-if="hasData" class="result-section">
<div class="risk-summary" :class="summaryClass">
<span class="risk-label">综合结果</span>
<span class="risk-value">
{{ summaryText }}
</span>
</div>
<div class="risk-tags">
<div v-for="item in riskItems" :key="item.key" class="risk-item" :class="item.hit ? 'hit' : 'normal'">
<div class="risk-item-title">{{ item.label }}</div>
<div class="risk-item-value">
{{ item.hit ? '命中' : '未命中' }}
</div>
</div>
</div>
</div>
<div v-if="hasParams" class="info-block">
<div class="block-title">被核验信息</div>
<div class="info-row">
<span class="info-label">持卡人姓名</span>
<span class="info-value">{{ maskedName }}</span>
</div>
<div class="info-row">
<span class="info-label">身份证号</span>
<span class="info-value font-mono">{{ maskedIdCard }}</span>
</div>
<div class="info-row">
<span class="info-label">银行卡号</span>
<span class="info-value font-mono">{{ maskedBankCard }}</span>
</div>
</div>
<div v-if="!hasData" class="empty-tip">暂无核验结果</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
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 hasData = computed(() => props.data && Object.keys(props.data).length > 0);
const riskItems = computed(() => {
const d = props.data || {};
const toBool = (v) => v === '1' || v === 1;
return [
{ key: 'badCardHolder', label: '不良持卡人', hit: toBool(d.badCardHolder) },
{ key: 'caseRelated', label: '涉案卡片', hit: toBool(d.caseRelated) },
{ key: 'fraudTrans', label: '交易欺诈卡片', hit: toBool(d.fraudTrans) },
{ key: 'offlineBlack', label: '线下卡号黑名单', hit: toBool(d.offlineBlack) },
{ key: 'onlineBlack', label: '线上卡号黑名单', hit: toBool(d.onlineBlack) },
{ key: 'otherBlack', label: '其他卡号黑名单', hit: toBool(d.otherBlack) },
];
});
const anyHit = computed(() => riskItems.value.some((i) => i.hit));
const summaryText = computed(() => {
if (!hasData.value) return '暂无结果';
if (!anyHit.value) return '未命中任何银行卡黑名单';
return '存在银行卡黑名单或风险记录';
});
const summaryClass = computed(() => {
if (!hasData.value) return 'summary unknown';
if (anyHit.value) return 'summary bad';
return 'summary ok';
});
const maskedName = computed(() => {
const name = props.params?.name || '';
if (!name) return '-';
return name.length > 1 ? name[0] + '*'.repeat(name.length - 1) : '*';
});
const maskedIdCard = computed(() => {
const id = props.params?.id_card || '';
if (!id || id.length < 8) return id || '-';
return id.slice(0, 4) + '********' + id.slice(-4);
});
const maskedBankCard = computed(() => {
const card = props.params?.bank_card || '';
if (!card || card.length < 8) return card || '-';
return card.slice(0, 4) + '********' + card.slice(-4);
});
const hasParams = computed(() => {
const p = props.params || {};
return p.name || p.id_card || p.bank_card;
});
</script>
<style scoped>
.card {
padding: 1rem;
}
.header-box {
margin-bottom: 1rem;
}
.header-title {
font-size: 1.125rem;
font-weight: 600;
}
.header-desc {
font-size: 0.875rem;
color: #6b7280;
margin-top: 0.25rem;
}
.result-section {
padding: 0.75rem 0.875rem;
border-radius: 0.75rem;
margin-bottom: 1rem;
border: 1px solid #e5e7eb;
background: #f9fafb;
}
.risk-summary {
font-size: 0.95rem;
margin-bottom: 0.5rem;
}
.summary.ok {
background: #ecfdf3;
border-color: #22c55e33;
}
.summary.bad {
background: #fef2f2;
border-color: #ef444433;
}
.summary.unknown {
background: #f9fafb;
}
.risk-label {
color: #6b7280;
}
.risk-value {
font-weight: 600;
color: #111827;
}
.risk-tags {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.5rem;
}
.risk-item {
border-radius: 0.5rem;
padding: 0.5rem 0.6rem;
font-size: 0.8rem;
}
.risk-item.hit {
background: #fef2f2;
border: 1px solid #ef4444;
color: #b91c1c;
}
.risk-item.normal {
background: #ecfdf3;
border: 1px solid #22c55e33;
color: #166534;
}
.risk-item-title {
margin-bottom: 0.15rem;
}
.risk-item-value {
font-weight: 600;
}
.info-block {
margin-top: 1rem;
padding: 0.75rem 0.875rem;
border-radius: 0.75rem;
background: #f9fafb;
}
.block-title {
font-size: 0.875rem;
font-weight: 600;
color: #374151;
margin-bottom: 0.5rem;
}
.info-row {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
padding: 0.15rem 0;
}
.info-label {
color: #6b7280;
}
.info-value {
color: #111827;
margin-left: 1rem;
text-align: right;
}
.empty-tip {
color: #9ca3af;
font-size: 0.875rem;
padding: 1rem 0;
text-align: center;
}
</style>

227
src/ui/CJRZQACAB.vue Normal file
View File

@@ -0,0 +1,227 @@
<template>
<div class="card">
<div class="header-box">
<h3 class="header-title">银行卡四要素验证详版</h3>
<p class="header-desc">核验姓名身份证号银行卡号与预留手机号是否匹配</p>
</div>
<div v-if="hasData" class="result-section" :class="resultSectionClass">
<div class="result-main">
<div class="result-label">验证结果</div>
<div class="result-value" :class="resultClass">
{{ stateText }}
</div>
</div>
</div>
<div v-if="hasParams" class="info-block">
<div class="block-title">被核验信息</div>
<div class="info-row">
<span class="info-label">持卡人姓名</span>
<span class="info-value">{{ maskedName }}</span>
</div>
<div class="info-row">
<span class="info-label">身份证号</span>
<span class="info-value font-mono">{{ maskedIdCard }}</span>
</div>
<div class="info-row">
<span class="info-label">银行卡号</span>
<span class="info-value font-mono">{{ maskedBankCard }}</span>
</div>
<div class="info-row">
<span class="info-label">预留手机号</span>
<span class="info-value font-mono">{{ maskedMobile }}</span>
</div>
</div>
<div v-if="!hasData" class="empty-tip">暂无核验结果</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
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 hasData = computed(() => props.data && Object.keys(props.data).length > 0);
const state = computed(() => props.data?.state || '');
const stateText = computed(() => {
switch (state.value) {
case '1':
return '验证一致';
case '2':
return '认证不一致:此卡已过期或卡号无效';
case '3':
return '认证不一致:认证未通过';
case '4':
return '认证不一致:今日验证次数过多,请明日再试';
case '5':
return '认证不一致:当前提交数量过多,请降低提交频率';
case '6':
return '认证不一致:持卡人信息有误或卡状态异常';
case '7':
return '认证不一致:未开通无卡支付或发卡行不支持该卡验证';
case '8':
return '认证不一致:认证受限';
default:
return '暂无结果';
}
});
const resultClass = computed(() => {
if (state.value === '1') return 'result-ok';
if (!state.value) return 'result-unknown';
return 'result-bad';
});
const resultSectionClass = computed(() => {
if (state.value === '1') return 'result-section ok';
if (!state.value) return 'result-section unknown';
return 'result-section bad';
});
const maskedName = computed(() => {
const name = props.params?.name || '';
if (!name) return '-';
return name.length > 1 ? name[0] + '*'.repeat(name.length - 1) : '*';
});
const maskedIdCard = computed(() => {
const id = props.params?.id_card || '';
if (!id || id.length < 8) return id || '-';
return id.slice(0, 4) + '********' + id.slice(-4);
});
const maskedBankCard = computed(() => {
const card = props.params?.bank_card || '';
if (!card || card.length < 8) return card || '-';
return card.slice(0, 4) + '********' + card.slice(-4);
});
const maskedMobile = computed(() => {
const m = props.params?.mobile || props.params?.mobile_no || '';
if (!m || m.length < 7) return m || '-';
return m.slice(0, 3) + '****' + m.slice(-4);
});
const hasParams = computed(() => {
const p = props.params || {};
return p.name || p.id_card || p.bank_card || p.mobile || p.mobile_no;
});
</script>
<style scoped>
.card {
padding: 1rem;
}
.header-box {
margin-bottom: 1rem;
}
.header-title {
font-size: 1.125rem;
font-weight: 600;
}
.header-desc {
font-size: 0.875rem;
color: #6b7280;
margin-top: 0.25rem;
}
.result-section {
padding: 0.75rem 0.875rem;
border-radius: 0.75rem;
margin-bottom: 1rem;
border: 1px solid #e5e7eb;
}
.result-section.ok {
background: #ecfdf3;
border-color: #22c55e33;
}
.result-section.bad {
background: #fef2f2;
border-color: #ef444433;
}
.result-section.unknown {
background: #f9fafb;
}
.result-main {
display: flex;
align-items: center;
justify-content: space-between;
}
.result-label {
font-size: 0.875rem;
color: #6b7280;
}
.result-value {
font-size: 1rem;
font-weight: 600;
}
.result-ok {
color: #16a34a;
}
.result-bad {
color: #dc2626;
}
.result-unknown {
color: #6b7280;
}
.info-block {
margin-top: 1rem;
padding: 0.75rem 0.875rem;
border-radius: 0.75rem;
background: #f9fafb;
}
.block-title {
font-size: 0.875rem;
font-weight: 600;
color: #374151;
margin-bottom: 0.5rem;
}
.info-row {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
padding: 0.15rem 0;
}
.info-label {
color: #6b7280;
}
.info-value {
color: #111827;
margin-left: 1rem;
text-align: right;
}
.empty-tip {
color: #9ca3af;
font-size: 0.875rem;
padding: 1rem 0;
text-align: center;
}
</style>

204
src/ui/CQCXG1H7Y.vue Normal file
View File

@@ -0,0 +1,204 @@
<template>
<div class="card">
<div class="header-box">
<div class="header-left">
<h3 class="header-title">车辆过户简版查询</h3>
<p class="header-desc">查看车辆最近是否发生过户及过户次数</p>
</div>
<div class="header-tag" :class="flagClass">
<span class="tag-label">是否过户</span>
<span class="tag-value">{{ flagText }}</span>
</div>
</div>
<template v-if="hasData">
<div class="summary-card" :class="flagClass">
<div class="summary-main">
<div class="summary-title">
最近过户情况
</div>
<div class="summary-flag">{{ flagDetailText }}</div>
</div>
<div class="summary-meta">
<div class="meta-line">
<span class="meta-label">最近过户时间</span>
<span class="meta-value">{{ formattedTransferDate }}</span>
</div>
<div class="meta-line">
<span class="meta-label">累计过户次数</span>
<span class="meta-value strong">{{ data.transferNum || '0' }} </span>
</div>
</div>
</div>
</template>
<div v-else class="empty">
<div class="icon"></div>
<div class="title">暂无过户信息</div>
<div class="sub">未查询到车辆过户记录</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useRiskNotifier } from '@/composables/useRiskNotifier';
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 hasData = computed(() => !!props.data && Object.keys(props.data).length > 0);
// transferFlag: 0-否1-是(字符串)
const flag = computed(() => props.data?.transferFlag);
const flagText = computed(() => {
if (flag.value === '1') return '已过户';
if (flag.value === '0') return '未过户';
return '未知';
});
const flagDetailText = computed(() => {
if (flag.value === '1') return '该车辆存在过户记录';
if (flag.value === '0') return '该车辆暂无过户记录';
return '未能识别过户状态';
});
const flagClass = computed(() => {
if (flag.value === '1') return 'flag-yes';
if (flag.value === '0') return 'flag-no';
return 'flag-unknown';
});
const formattedTransferDate = computed(() => {
const raw = props.data?.transferDate;
if (!raw) return '-';
if (raw === '近一年内过户') return '近一年内过户';
// 期望格式 yyyyMM
if (raw.length === 6) {
const y = raw.slice(0, 4);
const m = raw.slice(4, 6);
return `${y}${m}`;
}
return raw;
});
const riskScore = computed(() => 100);
useRiskNotifier(props, riskScore);
defineExpose({ riskScore });
</script>
<style scoped>
.card {
@apply bg-white rounded-2xl p-6 shadow-sm border border-gray-100;
}
.header-box {
@apply flex items-center justify-between mb-5 px-5 py-4 rounded-2xl bg-gradient-to-r from-sky-50 via-blue-50 to-sky-50;
}
.header-left {
@apply flex flex-col;
}
.header-title {
@apply text-2xl font-semibold m-0 text-sky-900;
}
.header-desc {
@apply text-base mt-3 m-0 text-sky-800 opacity-90;
}
.header-tag {
@apply inline-flex flex-col items-end gap-1 px-3 py-2 rounded-xl bg-white/80 shadow-sm;
}
.header-tag.flag-yes {
@apply text-amber-800;
}
.header-tag.flag-no {
@apply text-emerald-800;
}
.header-tag.flag-unknown {
@apply text-gray-700;
}
.tag-label {
@apply text-sm text-gray-500;
}
.tag-value {
@apply text-xl font-bold leading-none whitespace-nowrap;
}
.summary-card {
@apply rounded-2xl border px-5 py-4 mb-4;
}
.summary-card.flag-yes {
@apply bg-amber-50 border-amber-100;
}
.summary-card.flag-no {
@apply bg-emerald-50 border-emerald-100;
}
.summary-card.flag-unknown {
@apply bg-gray-50 border-gray-200;
}
.summary-main {
@apply flex items-baseline justify-between mb-3;
}
.summary-title {
@apply text-lg font-semibold text-gray-900;
}
.summary-flag {
@apply text-base text-gray-700;
}
.summary-meta {
@apply space-y-2 text-base text-gray-800;
}
.meta-line {
@apply flex items-center gap-2;
}
.meta-label {
@apply text-gray-500;
}
.meta-value {
@apply font-medium;
}
.meta-value.strong {
@apply text-lg font-semibold;
}
.empty {
@apply text-center py-10 text-gray-500;
}
.empty .icon {
@apply text-3xl mb-2;
}
.empty .title {
@apply text-lg font-medium mb-1;
}
.empty .sub {
@apply text-sm;
}
</style>

374
src/ui/CQCXG1U4U.vue Normal file
View File

@@ -0,0 +1,374 @@
<template>
<div class="card">
<div class="header-box">
<div class="header-left">
<h3 class="header-title">车辆里程记录混合查询</h3>
<p class="header-desc">综合诊断与维保记录展示车辆里程变化与是否存在调表嫌疑</p>
</div>
</div>
<template v-if="hasData">
<!-- 概览里程是否异常 + 最新里程 -->
<div class="summary-card" :class="suspectClass">
<div class="summary-main">
<div class="summary-left">
<div class="vin-label">VIN</div>
<div class="vin-value font-mono">{{ vin || '-' }}</div>
</div>
<div class="summary-right">
<div class="summary-label">最新里程</div>
<div class="summary-mileage">{{ latestMileageText }}</div>
<div class="summary-sub">最近记录日期{{ latestReportTime || '-' }}</div>
</div>
</div>
<div class="summary-meta">
<div class="meta-line">
<span>里程是否异常</span>
<span class="strong" :class="suspectClass">{{ suspectedText }}</span>
</div>
<div class="meta-line" v-if="imageUrl">
<span>行驶证图片</span>
</div>
<div v-if="imageUrl" class="image-wrap">
<img :src="imageUrl" alt="行驶证图片" class="licence-img" />
</div>
</div>
</div>
<!-- 里程时间轴 -->
<div class="detail-card">
<h4 class="section-title">里程记录时间轴</h4>
<div v-if="mileageList && mileageList.length" class="timeline">
<div v-for="(item, idx) in mileageList" :key="idx" class="timeline-item">
<div class="timeline-left">
<div class="dot-wrap">
<div class="dot" :class="item.mileageStatus === '1' ? 'dot-abnormal' : 'dot-normal'">
</div>
<div v-if="idx !== mileageList.length - 1" class="line"></div>
</div>
</div>
<div class="timeline-content">
<div class="row-main">
<div class="date">{{ formatDate(item.reportTime) }}</div>
<div class="km">{{ formatMileage(item.mileage) }}</div>
</div>
<div class="row-sub">
<span>来源{{ sourceText(item.source) }}</span>
<span v-if="item.mileageStatus === '1'" class="badge-abnormal">异常里程</span>
</div>
</div>
</div>
</div>
<div v-else class="empty-small">
暂无里程记录
</div>
</div>
<!-- 异常里程列表 -->
<div class="detail-card" v-if="adjustList && adjustList.length">
<h4 class="section-title">疑似调表记录</h4>
<div class="adjust-list">
<div v-for="(item, idx) in adjustList" :key="idx" class="adjust-item">
<div class="adjust-time">{{ formatDate(item.reportTime) }}</div>
<div class="adjust-body">
<div>
<div class="adjust-label">调整前</div>
<div class="adjust-value">{{ formatMileage(item.beforeMileage) }}</div>
</div>
<div class="adjust-arrow"></div>
<div>
<div class="adjust-label">调整后</div>
<div class="adjust-value">{{ formatMileage(item.afterMileage) }}</div>
</div>
</div>
</div>
</div>
</div>
</template>
<div v-else class="empty">
<div class="icon"></div>
<div class="title">暂无里程数据</div>
<div class="sub">未查询到车辆里程记录或返回数据为空</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useRiskNotifier } from '@/composables/useRiskNotifier';
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 hasData = computed(() => !!props.data && Object.keys(props.data).length > 0);
const vin = computed(() => props.data?.vehicleInfo?.vin || '');
const mileageList = computed(() => props.data?.mileageInfo?.mileageList || []);
const adjustList = computed(() => props.data?.mileageInfo?.suspectedAdjustMileageList || []);
const suspectedAdjust = computed(() => props.data?.mileageInfo?.suspectedAdjust);
const imageUrl = computed(() => props.data?.imageUrl || '');
const latestRecord = computed(() => {
const list = mileageList.value;
if (!list || !list.length) return null;
// 默认接口数据已按时间升序,直接取最后一条
return list[list.length - 1];
});
const latestMileageText = computed(() => {
if (!latestRecord.value) return '-';
return formatMileage(latestRecord.value.mileage);
});
const latestReportTime = computed(() => latestRecord.value?.reportTime || '');
const suspectedText = computed(() => {
if (suspectedAdjust.value === 'true') return '存在异常里程行为';
if (suspectedAdjust.value === 'false') return '未发现里程异常';
return '未知';
});
const suspectClass = computed(() => {
if (suspectedAdjust.value === 'true') return 'suspect-yes';
if (suspectedAdjust.value === 'false') return 'suspect-no';
return 'suspect-unknown';
});
const formatMileage = (val) => {
if (!val && val !== 0) return '-';
const num = Number(val);
if (Number.isNaN(num)) return `${val} km`;
// 默认 km按 km 展示
return `${num.toLocaleString()} km`;
};
const sourceText = (source) => {
if (source === '0') return '诊断里程';
if (source === '1') return '维保里程';
return '其他';
};
const formatDate = (val) => {
if (!val) return '-';
// 期望格式 yyyy-MM-dd
const m = String(val).match(/^(\d{4})-(\d{2})-(\d{2})/);
if (m) {
return `${m[1]}${m[2]}${m[3]}`;
}
return val;
};
const riskScore = computed(() => 100);
useRiskNotifier(props, riskScore);
defineExpose({ riskScore });
</script>
<style scoped>
.card {
@apply bg-white rounded-2xl p-6 shadow-sm border border-gray-100;
}
.header-box {
@apply flex items-center mb-4 px-5 py-4 rounded-2xl bg-gradient-to-r from-sky-50 via-blue-50 to-indigo-50;
}
.header-left {
@apply flex flex-col;
}
.header-title {
@apply text-2xl font-semibold m-0 text-sky-900;
}
.header-desc {
@apply text-base mt-3 m-0 text-sky-800 opacity-90;
}
.summary-card {
@apply rounded-2xl border px-5 py-4 mb-4;
}
.summary-card.suspect-yes {
@apply bg-amber-50 border-amber-100;
}
.summary-card.suspect-no {
@apply bg-emerald-50 border-emerald-100;
}
.summary-card.suspect-unknown {
@apply bg-gray-50 border-gray-200;
}
.summary-main {
@apply flex flex-col mb-3 gap-3;
}
.summary-left {
@apply flex flex-col gap-1;
}
.vin-label {
@apply text-sm text-gray-500;
}
.vin-value {
@apply text-base text-gray-900;
}
.summary-right {
@apply text-left;
}
.summary-label {
@apply text-sm text-gray-500;
}
.summary-mileage {
@apply text-2xl font-bold text-sky-800 mt-1;
}
.summary-sub {
@apply text-sm text-sky-700 opacity-90;
}
.summary-meta {
@apply space-y-1 text-base text-gray-800;
}
.meta-line {
@apply flex flex-wrap items-center gap-2;
}
.meta-line .strong {
@apply font-semibold;
}
.image-wrap {
@apply mt-2 flex justify-center;
}
.licence-img {
@apply rounded-xl border border-gray-200 max-w-full;
max-height: 220px;
object-fit: contain;
}
.detail-card {
@apply rounded-2xl border border-gray-100 bg-gray-50/60 px-5 py-4 mb-4;
}
.section-title {
@apply text-base font-semibold text-gray-800 mb-3;
}
.timeline {
@apply mt-2;
}
.timeline-item {
@apply flex mb-3;
}
.timeline-left {
@apply mr-3 flex flex-col items-center;
}
.dot-wrap {
@apply flex flex-col items-stretch;
}
.dot {
@apply w-3 h-3 rounded-full bg-gray-400 self-center;
}
.dot-normal {
@apply bg-emerald-500;
}
.dot-abnormal {
@apply bg-red-500;
}
.line {
@apply flex-1 w-px bg-gray-300 mx-auto;
}
.timeline-content {
@apply flex-1 rounded-2xl border border-gray-100 bg-white px-4 py-3;
}
.row-main {
@apply flex items-baseline justify-between mb-1;
}
.row-main .date {
@apply text-base font-medium text-gray-900;
}
.row-main .km {
@apply text-lg font-semibold text-gray-900;
}
.row-sub {
@apply flex items-center justify-between text-sm text-gray-600 mt-1;
}
.badge-abnormal {
@apply inline-flex items-center px-2 py-0.5 rounded-full bg-red-50 text-red-700 text-xs font-medium;
}
.adjust-list {
@apply space-y-3;
}
.adjust-item {
@apply rounded-2xl border border-amber-100 bg-amber-50/70 px-4 py-3;
}
.adjust-time {
@apply text-sm text-gray-700 mb-2;
}
.adjust-body {
@apply flex items-center justify-between gap-4;
}
.adjust-label {
@apply text-xs text-gray-500 mb-1;
}
.adjust-value {
@apply text-base font-semibold text-gray-900;
}
.adjust-arrow {
@apply text-2xl text-gray-400;
}
.empty {
@apply text-center py-10 text-gray-500;
}
.empty-small {
@apply text-center py-6 text-sm text-gray-500;
}
.empty .icon {
@apply text-3xl mb-2;
}
.empty .title {
@apply text-lg font-medium mb-1;
}
.empty .sub {
@apply text-sm;
}
</style>

284
src/ui/CQCXG3Y6B.vue Normal file
View File

@@ -0,0 +1,284 @@
<template>
<div class="card">
<div class="header-box">
<div class="header-left">
<h3 class="header-title">车辆维保简版查询</h3>
<p class="header-desc">按时间轴展示维保记录包含保养与更换材料明细</p>
</div>
</div>
<template v-if="hasData">
<!-- 概览VIN + 维保次数 + 最近一次维保 -->
<div class="summary-card">
<div class="summary-main">
<div class="summary-left">
<div class="summary-label">车架号 VIN</div>
<div class="summary-vin font-mono">{{ vin || '-' }}</div>
</div>
<div class="summary-right">
<div class="summary-count">维保记录 {{ totalCount }} </div>
<div class="summary-last" v-if="lastRecord">
最近一次{{ formatDate(lastRecord.lastTime) }} · {{ formatMileage(lastRecord.mileage) }}
</div>
</div>
</div>
</div>
<!-- 维保时间轴 -->
<div class="detail-card">
<h4 class="section-title">维保记录时间轴</h4>
<div v-if="records && records.length" class="timeline">
<div v-for="(item, idx) in records" :key="idx" class="timeline-item">
<div class="timeline-left">
<div class="dot-wrap">
<div class="dot"></div>
<div v-if="idx !== records.length - 1" class="line"></div>
</div>
</div>
<div class="timeline-content">
<div class="row-main">
<div class="date">{{ formatDate(item.lastTime) }}</div>
<div class="km">{{ formatMileage(item.mileage) }}</div>
</div>
<div class="row-sub">
<span class="repair-type">{{ item.repairType || '维保' }}</span>
<span class="vin-small font-mono">VIN: {{ item.vin || vin || '-' }}</span>
</div>
<div v-if="item.details && item.details.length" class="sub-section">
<div class="sub-title">维修项目</div>
<ul class="sub-list">
<li v-for="(d, di) in item.details" :key="di">
<span v-if="d.type" class="tag">{{ d.type }}</span>
<span>{{ d.content }}</span>
</li>
</ul>
</div>
<div v-if="item.materials && item.materials.length" class="sub-section">
<div class="sub-title">使用材料</div>
<ul class="sub-list">
<li v-for="(m, mi) in item.materials" :key="mi">
<span v-if="m.type" class="tag tag-material">{{ m.type }}</span>
<span>{{ m.content }}</span>
</li>
</ul>
</div>
</div>
</div>
</div>
<div v-else class="empty-small">
暂无维保记录
</div>
</div>
</template>
<div v-else class="empty">
<div class="icon"></div>
<div class="title">暂无维保数据</div>
<div class="sub">未查询到车辆维保记录或返回数据为空</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useRiskNotifier } from '@/composables/useRiskNotifier';
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 hasData = computed(() => !!props.data && Object.keys(props.data).length > 0);
const records = computed(() => Array.isArray(props.data?.record) ? props.data.record : []);
const vin = computed(() => {
if (records.value.length && records.value[0].vin) return records.value[0].vin;
return props.params?.vin_code || '';
});
const totalCount = computed(() => records.value.length || 0);
const lastRecord = computed(() => (records.value.length ? records.value[records.value.length - 1] : null));
const formatDate = (val) => {
if (!val) return '-';
const m = String(val).match(/^(\d{4})-(\d{2})-(\d{2})/);
if (m) {
return `${m[1]}${m[2]}${m[3]}`;
}
return val;
};
const formatMileage = (val) => {
if (!val && val !== 0) return '-';
const num = Number(val);
if (Number.isNaN(num)) return `${val} km`;
return `${num.toLocaleString()} km`;
};
const riskScore = computed(() => 100);
useRiskNotifier(props, riskScore);
defineExpose({ riskScore });
</script>
<style scoped>
.card {
@apply bg-white rounded-2xl p-6 shadow-sm border border-gray-100;
}
.header-box {
@apply flex items-center mb-4 px-5 py-4 rounded-2xl bg-gradient-to-r from-emerald-50 via-sky-50 to-teal-50;
}
.header-left {
@apply flex flex-col;
}
.header-title {
@apply text-2xl font-semibold m-0 text-emerald-900;
}
.header-desc {
@apply text-base mt-3 m-0 text-emerald-800 opacity-90;
}
.summary-card {
@apply rounded-2xl border border-emerald-100 bg-emerald-50/60 px-5 py-4 mb-4;
}
.summary-main {
@apply flex items-start justify-between mb-3 gap-4;
}
.summary-left {
@apply flex flex-col gap-1;
}
.summary-label {
@apply text-sm text-gray-500;
}
.summary-vin {
@apply text-base text-gray-900;
}
.summary-right {
@apply text-right;
}
.summary-count {
@apply text-base font-semibold text-gray-900;
}
.summary-last {
@apply text-sm text-emerald-800 mt-1;
}
.detail-card {
@apply rounded-2xl border border-gray-100 bg-gray-50/60 px-5 py-4 mb-4;
}
.section-title {
@apply text-base font-semibold text-gray-800 mb-3;
}
.timeline {
@apply mt-2;
}
.timeline-item {
@apply flex mb-3;
}
.timeline-left {
@apply mr-3 flex flex-col items-center;
}
.dot-wrap {
@apply flex flex-col items-stretch;
}
.dot {
@apply w-3 h-3 rounded-full bg-emerald-500 self-center;
}
.line {
@apply flex-1 w-px bg-gray-300 mx-auto;
}
.timeline-content {
@apply flex-1 rounded-2xl border border-gray-100 bg-white px-4 py-3;
}
.row-main {
@apply flex items-baseline justify-between mb-1;
}
.row-main .date {
@apply text-base font-medium text-gray-900;
}
.row-main .km {
@apply text-lg font-semibold text-gray-900;
}
.row-sub {
@apply flex items-center justify-between text-sm text-gray-600 mt-1;
}
.repair-type {
@apply font-medium text-gray-800;
}
.vin-small {
@apply text-xs text-gray-500;
}
.sub-section {
@apply mt-3;
}
.sub-title {
@apply text-sm font-semibold text-gray-800 mb-1;
}
.sub-list {
@apply text-sm text-gray-800 space-y-1;
}
.sub-list li {
@apply flex flex-wrap gap-1;
}
.tag {
@apply inline-flex items-center px-2 py-0.5 rounded-full bg-emerald-50 text-emerald-700 text-xs font-medium;
}
.tag-material {
@apply bg-sky-50 text-sky-700;
}
.empty {
@apply text-center py-10 text-gray-500;
}
.empty-small {
@apply text-center py-6 text-sm text-gray-500;
}
.empty .icon {
@apply text-3xl mb-2;
}
.empty .title {
@apply text-lg font-medium mb-1;
}
.empty .sub {
@apply text-sm;
}
</style>

288
src/ui/CQCXG3Z3L.vue Normal file
View File

@@ -0,0 +1,288 @@
<template>
<div class="card">
<div class="header-box">
<div class="header-left">
<h3 class="header-title">车辆维保详细版查询</h3>
<p class="header-desc">展示品牌车架号等基本信息及每次维保的详细内容</p>
</div>
</div>
<template v-if="hasData">
<!-- 概览品牌 + VIN + 车牌号 + 发动机号 -->
<div class="summary-card">
<div class="summary-main">
<div class="summary-left">
<div class="summary-label">品牌名称</div>
<div class="summary-brand">{{ data.brandName || '未知品牌' }}</div>
</div>
<div class="summary-right">
<div class="summary-label">车架号 VIN</div>
<div class="summary-vin font-mono">{{ data.vin || '-' }}</div>
</div>
</div>
<div class="summary-meta">
<div class="meta-line">
<span>车牌号</span><span class="strong">{{ data.licensePlate || '未提供' }}</span>
<span class="dot"></span>
<span>发动机号</span><span class="strong code">{{ data.engine || '-' }}</span>
</div>
<div class="meta-line" v-if="records.length">
<span>维保记录</span><span class="strong">{{ records.length }} </span>
<span class="dot"></span>
<span>最近一次</span>
<span class="strong">{{ formatDate(records[records.length - 1].date) }}</span>
</div>
</div>
</div>
<!-- 维保时间轴 -->
<div class="detail-card">
<h4 class="section-title">维保记录时间轴</h4>
<div v-if="records && records.length" class="timeline">
<div v-for="(item, idx) in records" :key="idx" class="timeline-item">
<div class="timeline-left">
<div class="dot-wrap">
<div class="dot"></div>
<div v-if="idx !== records.length - 1" class="line"></div>
</div>
</div>
<div class="timeline-content">
<div class="row-main">
<div class="date">{{ formatDate(item.date) }}</div>
<div class="km">{{ formatMileage(item.mileage) }}</div>
</div>
<div class="row-sub">
<span class="repair-type">{{ item.type || '维保' }}</span>
<span class="brand-small" v-if="data.brandName">{{ data.brandName }}</span>
</div>
<div class="sub-section" v-if="item.content">
<div class="sub-title">维修内容</div>
<div class="sub-text">{{ item.content }}</div>
</div>
<div class="sub-section" v-if="item.material">
<div class="sub-title">材料</div>
<div class="sub-text">{{ item.material }}</div>
</div>
<div class="sub-section" v-if="item.remark">
<div class="sub-title">备注</div>
<div class="sub-text">{{ item.remark }}</div>
</div>
</div>
</div>
</div>
<div v-else class="empty-small">
暂无维保记录
</div>
</div>
</template>
<div v-else class="empty">
<div class="icon"></div>
<div class="title">暂无维保数据</div>
<div class="sub">未查询到车辆维保记录或返回数据为空</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useRiskNotifier } from '@/composables/useRiskNotifier';
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 hasData = computed(() => !!props.data && Object.keys(props.data).length > 0);
const records = computed(() => Array.isArray(props.data?.record) ? props.data.record : []);
const data = computed(() => props.data || {});
const formatDate = (val) => {
if (!val) return '-';
const m = String(val).match(/^(\d{4})-(\d{2})-(\d{2})/);
if (m) {
return `${m[1]}${m[2]}${m[3]}`;
}
return val;
};
const formatMileage = (val) => {
if (!val && val !== 0) return '-';
const num = Number(val);
if (Number.isNaN(num)) return `${val} km`;
return `${num.toLocaleString()} km`;
};
const riskScore = computed(() => 100);
useRiskNotifier(props, riskScore);
defineExpose({ riskScore });
</script>
<style scoped>
.card {
@apply bg-white rounded-2xl p-6 shadow-sm border border-gray-100;
}
.header-box {
@apply flex items-center mb-4 px-5 py-4 rounded-2xl bg-gradient-to-r from-indigo-50 via-sky-50 to-emerald-50;
}
.header-left {
@apply flex flex-col;
}
.header-title {
@apply text-2xl font-semibold m-0 text-indigo-900;
}
.header-desc {
@apply text-base mt-3 m-0 text-indigo-800 opacity-90;
}
.summary-card {
@apply rounded-2xl border border-indigo-100 bg-indigo-50/60 px-5 py-4 mb-4;
}
.summary-main {
@apply flex items-start justify-between mb-3 gap-4;
}
.summary-left {
@apply flex flex-col gap-1;
}
.summary-right {
@apply text-right;
}
.summary-label {
@apply text-sm text-gray-500;
}
.summary-brand {
@apply text-lg font-semibold text-gray-900;
}
.summary-vin {
@apply text-base text-gray-900;
}
.summary-meta {
@apply space-y-1 text-base text-gray-800;
}
.meta-line {
@apply flex flex-wrap items-center gap-2;
}
.meta-line .dot {
@apply w-1 h-1 rounded-full bg-gray-400;
}
.strong {
@apply font-semibold;
}
.code {
@apply font-mono tracking-wide;
}
.detail-card {
@apply rounded-2xl border border-gray-100 bg-gray-50/60 px-5 py-4 mb-4;
}
.section-title {
@apply text-base font-semibold text-gray-800 mb-3;
}
.timeline {
@apply mt-2;
}
.timeline-item {
@apply flex mb-3;
}
.timeline-left {
@apply mr-3 flex flex-col items-center;
}
.dot-wrap {
@apply flex flex-col items-stretch;
}
.dot {
@apply w-3 h-3 rounded-full bg-indigo-500 self-center;
}
.line {
@apply flex-1 w-px bg-gray-300 mx-auto;
}
.timeline-content {
@apply flex-1 rounded-2xl border border-gray-100 bg-white px-4 py-3;
}
.row-main {
@apply flex items-baseline justify-between mb-1;
}
.row-main .date {
@apply text-base font-medium text-gray-900;
}
.row-main .km {
@apply text-lg font-semibold text-gray-900;
}
.row-sub {
@apply flex items-center justify-between text-sm text-gray-600 mt-1;
}
.repair-type {
@apply font-medium text-gray-800;
}
.brand-small {
@apply text-xs text-gray-500;
}
.sub-section {
@apply mt-3;
}
.sub-title {
@apply text-sm font-semibold text-gray-800 mb-1;
}
.sub-text {
@apply text-sm text-gray-800 whitespace-pre-wrap break-words;
}
.empty {
@apply text-center py-10 text-gray-500;
}
.empty-small {
@apply text-center py-6 text-sm text-gray-500;
}
.empty .icon {
@apply text-3xl mb-2;
}
.empty .title {
@apply text-lg font-medium mb-1;
}
.empty .sub {
@apply text-sm;
}
</style>

126
src/ui/CQCXG4D2E.vue Normal file
View File

@@ -0,0 +1,126 @@
<template>
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<!-- 车辆总数统计 -->
<div class="flex justify-between items-center mb-6 pb-4 border-b border-gray-100">
<div class="flex items-center gap-3">
<div>
<div class="text-lg font-semibold text-gray-900">名下车辆(数量)</div>
</div>
</div>
<div class="bg-blue-50 text-blue-700 px-4 py-2 rounded-full text-sm font-medium">
{{ vehicleCount }}
</div>
</div>
<!-- 车辆列表 -->
<div class="space-y-3" v-if="vehicleList && vehicleList.length > 0">
<div class="bg-gray-50 rounded-lg p-4 border border-gray-200 hover:bg-blue-50 hover:border-blue-200 transition-colors duration-200"
v-for="(vehicle, index) in vehicleList" :key="index">
<div class="space-y-3">
<div class="text-xl font-bold text-gray-900 font-mono tracking-wider">
{{ vehicle.plateNum }}
</div>
<div class="flex items-center gap-3">
<div class="inline-flex items-center gap-1 px-3 py-1 rounded text-xs font-medium text-white"
:class="getPlateColorClass(vehicle.plateColor)">
<span>🏷</span>
<span>{{ getPlateColorText(vehicle.plateColor) }}</span>
</div>
<div class="text-sm text-gray-600">
<span class="text-gray-500">车辆类型:</span>
<span class="font-medium text-gray-900 ml-1">{{ getVehicleTypeText(vehicle.vehicleType)
}}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 无数据状态 -->
<div class="text-center py-12 text-gray-500" v-else>
<div class="text-4xl mb-3">🚫</div>
<div class="text-lg font-medium mb-1">暂无车辆信息</div>
<div class="text-sm">No vehicle records found</div>
</div>
</div>
</template>
<script setup>
import { defineProps, computed } from 'vue';
import { useRiskNotifier } from '@/composables/useRiskNotifier';
const props = defineProps({
data: Object,
params: Object,
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => { } },
});
const plateColorMap = {
0: '蓝色 - 普通燃油车',
1: '黄色 - 大型车/货车',
2: '黑色 - 外籍车辆/港澳台车',
3: '白色 - 警车/军车/武警车',
4: '渐变绿色 - 新能源汽车',
5: '黄绿双拼色 - 大型新能源汽车',
6: '蓝白渐变色 - 临时牌照',
7: '临时牌照 - 临时行驶车辆',
11: '绿色 - 新能源汽车',
12: '红色 - 教练车/试验车'
};
const vehicleTypeMap = {
1: '一型客车',
2: '二型客车',
3: '三型客车',
4: '四型客车',
11: '一型货车',
12: '二型货车',
13: '三型货车',
14: '四型货车',
15: '五型货车',
16: '六型货车',
21: '一型专项作业车',
22: '二型专项作业车',
23: '三型专项作业车',
24: '四型专项作业车',
25: '五型专项作业车',
26: '六型专项作业车'
};
const vehicleList = computed(() => props.data?.list || []);
const vehicleCount = computed(() => props.data?.vehicleCount || 0);
const getPlateColorText = (plateColor) => {
return plateColorMap[plateColor] || '未知颜色 - 未知类型';
};
const getPlateColorClass = (plateColor) => {
const colorClassMap = {
0: 'bg-blue-500',
1: 'bg-yellow-500',
2: 'bg-gray-800',
3: 'bg-gray-200 text-gray-800',
4: 'bg-green-500',
5: 'bg-gradient-to-r from-yellow-500 to-green-500',
6: 'bg-gradient-to-r from-blue-500 to-white text-blue-800',
7: 'bg-red-500',
11: 'bg-green-500',
12: 'bg-red-500'
};
return colorClassMap[plateColor] || 'bg-gray-500';
};
const getVehicleTypeText = (vehicleType) => {
return vehicleTypeMap[vehicleType] || '未知类型';
};
const riskScore = computed(() => 100);
useRiskNotifier(props, riskScore);
defineExpose({ riskScore });
</script>
<style scoped>
/* 保持与 CQCXG9P1C 一致的布局风格 */
</style>

256
src/ui/CQCXG4I1Z.vue Normal file
View File

@@ -0,0 +1,256 @@
<template>
<div class="card">
<div class="header-box">
<div class="header-left">
<h3 class="header-title">车辆过户详版查询</h3>
<p class="header-desc">按时间轴展示每一次车辆过户的车牌与地区变更情况</p>
</div>
<div class="header-tag" v-if="totalTimes">
<span class="tag-label">总过户次数</span>
<span class="tag-value">{{ totalTimes }} </span>
</div>
</div>
<template v-if="transfers && transfers.length">
<div class="timeline">
<div v-for="(item, index) in transfers" :key="index" class="timeline-item">
<div class="timeline-left">
<div class="dot-wrap">
<div class="dot"></div>
<div v-if="index !== transfers.length - 1" class="line"></div>
</div>
</div>
<div class="timeline-content">
<div class="transfer-header">
<div class="transfer-date">{{ item.changeMonthFormatted }}</div>
<div class="transfer-count"> {{ item.transTimeSum }} 次过户</div>
</div>
<div class="plates-row">
<div class="plate old">
<div class="label">过户前车牌</div>
<div class="value">{{ item.oldCp || '未知' }}</div>
<div class="city" v-if="item.cityBefore">所在城市{{ item.cityBefore }}</div>
</div>
<div class="arrow"></div>
<div class="plate new">
<div class="label">过户后车牌</div>
<div class="value">{{ item.newCp || '未知' }}</div>
<div class="city" v-if="item.cityAfter">所在城市{{ item.cityAfter }}</div>
</div>
</div>
<div class="interval-row">
<span>距上次过户</span>
<span class="strong">{{ item.intervalText }}</span>
</div>
<div class="vin-row">
<span class="vin-label">VIN</span>
<span class="vin-value font-mono">{{ item.vin || '-' }}</span>
</div>
</div>
</div>
</div>
</template>
<div v-else class="empty">
<div class="icon"></div>
<div class="title">暂无过户明细</div>
<div class="sub">未查询到车辆过户明细记录</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useRiskNotifier } from '@/composables/useRiskNotifier';
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 rawList = computed(() => Array.isArray(props.data?.retdata) ? props.data.retdata : []);
const transfers = computed(() =>
rawList.value.map((item) => {
const changeMonth = item.changeMonth;
let changeMonthFormatted = '-';
if (changeMonth === '近一年内过户') {
changeMonthFormatted = '近一年内过户';
} else if (typeof changeMonth === 'string' && changeMonth.length === 6) {
const y = changeMonth.slice(0, 4);
const m = changeMonth.slice(4, 6);
changeMonthFormatted = `${y}${m}`;
} else if (changeMonth) {
changeMonthFormatted = changeMonth;
}
let intervalText = '-';
if (item.transYear || item.transMonth) {
const years = item.transYear ? `${item.transYear}` : '';
const months = item.transMonth ? `${item.transMonth}个月` : '';
intervalText = `${years}${months}` || '-';
}
return {
...item,
changeMonthFormatted,
intervalText,
};
})
);
const totalTimes = computed(() => {
if (!transfers.value.length) return '';
const last = transfers.value[transfers.value.length - 1];
return last.transTimeSum ?? '';
});
const riskScore = computed(() => 100);
useRiskNotifier(props, riskScore);
defineExpose({ riskScore });
</script>
<style scoped>
.card {
@apply bg-white rounded-2xl p-6 shadow-sm border border-gray-100;
}
.header-box {
@apply flex items-center justify-between mb-5 px-5 py-4 rounded-2xl bg-gradient-to-r from-indigo-50 via-blue-50 to-indigo-50;
}
.header-left {
@apply flex flex-col;
}
.header-title {
@apply text-2xl font-semibold m-0 text-indigo-900;
}
.header-desc {
@apply text-base mt-3 m-0 text-indigo-800 opacity-90;
}
.header-tag {
@apply inline-flex flex-col items-end gap-1 px-3 py-2 rounded-xl bg-white/80 shadow-sm text-indigo-800;
}
.tag-label {
@apply text-sm text-gray-500;
}
.tag-value {
@apply text-2xl font-bold leading-none whitespace-nowrap;
}
.timeline {
@apply mt-4;
}
.timeline-item {
@apply flex mb-4;
}
.timeline-left {
@apply mr-3 flex flex-col items-center;
}
.dot-wrap {
@apply flex flex-col items-center;
}
.dot {
@apply w-3 h-3 rounded-full bg-indigo-500;
}
.line {
@apply flex-1 w-px bg-gray-300 mt-1;
}
.timeline-content {
@apply flex-1 rounded-2xl border border-gray-100 bg-gray-50/70 px-4 py-3;
}
.transfer-header {
@apply flex items-baseline justify-between mb-2;
}
.transfer-date {
@apply text-lg font-semibold text-gray-900;
}
.transfer-count {
@apply text-sm text-gray-600;
}
.plates-row {
@apply flex items-stretch gap-3 mt-2;
}
.plate {
@apply flex-1 rounded-xl px-3 py-2 border border-gray-200 bg-white;
}
.plate .label {
@apply text-xs text-gray-500 mb-1;
}
.plate .value {
@apply text-lg font-semibold text-gray-900;
}
.plate .city {
@apply text-sm text-gray-600 mt-1;
}
.plate.old {
@apply bg-gray-50;
}
.plate.new {
@apply bg-indigo-50/60 border-indigo-100;
}
.arrow {
@apply flex items-center justify-center text-2xl text-gray-400;
}
.interval-row {
@apply mt-3 text-base text-gray-700;
}
.interval-row .strong {
@apply font-semibold;
}
.vin-row {
@apply mt-2 text-sm text-gray-600;
}
.vin-label {
@apply text-gray-500;
}
.vin-value {
@apply text-base text-gray-900;
}
.empty {
@apply text-center py-10 text-gray-500;
}
.empty .icon {
@apply text-3xl mb-2;
}
.empty .title {
@apply text-lg font-medium mb-1;
}
.empty .sub {
@apply text-sm;
}
</style>

212
src/ui/CQCXG5U0Z.vue Normal file
View File

@@ -0,0 +1,212 @@
<template>
<div class="card">
<div class="header-box">
<div class="header-left">
<h3 class="header-title">车辆静态信息查询</h3>
<p class="header-desc">查看车辆生产排放标准及燃料等核心静态信息</p>
</div>
</div>
<template v-if="records && records.length">
<div v-for="(item, idx) in records" :key="idx" class="vehicle-card">
<div class="vehicle-title">
<div>
<div class="vehicle-chip">车辆 {{ idx + 1 }}</div>
<div class="vehicle-model">
{{ item.vType || '未知车型' }}
</div>
</div>
<div class="vehicle-meta">
<span class="badge">{{ item.vFuelType || '燃料未知' }}</span>
</div>
</div>
<div class="field-grid">
<div class="field">
<div class="field-label">发动机号</div>
<div class="field-value code">{{ item.engineNO || '-' }}</div>
</div>
<div class="field">
<div class="field-label">发动机型号</div>
<div class="field-value code">{{ item.engineType || '-' }}</div>
</div>
<div class="field">
<div class="field-label">生产日期</div>
<div class="field-value">{{ item.vScdate || '-' }}</div>
</div>
<div class="field">
<div class="field-label">排放阶段</div>
<div class="field-value">{{ item.dischargeStage || '-' }}</div>
</div>
<div class="field">
<div class="field-label">车辆分类</div>
<div class="field-value">{{ item.vClassification || '-' }}</div>
</div>
<div class="field field-span">
<div class="field-label">生产企业名称</div>
<div class="field-value">{{ item.vManufacturer || '-' }}</div>
</div>
<div class="field field-span">
<div class="field-label">生产厂地址</div>
<div class="field-value">{{ item.vSccdz || '-' }}</div>
</div>
</div>
</div>
</template>
<div v-else class="empty">
<div class="icon"></div>
<div class="title">暂无车辆静态信息</div>
<div class="sub">未查询到车辆静态信息或返回格式不正确</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useRiskNotifier } from '@/composables/useRiskNotifier';
const props = defineProps({
data: { type: [Object, String, Array], default: () => ({}) },
params: { type: Object, default: () => ({}) },
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => { } },
});
// 解析返回的 JSON 字符串,得到数组
const records = computed(() => {
const raw = props.data;
if (!raw) return [];
// data 本身是字符串
if (typeof raw === 'string') {
try {
const parsed = JSON.parse(raw);
if (Array.isArray(parsed)) return parsed;
return [];
} catch {
return [];
}
}
// data 已经是数组
if (Array.isArray(raw)) {
return raw;
}
// data 是对象,里层再包了一层字符串/数组的情况
if (typeof raw === 'object') {
if (Array.isArray(raw.list)) return raw.list;
if (typeof raw.data === 'string') {
try {
const parsed = JSON.parse(raw.data);
if (Array.isArray(parsed)) return parsed;
} catch {
return [];
}
}
}
return [];
});
const riskScore = computed(() => 100);
useRiskNotifier(props, riskScore);
defineExpose({ riskScore });
</script>
<style scoped>
.card {
@apply bg-white rounded-2xl p-6 shadow-sm border border-gray-100;
}
.header-box {
@apply rounded-2xl mb-5 px-5 py-4 bg-gradient-to-r from-blue-50 via-indigo-50 to-blue-50 flex items-center justify-between;
color: #1e40af;
}
.header-title {
@apply text-xl font-semibold m-0;
}
.header-desc {
@apply text-sm mt-2 m-0 opacity-80 text-gray-700;
}
.header-left {
@apply flex flex-col;
}
.header-tag {
@apply inline-flex items-center gap-2 px-3 py-1 rounded-full text-base font-medium bg-white/80 text-blue-700 shadow-sm;
}
.header-tag .dot {
@apply w-2 h-2 rounded-full bg-green-500;
}
.vehicle-card {
@apply mb-4 p-5 rounded-2xl border border-gray-100 bg-gray-50/80;
}
.vehicle-title {
@apply flex items-start justify-between text-base text-gray-700;
}
.vehicle-title .label {
@apply text-base uppercase tracking-wide text-gray-500;
}
.vehicle-title .value {
@apply font-semibold text-lg text-gray-900 mt-1;
}
.vehicle-model {
@apply text-base font-semibold text-gray-900 mt-1;
}
.vehicle-meta {
@apply flex items-center gap-2;
}
.badge {
@apply inline-flex items-center px-3 py-1 rounded-full text-base font-medium bg-blue-100 text-blue-700;
}
.field-label {
@apply text-base text-gray-500 mb-1;
}
.field-value {
@apply text-base text-gray-900;
}
.field-value.code {
@apply font-mono tracking-wide;
}
.field-grid {
@apply grid gap-y-3 gap-x-6 mt-4;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}
.field-span {
grid-column: 1 / -1;
}
.empty {
@apply text-center py-10 text-gray-500;
}
.empty .icon {
@apply text-3xl mb-2;
}
.empty .title {
@apply text-base font-medium mb-1;
}
.empty .sub {
@apply text-base;
}
</style>

493
src/ui/CQCXG6B4E.vue Normal file
View File

@@ -0,0 +1,493 @@
<template>
<div class="card">
<div class="header-box">
<div class="header-left">
<h3 class="header-title">车辆出险记录核验</h3>
<p class="header-desc">综合车辆出险脱保重大事故等信息评估风险</p>
</div>
</div>
<template v-if="hasData">
<div class="risk-band" :class="riskLevelClass">
<div class="risk-band-label">风险等级</div>
<div class="risk-band-text">{{ riskLevelText }}</div>
</div>
<!-- 顶部车辆与价格概览 -->
<div class="summary-card">
<div class="summary-main">
<div class="summary-left">
<div class="plate" v-if="data.LicensePlate">{{ data.LicensePlate }}</div>
<div class="car-type">{{ data.CarType || '未知车型' }}</div>
</div>
<div class="summary-right">
<div class="summary-label">二手车价格参考</div>
<div class="summary-price">{{ usedCarPriceText }}</div>
<div class="summary-sub">新车购置价{{ newCarPriceText }}</div>
</div>
</div>
<div class="summary-meta">
<div class="meta-line">
<span>燃料</span><span class="strong">{{ data.FuelType || '未知' }}</span>
<span class="dot"></span>
<span>发动机号</span><span class="strong code">{{ data.EngineNumber || '-' }}</span>
</div>
<div class="meta-line">
<span>初登日期</span><span class="strong">{{ data.DebutDate || '-' }}</span>
<span class="dot"></span>
<span>车龄</span><span class="strong">{{ carAgeText }}</span>
</div>
</div>
</div>
<!-- 核心风险指标 -->
<div class="detail-card">
<h4 class="section-title">核心风险指标</h4>
<div class="field-grid">
<div class="field">
<div class="field-label">是否高风险车辆</div>
<div class="field-value" :class="flagClass(data.IfHighriskVehicle === '1')">
{{ yesNoText(data.IfHighriskVehicle, '高风险车辆') }}
</div>
</div>
<div class="field">
<div class="field-label">是否营运车辆</div>
<div class="field-value" :class="flagClass(data.IsOperation === '1')">
{{ yesNoText(data.IsOperation, '营运车辆') }}
</div>
</div>
<div class="field">
<div class="field-label">是否投保车损险</div>
<div class="field-value" :class="flagClass(data.IfCarDamage === '1')">
{{ yesNoText(data.IfCarDamage, '已投保车损险', '未投保车损险') }}
</div>
</div>
<div class="field">
<div class="field-label">是否连续投保</div>
<div class="field-value" :class="flagClass(data.IsConInsure === '1')">
{{ yesNoText(data.IsConInsure, '连续投保', '非连续投保') }}
</div>
</div>
<div class="field">
<div class="field-label">历史是否脱保</div>
<div class="field-value" :class="flagClass(data.IfTuoBao === '1')">
{{ yesNoText(data.IfTuoBao, '有脱保记录', '无脱保记录') }}
</div>
</div>
<div class="field">
<div class="field-label">历史最大脱保时间</div>
<div class="field-value">{{ data.TuoBaoTime || '-' }}</div>
</div>
<div class="field">
<div class="field-label">最高车损险损失比例</div>
<div class="field-value">{{ data.CompensationRatioo || '-' }}</div>
</div>
<div class="field">
<div class="field-label">车损险综合评分</div>
<div class="field-value strong">{{ data.Total || '-' }}</div>
</div>
</div>
</div>
<!-- 出险与事故情况 -->
<div class="detail-card">
<h4 class="section-title">出险与事故情况</h4>
<div class="field-grid">
<div class="field">
<div class="field-label">商业险出险</div>
<div class="field-value">{{ formatDangerCount(data.CommercialPolicyDangerCount, '商业险') }}</div>
</div>
<div class="field">
<div class="field-label">交强险出险</div>
<div class="field-value">{{ formatDangerCount(data.CompulsoryPolicyDangerCount, '交强险') }}</div>
</div>
<div class="field">
<div class="field-label">三者险出险次数</div>
<div class="field-value">{{ formatDangerCount(data.ThreeRisksDangerCount, '三者险') }}</div>
</div>
<div class="field">
<div class="field-label">全损情况</div>
<div class="field-value">{{ totalLossText }}</div>
</div>
<div class="field field-span">
<div class="field-label">重大事故标志</div>
<div class="field-value">{{ formatMajorAccident(data.MajorAccident) }}</div>
</div>
<div class="field">
<div class="field-label">事故次数</div>
<div class="field-value">{{ data.IsMajorAccidentData || '-' }}</div>
</div>
<div class="field">
<div class="field-label">事故等级</div>
<div class="field-value">{{ data.IsMajorAccidentLevel || '-' }}</div>
</div>
<div class="field field-span">
<div class="field-label">损失部位</div>
<div class="field-value">{{ formatLossPart(data.LossPart) }}</div>
</div>
</div>
</div>
<!-- 保单与责任险可保情况 -->
<div class="detail-card">
<h4 class="section-title">保单与责任险承保情况</h4>
<div class="field-grid">
<div class="field">
<div class="field-label">商业险保单倒计时</div>
<div class="field-value">{{ formatPolicyTime(data.CommercialPolicyTime, '商业险') }}</div>
</div>
<div class="field">
<div class="field-label">交强险保单倒计时</div>
<div class="field-value">{{ formatPolicyTime(data.CompulsoryPolicyTime, '交强险') }}</div>
</div>
<div class="field">
<div class="field-label">商业险过户次数</div>
<div class="field-value">{{ formatTransferCount(data.CommercialPolicyTransferCount) }}</div>
</div>
<div class="field">
<div class="field-label">交强险过户次数</div>
<div class="field-value">{{ formatTransferCount(data.CompulsoryPolicyTransferCount) }}</div>
</div>
<div class="field">
<div class="field-label">是否可投保责任险</div>
<div class="field-value" :class="flagClass(data.IsLiabilityAvailable === 'Y')">
{{ ynText(data.IsLiabilityAvailable, '可投保', '不可投保') }}
</div>
</div>
<div class="field">
<div class="field-label">是否可承保延保</div>
<div class="field-value" :class="flagClass(data.IsExtendAvailable === 'Y')">
{{ ynText(data.IsExtendAvailable, '可承保', '不可承保') }}
</div>
</div>
</div>
</div>
</template>
<div v-else class="empty">
<div class="icon"></div>
<div class="title">暂无出险记录</div>
<div class="sub">未查询到车辆出险记录或返回数据为空</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useRiskNotifier } from '@/composables/useRiskNotifier';
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 data = computed(() => props.data || {});
const hasData = computed(() => !!data.value && Object.keys(data.value).length > 0);
const usedCarPriceText = computed(() => {
const v = data.value.UsedCarPrice;
if (!v) return '-';
return `${v}`;
});
const newCarPriceText = computed(() => {
const v = data.value.PurchasePrice;
if (!v) return '-';
return `${v}`;
});
const carAgeText = computed(() => {
const m = data.value.CarAge;
if (!m) return '-';
return `${m} 个月`;
});
const totalLossText = computed(() => {
const v = data.value.TotalLoss;
if (v === '1') return '存在全损记录';
if (v === '0') return '无全损记录';
return '-';
});
// 简单按高风险车辆/重大事故等情况给出一个文字风险等级
const riskLevelText = computed(() => {
if (data.value.IfHighriskVehicle === '1') return '高风险';
if (data.value.IsMajorAccidentLevel && data.value.IsMajorAccidentLevel !== '一般') return '较高风险';
if (data.value.IsMajorAccidentData && data.value.IsMajorAccidentData !== '0') return '有事故记录';
return '风险可控';
});
const riskLevelClass = computed(() => {
const t = riskLevelText.value;
if (t === '高风险') return 'risk-high';
if (t === '较高风险' || t === '有事故记录') return 'risk-mid';
return 'risk-low';
});
const flagClass = (flag) => {
return flag ? 'flag-yes' : 'flag-no';
};
const yesNoText = (val, yesText, noText = '否') => {
if (val === '1') return yesText;
if (val === '0') return noText;
return '-';
};
const ynText = (val, yesText, noText) => {
if (val === 'Y') return yesText;
if (val === 'N') return noText;
return '-';
};
const formatPolicyTime = (val, label) => {
if (!val || val === 'NULL') {
return `当期无${label}保单`;
}
const parts = String(val).split(':');
if (parts.length < 2) return val;
const daysRaw = parts[1];
if (!daysRaw || daysRaw.toLowerCase() === 'null') {
return `${label}保单已过期`;
}
const days = Number(daysRaw);
if (Number.isNaN(days)) return val;
if (days < 0) return `${label}保单已过期`;
return `${label}保单剩余 ${days}`;
};
const formatDangerCount = (val, label) => {
if (!val) return '-';
const parts = String(val).split(':');
const countRaw = parts[1] ?? '';
if (!countRaw || countRaw.toLowerCase() === 'null') {
return `${label}暂无出险记录`;
}
const count = Number(countRaw);
if (Number.isNaN(count)) return val;
if (count === 0) return `${label}暂无出险记录`;
return `${label}出险 ${count}`;
};
const formatTransferCount = (val) => {
if (!val) return '-';
const parts = String(val).split(':');
const countRaw = parts[1] ?? '';
if (!countRaw || countRaw.toLowerCase() === 'null') return '-';
const count = Number(countRaw);
if (Number.isNaN(count)) return val;
return `${count}`;
};
const formatMajorAccident = (val) => {
if (!val) return '-';
const map = {
A: '碰撞',
B: '火自燃',
C: '水淹',
D: '盗抢',
};
const list = [];
String(val)
.split(',')
.forEach((pair) => {
const [k, v] = pair.split(':');
if (v === '1' && map[k]) {
list.push(map[k]);
}
});
if (!list.length) return '无重大事故记录';
return `重大事故类型:${list.join('、')}`;
};
const formatLossPart = (val) => {
if (!val) return '-';
const partMap = {
1: '正前方',
2: '正后方',
3: '顶部',
4: '底部',
5: '前方左侧',
6: '后方左侧',
7: '中间左侧',
8: '前方右侧',
9: '后方右侧',
10: '中间右侧',
11: '内部',
12: '其它',
13: '不详',
};
const items = [];
String(val)
.split(',')
.forEach((pair) => {
const [kRaw, vRaw] = pair.split(':');
const key = Number(kRaw);
const count = Number(vRaw);
if (!Number.isNaN(key) && !Number.isNaN(count) && count > 0) {
const label = partMap[key] || `部位${key}`;
items.push(`${label}${count}`);
}
});
if (!items.length) return '暂无损失部位信息';
return items.join('、');
};
const riskScore = computed(() => 100);
useRiskNotifier(props, riskScore);
defineExpose({ riskScore });
</script>
<style scoped>
.card {
@apply bg-white rounded-2xl p-6 shadow-sm border border-gray-100;
}
.header-box {
@apply flex items-center mb-3 px-5 py-4 rounded-2xl bg-gradient-to-r from-rose-50 via-orange-50 to-amber-50;
}
.header-left {
@apply flex flex-col;
}
.header-title {
@apply text-2xl font-semibold m-0 text-rose-900;
}
.header-desc {
@apply text-base mt-3 m-0 text-rose-800 opacity-90;
}
.risk-band {
@apply mb-4 px-4 py-3 rounded-2xl flex items-center justify-between;
}
.risk-band.risk-high {
@apply bg-red-50 border border-red-100;
}
.risk-band.risk-mid {
@apply bg-amber-50 border border-amber-100;
}
.risk-band.risk-low {
@apply bg-emerald-50 border border-emerald-100;
}
.risk-band-label {
@apply text-sm text-gray-600;
}
.risk-band-text {
@apply text-xl font-bold;
}
.summary-card {
@apply rounded-2xl border border-amber-100 bg-amber-50/60 px-5 py-4 mb-4;
}
.summary-main {
@apply flex items-start justify-between mb-3 gap-4;
}
.summary-left {
@apply flex flex-col gap-2;
}
.plate {
@apply inline-flex items-center px-4 py-2 rounded-full bg-slate-900 text-white text-xl font-semibold tracking-widest;
}
.car-type {
@apply text-lg font-medium text-gray-800;
}
.summary-right {
@apply text-right;
}
.summary-label {
@apply text-sm text-gray-500;
}
.summary-price {
@apply text-2xl font-bold text-amber-800 mt-1;
}
.summary-sub {
@apply text-sm text-amber-700 opacity-90;
}
.summary-meta {
@apply space-y-1 text-base text-gray-800;
}
.meta-line {
@apply flex flex-wrap items-center gap-2;
}
.meta-line .dot {
@apply w-1 h-1 rounded-full bg-gray-400;
}
.strong {
@apply font-semibold;
}
.code {
@apply font-mono tracking-wide;
}
.detail-card {
@apply rounded-2xl border border-gray-100 bg-gray-50/60 px-5 py-4 mb-4;
}
.section-title {
@apply text-base font-semibold text-gray-800 mb-3;
}
.field-grid {
@apply grid gap-y-3 gap-x-6;
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
}
.field-label {
@apply text-sm text-gray-500 mb-1;
}
.field-value {
@apply text-base text-gray-900;
}
.field-span {
grid-column: 1 / -1;
}
.flag-yes {
@apply text-red-700;
}
.flag-no {
@apply text-emerald-700;
}
.empty {
@apply text-center py-10 text-gray-500;
}
.empty .icon {
@apply text-3xl mb-2;
}
.empty .title {
@apply text-lg font-medium mb-1;
}
.empty .sub {
@apply text-sm;
}
</style>

216
src/ui/CQCXGGB2Q.vue Normal file
View File

@@ -0,0 +1,216 @@
<template>
<div class="card">
<div class="header-box">
<h3 class="header-title">人车核验简版</h3>
<p class="header-desc">校验人员姓名与车辆号牌是否匹配</p>
</div>
<div class="result-section" :class="resultSectionClass">
<div class="result-icon-wrap">
<span class="result-icon" :class="iconClass">
{{ iconChar }}
</span>
</div>
<div class="result-label">核验结果</div>
<div class="result-value" :class="resultClass">{{ resultText }}</div>
</div>
<div v-if="hasParams" class="info-rows">
<div class="info-row">
<span class="info-label">姓名</span>
<span class="info-value">{{ maskedName }}</span>
</div>
<div class="info-row">
<span class="info-label">车牌号</span>
<span class="info-value font-mono">{{ params?.plate_no || params?.car_license || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">号牌类型</span>
<span class="info-value">{{ params?.carplate_type || params?.car_type || '-' }}</span>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useRiskNotifier } from '@/composables/useRiskNotifier';
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 maskedName = computed(() => {
const name = props.params?.name || '';
if (!name) return '-';
return name.length > 1 ? name[0] + '*'.repeat(name.length - 1) : '*';
});
// verify_code: 1 一致2 不匹配
const isMatch = computed(() => {
const code = props.data?.verify_code;
if (code === 1) return true;
if (code === 2) return false;
return null; // 无有效数据时
});
const resultText = computed(() => {
if (isMatch.value === true) return '一致';
if (isMatch.value === false) return '不匹配';
return '暂无结果';
});
const resultClass = computed(() => {
if (isMatch.value === true) return 'result-match';
if (isMatch.value === false) return 'result-mismatch';
return 'result-unknown';
});
const resultSectionClass = computed(() => {
if (isMatch.value === true) return 'result-section match';
if (isMatch.value === false) return 'result-section mismatch';
return 'result-section unknown';
});
const iconClass = computed(() => {
if (isMatch.value === true) return 'icon-match';
if (isMatch.value === false) return 'icon-mismatch';
return 'icon-unknown';
});
const iconChar = computed(() => {
if (isMatch.value === true) return '✓';
if (isMatch.value === false) return '✕';
return '?';
});
const hasParams = computed(() => {
const p = props.params || {};
return p.name || p.plate_no || p.car_license || p.carplate_type || p.car_type;
});
// 简版人车核验本身不直接计为负面风险,给满分
const riskScore = computed(() => 100);
useRiskNotifier(props, riskScore);
defineExpose({
riskScore,
});
</script>
<style scoped>
.card {
@apply bg-white rounded-lg p-4 shadow-sm border border-gray-100;
}
.header-box {
@apply rounded-lg mb-4 p-4;
background: linear-gradient(135deg, var(--van-theme-primary) 0%, var(--van-theme-primary-dark, #1565c0) 100%);
color: #fff;
}
.header-title {
@apply text-lg font-semibold m-0;
}
.header-desc {
@apply text-sm mt-1 opacity-90 m-0;
}
.result-section {
@apply rounded-xl p-5 text-center mb-4;
}
.result-section.match {
background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);
border: 1px solid rgba(76, 175, 80, 0.3);
}
.result-section.mismatch {
background: linear-gradient(135deg, #ffebee 0%, #ffcdd2 100%);
border: 1px solid rgba(244, 67, 54, 0.3);
}
.result-section.unknown {
@apply bg-gray-50 border border-gray-200;
}
.result-icon-wrap {
@apply mb-2;
}
.result-icon {
@apply inline-flex items-center justify-center w-12 h-12 rounded-full text-2xl font-bold text-white;
}
.result-icon.icon-match {
background: linear-gradient(135deg, #43a047 0%, #2e7d32 100%);
box-shadow: 0 2px 8px rgba(76, 175, 80, 0.4);
}
.result-icon.icon-mismatch {
background: linear-gradient(135deg, #e53935 0%, #c62828 100%);
box-shadow: 0 2px 8px rgba(244, 67, 54, 0.4);
}
.result-icon.icon-unknown {
background: linear-gradient(135deg, #78909c 0%, #546e7a 100%);
box-shadow: 0 2px 8px rgba(96, 125, 139, 0.3);
}
.result-label {
@apply text-sm text-gray-500 mb-1;
}
.result-value {
@apply text-xl font-semibold;
}
.result-match {
color: #2e7d32;
}
.result-mismatch {
color: #c62828;
}
.result-unknown {
@apply text-gray-500;
}
.info-rows {
@apply space-y-3 pt-2 border-t border-gray-100;
}
.info-row {
@apply flex items-center text-sm;
}
.info-label {
@apply w-20 text-gray-500 shrink-0;
}
.info-value {
@apply font-medium text-gray-800;
}
</style>

634
src/ui/CQCXGP00W.vue Normal file
View File

@@ -0,0 +1,634 @@
<template>
<div class="card">
<div class="header-box">
<div class="header-left">
<h3 class="header-title">车辆出险详版查询</h3>
<p class="header-desc">展示多维出险记录碰撞部位及车况信息辅助评估车辆风险</p>
</div>
</div>
<template v-if="hasData">
<!-- 车辆基本信息 + 碰撞统计 -->
<div class="summary-card">
<div class="summary-main">
<div class="summary-left">
<div class="summary-label">品牌名称</div>
<div class="summary-brand">{{ clxx.brandName || '未知品牌' }}</div>
<div class="summary-subline" v-if="clxx.vehicleStyle">
{{ clxx.vehicleStyle }}
</div>
</div>
<div class="summary-right">
<div class="summary-label">车架号 VIN</div>
<div class="summary-vin font-mono">{{ pzVin || clxxVin || '-' }}</div>
<div class="summary-subline" v-if="clxx.licensePlate">
车牌号{{ clxx.licensePlate }}
</div>
</div>
</div>
<div class="summary-meta">
<div class="meta-line">
<span class="meta-label">事故总次数</span>
<span class="meta-value strong">{{ tjxx.claimCount ?? '-' }}</span>
</div>
<div class="meta-line">
<span class="meta-label">总维修金额</span>
<span class="meta-value strong">{{ tjxx.totalAmount || '-' }}</span>
</div>
<div class="meta-line">
<span class="meta-label">最大单次维修金额</span>
<span class="meta-value strong">{{ tjxx.largestAmount || '-' }}</span>
</div>
<div class="meta-line">
<span class="meta-label">已结案次数</span>
<span class="meta-value strong">{{ tjxx.claimCacCount ?? 0 }} </span>
</div>
<div class="meta-line">
<span class="meta-label">未结案次数</span>
<span class="meta-value strong">{{ tjxx.claimUnCacCount ?? 0 }} </span>
</div>
</div>
</div>
<!-- 碰撞记录时间轴 -->
<div class="detail-card" v-if="pzRecords && pzRecords.length">
<h4 class="section-title">碰撞出险记录</h4>
<div class="timeline">
<div v-for="(rec, idx) in pzRecords" :key="idx" class="timeline-item">
<div class="timeline-content">
<div class="row-main">
<div class="date">{{ rec.date || '-' }}</div>
<div class="amount">{{ formatFen(rec.serviceMoney) }}</div>
</div>
<div class="row-sub">
<span>{{ rec.accidentType || '出险' }}</span>
<span class="status">{{ rec.claimStatus || '-' }}</span>
</div>
<div class="sub-section" v-if="rec.result && rec.result.length">
<div class="sub-title">维修明细</div>
<ul class="sub-list">
<li v-for="(d, di) in rec.result" :key="di">
<span class="tag">{{ dangerTypeText(d.dangerSingleType) }}</span>
<span>{{ d.dangerSingleName }}</span>
<span v-if="d.dangerSingleNum">×{{ d.dangerSingleNum }}</span>
<span v-if="d.dangerSingleMoney" class="money">
{{ formatFen(d.dangerSingleMoney) }}
</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- 车况排查大类 -->
<div class="detail-card" v-if="ckdlpc">
<h4 class="section-title">车况排查大类</h4>
<div class="ckdlpc-grid">
<div v-for="item in ckdlpcList" :key="item.key" class="ckdlpc-item">
<div class="ckdlpc-name">{{ item.label }}</div>
<div class="ckdlpc-status" :class="ckLevelClass(item.value)">
{{ ckLevelText(item.value) }}
</div>
</div>
</div>
</div>
<!-- 车况明细排查部件所有分组都展示如整体无命中则显示一行暂无车况明细排查记录 -->
<div class="detail-card" v-if="ckpclbGroups && ckpclbGroups.length">
<h4 class="section-title">车况明细排查部件</h4>
<template v-if="hasCkpclbHit">
<div class="ckpclb-grid">
<div v-for="group in ckpclbGroups" :key="group && group.key" v-if="group" class="ckpclb-group">
<div class="ckpclb-title">{{ group.label }}</div>
<div class="ckpclb-tags" v-if="group.items && group.items.length">
<span v-for="(p, pi) in group.items" :key="pi" class="part-tag part-tag-hit">
{{ p.name }}{{ p.type }}
</span>
</div>
<div v-else class="ckpclb-empty">无相关排查记录</div>
</div>
</div>
</template>
<div v-else class="text-sm text-gray-500">暂无车况明细排查记录</div>
</div>
<!-- 车辆损失方位总结所有方位按矩阵展示有受损的高亮显示 -->
<div class="detail-card" v-if="clfwzjMatrix && clfwzjMatrix.length">
<h4 class="section-title">车辆损失方位总结</h4>
<div class="clfwzj-matrix">
<div v-for="(row, ri) in clfwzjMatrix" :key="ri" class="clfwzj-row">
<div v-for="(cell, ci) in row" :key="ci" class="clfwzj-cell">
<div v-if="cell && cell.label"
:class="['pos-box', cell.value === 1 ? 'pos-box-hit' : 'pos-box-normal']">
{{ cell.label }}
</div>
</div>
</div>
</div>
<p class="mt-4 text-sm text-gray-500">红色方位表示该部位存在受损记录灰色表示当前无受损记录</p>
</div>
<!-- 车况信息简要 -->
<div class="detail-card" v-if="ckxx">
<h4 class="section-title">车况信息概览</h4>
<div class="field-grid">
<div class="field">
<div class="field-label">是否火烧</div>
<div class="field-value" :class="flagClass(ckxx.isFire === 1)">
{{ bool01Text(ckxx.isFire) }}
</div>
</div>
<div class="field">
<div class="field-label">是否水淹</div>
<div class="field-value" :class="flagClass(ckxx.isFlood === 1)">
{{ bool01Text(ckxx.isFlood) }}
</div>
</div>
<div class="field">
<div class="field-label">是否偷盗</div>
<div class="field-value" :class="flagClass(ckxx.isTheft === 1)">
{{ bool01Text(ckxx.isTheft) }}
</div>
</div>
<div class="field">
<div class="field-label">是否覆盖件损伤</div>
<div class="field-value" :class="flagClass(ckxx.isPanel === 1)">
{{ bool01Text(ckxx.isPanel) }}
</div>
</div>
<div class="field">
<div class="field-label">是否大额赔偿</div>
<div class="field-value">
{{ largeCostText(ckxx.isLargeCost) }}
</div>
</div>
<div class="field">
<div class="field-label">未结案记录</div>
<div class="field-value">{{ ynUnknown(ckxx.recordIcpending) }}</div>
</div>
<div class="field">
<div class="field-label">注销记录</div>
<div class="field-value">{{ ynUnknown(ckxx.recordIwriteoff) }}</div>
</div>
<div class="field">
<div class="field-label">拒赔记录</div>
<div class="field-value">{{ ynUnknown(ckxx.refusalRecord) }}</div>
</div>
</div>
</div>
</template>
<div v-else class="empty">
<div class="icon"></div>
<div class="title">暂无出险详版数据</div>
<div class="sub">未查询到车辆详细出险记录或返回数据为空</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useRiskNotifier } from '@/composables/useRiskNotifier';
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 retdata = computed(() => props.data?.retdata || {});
const hasData = computed(() => !!retdata.value && Object.keys(retdata.value).length > 0);
const pzlsmx = computed(() => retdata.value.pzlsmx || {});
const pzRecords = computed(() => Array.isArray(pzlsmx.value.records) ? pzlsmx.value.records : []);
const pzVin = computed(() => (pzRecords.value[0]?.vin) || '');
const ckdlpc = computed(() => retdata.value.ckdlpc || null);
const ckxx = computed(() => retdata.value.ckxx || null);
const ckpclb = computed(() => retdata.value.ckpclb || null);
const clxx = computed(() => retdata.value.clxx || {});
const clfwzj = computed(() => retdata.value.clfwzj || null);
const tjxx = computed(() => retdata.value.tjxx || {});
const clxxVin = computed(() => clxx.value.vin || '');
const ckdlpcList = computed(() => {
if (!ckdlpc.value) return [];
const map = [
{ key: 'type1', label: '骨架' },
{ key: 'type2', label: '外观' },
{ key: 'type3', label: '发动机/变速箱' },
{ key: 'type4', label: '火烧' },
{ key: 'type5', label: '水淹' },
{ key: 'type6', label: '气囊' },
{ key: 'type7', label: '加强件' },
];
return map.map((m) => ({
key: m.key,
label: m.label,
value: ckdlpc.value[m.key],
}));
});
const hasCkpclb = computed(() => {
const v = ckpclb.value;
if (!v) return false;
return Object.values(v).some((arr) => Array.isArray(arr) && arr.length > 0);
});
const hasCkpclbHit = computed(() => {
const v = ckpclb.value;
if (!v) return false;
return Object.values(v).some((arr) => Array.isArray(arr) && arr.length > 0);
});
const ckpclbGroups = computed(() => {
if (!ckpclb.value) return [];
const labelMap = {
dp: '底盘悬挂',
fdj: '发动机',
fspj: '附属配件',
gj: '骨架',
hs: '火烧',
jqj: '加强件',
qn: '气囊',
sy: '水淹',
wg: '外观',
};
return Object.entries(ckpclb.value).map(([key, arr]) => ({
key,
label: labelMap[key] || key,
items: Array.isArray(arr) ? arr : [],
}));
});
const hasClfwzj = computed(() => {
if (!clfwzj.value) return false;
return Object.values(clfwzj.value).some((v) => v === 1);
});
const clfwzjMatrix = computed(() => {
const src = clfwzj.value || {};
const val = (key) => (src[key] === 1 ? 1 : 0);
// 按大致方位排布成矩阵,便于理解
return [
[
{ label: '', value: null },
{ label: '正前方', value: val('正前方') },
{ label: '', value: null },
],
[
{ label: '前方左侧', value: val('前方左侧') },
{ label: '顶部', value: val('顶部') },
{ label: '前方右侧', value: val('前方右侧') },
],
[
{ label: '中间左侧', value: val('中间左侧') },
{ label: '内部', value: val('内部') },
{ label: '中间右侧', value: val('中间右侧') },
],
[
{ label: '后方左侧', value: val('后方左侧') },
{ label: '底部', value: val('底部') },
{ label: '后方右侧', value: val('后方右侧') },
],
[
{ label: '', value: null },
{ label: '正后方', value: val('正后方') },
{ label: '其他', value: val('其他') },
],
];
});
const ckLevelText = (v) => {
const num = Number(v);
if (Number.isNaN(num)) return '未知';
if (num === 0) return '正常';
if (num === 1) return '无法确定';
if (num === 2) return '疑似异常';
if (num === 3) return '维保异常';
if (num === 4) return '碰撞异常';
return '未知';
};
const ckLevelClass = (v) => {
const num = Number(v);
if (num === 0) return 'level-ok';
if (num === 1) return 'level-unknown';
if (num === 2) return 'level-suspect';
if (num === 3) return 'level-maintain';
if (num === 4) return 'level-collision';
return 'level-unknown';
};
const formatFen = (val) => {
if (!val && val !== 0) return '-';
const n = Number(val);
if (Number.isNaN(n)) return `${val}`;
const yuan = n / 100;
return `${yuan.toLocaleString()}`;
};
const dangerTypeText = (t) => {
if (t === '1') return '更换';
if (t === '2') return '维修';
if (t === '3') return '材料';
return '其他';
};
const bool01Text = (v) => {
if (v === 1) return '是';
if (v === 0) return '否';
return '未知';
};
const largeCostText = (v) => {
if (v === 0) return '无大额赔偿记录';
if (v === 1) return '有大额赔偿记录';
if (v === 2) return '无法确定是否大额赔偿';
return '未知';
};
const ynUnknown = (v) => {
if (v === '是') return '是';
if (v === '否') return '否';
if (v == null) return '未知';
return v;
};
const flagClass = (flag) => {
return flag ? 'flag-yes' : 'flag-no';
};
const riskScore = computed(() => 100);
useRiskNotifier(props, riskScore);
defineExpose({ riskScore });
</script>
<style scoped>
.card {
@apply bg-white rounded-2xl p-6 shadow-sm border border-gray-100;
}
.header-box {
@apply flex items-center mb-4 px-5 py-4 rounded-2xl bg-gradient-to-r from-orange-50 via-amber-50 to-rose-50;
}
.header-left {
@apply flex flex-col;
}
.header-title {
@apply text-2xl font-semibold m-0 text-orange-900;
}
.header-desc {
@apply text-base mt-3 m-0 text-orange-800 opacity-90;
}
.summary-card {
@apply rounded-2xl border border-amber-100 bg-amber-50/60 px-5 py-4 mb-4;
}
.summary-main {
@apply flex items-start justify-between mb-3 gap-4;
}
.summary-left {
@apply flex flex-col gap-1;
}
.summary-right {
@apply text-right;
}
.summary-label {
@apply text-sm text-gray-500;
}
.summary-brand {
@apply text-lg font-semibold text-gray-900;
}
.summary-vin {
@apply text-base text-gray-900;
}
.summary-subline {
@apply text-sm text-gray-700 mt-1;
}
.summary-meta {
@apply space-y-2 text-base text-gray-800;
}
.meta-line {
@apply flex justify-between items-center;
}
.meta-label {
@apply text-sm text-gray-600;
}
.meta-value {
@apply text-base;
}
.meta-line .dot {
@apply w-1 h-1 rounded-full bg-gray-400;
}
.strong {
@apply font-semibold;
}
.code {
@apply font-mono tracking-wide;
}
.detail-card {
@apply rounded-2xl border border-gray-100 bg-gray-50/60 px-5 py-4 mb-4;
}
.section-title {
@apply text-base font-semibold text-gray-800 mb-3;
}
.timeline {
@apply mt-2;
}
.timeline-item {
@apply flex-1 rounded-2xl border border-gray-100 bg-white px-4 py-3;
}
.row-main {
@apply flex items-baseline justify-between mb-1;
}
.row-main .date {
@apply text-base font-medium text-gray-900;
}
.row-main .amount {
@apply text-lg font-semibold text-gray-900;
}
.row-sub {
@apply flex items-center justify-between text-sm text-gray-600 mt-1;
}
.status {
@apply text-xs px-2 py-0.5 rounded-full bg-emerald-50 text-emerald-700 font-medium;
}
.sub-section {
@apply mt-3;
}
.sub-title {
@apply text-sm font-semibold text-gray-800 mb-1;
}
.sub-list {
@apply text-sm text-gray-800 space-y-1;
}
.sub-list li {
@apply flex flex-wrap gap-1;
}
.tag {
@apply inline-flex items-center px-2 py-0.5 rounded-full bg-orange-50 text-orange-700 text-xs font-medium;
}
.money {
@apply text-xs text-gray-500;
}
.ckdlpc-grid {
@apply grid gap-3;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
.ckdlpc-item {
@apply p-3 rounded-xl bg-white border border-gray-100;
}
.ckdlpc-name {
@apply text-sm text-gray-600 mb-1;
}
.ckdlpc-status {
@apply text-sm font-semibold;
}
.level-ok {
@apply text-emerald-700;
}
.level-unknown {
@apply text-gray-600;
}
.level-suspect {
@apply text-amber-700;
}
.level-maintain {
@apply text-blue-700;
}
.level-collision {
@apply text-red-700;
}
.ckpclb-grid {
@apply grid gap-3;
}
.ckpclb-group {
@apply p-3 rounded-xl bg-white border border-gray-100;
}
.ckpclb-title {
@apply text-sm font-semibold text-gray-800 mb-2;
}
.ckpclb-tags {
@apply flex flex-wrap gap-2;
}
.part-tag {
@apply inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium;
}
.part-tag-hit {
@apply bg-sky-50 text-sky-700;
}
.clfwzj-matrix {
@apply grid gap-2;
}
.clfwzj-row {
@apply grid gap-2;
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.clfwzj-cell {
@apply flex items-center justify-center;
}
.pos-box {
@apply w-full text-center px-3 py-2 rounded-xl text-xs font-medium;
}
.pos-box-hit {
@apply bg-rose-50 text-rose-700;
}
.pos-box-normal {
@apply bg-gray-100 text-gray-500;
}
.field-grid {
@apply grid gap-y-3 gap-x-6;
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
}
.field-label {
@apply text-sm text-gray-500 mb-1;
}
.field-value {
@apply text-base text-gray-900;
}
.flag-yes {
@apply text-red-700;
}
.flag-no {
@apply text-emerald-700;
}
.empty {
@apply text-center py-10 text-gray-500;
}
.empty .icon {
@apply text-3xl mb-2;
}
.empty .title {
@apply text-lg font-medium mb-1;
}
.empty .sub {
@apply text-sm;
}
</style>

228
src/ui/CQCXGY7F2.vue Normal file
View File

@@ -0,0 +1,228 @@
<template>
<div class="card">
<div class="header-box">
<div class="header-left">
<h3 class="header-title">二手车VIN估值</h3>
<p class="header-desc">基于车型排量排放标准等信息给出参考估值</p>
</div>
<div class="header-tag">
<span class="tag-label">估值结果</span>
<span class="tag-value">{{ data.estimatedValue || '-' }}</span>
</div>
</div>
<template v-if="hasData">
<div class="summary-card">
<div class="summary-main">
<div class="summary-price">{{ data.estimatedValue || '-' }}</div>
<div class="summary-sub">参考估值仅供参考实际价格以市场为准</div>
</div>
<div class="summary-meta">
<div class="badge">{{ data.seriesName || '未知车系' }}</div>
<div class="meta-line">
<span>{{ data.manufacturerName || '未知厂商' }}</span>
<span v-if="data.productionDate" class="dot"></span>
<span v-if="data.productionDate">{{ data.productionDate }} 年出厂</span>
</div>
<div class="meta-line meta-small">
<span v-if="data.displacement">排量{{ data.displacement }}</span>
<span v-if="data.transmissionType" class="dot"></span>
<span v-if="data.transmissionType">变速箱{{ data.transmissionType }}</span>
<span v-if="data.emissionStandard" class="dot"></span>
<span v-if="data.emissionStandard">排放{{ data.emissionStandard }}</span>
</div>
</div>
</div>
<div class="detail-card">
<h4 class="section-title">基础信息</h4>
<div class="field-grid">
<div class="field">
<div class="field-label">厂商品牌名称</div>
<div class="field-value">{{ data.manufacturerName || '-' }}</div>
</div>
<div class="field">
<div class="field-label">车系名称</div>
<div class="field-value">{{ data.seriesName || '-' }}</div>
</div>
<div class="field">
<div class="field-label">车型年款</div>
<div class="field-value">{{ data.modelYear || data.productionDate || '-' }}</div>
</div>
<div class="field">
<div class="field-label">座位数</div>
<div class="field-value">{{ data.seatingCapacity || '-' }}</div>
</div>
<div class="field field-span">
<div class="field-label">车型名称</div>
<div class="field-value">{{ data.modelName || '-' }}</div>
</div>
<div class="field field-span">
<div class="field-label">车型指导价</div>
<div class="field-value">{{ data.msrp || '-' }}</div>
</div>
</div>
</div>
<div class="detail-card">
<h4 class="section-title">技术参数</h4>
<div class="field-grid">
<div class="field">
<div class="field-label">排量</div>
<div class="field-value">{{ data.displacement || '-' }}</div>
</div>
<div class="field">
<div class="field-label">变速箱类型</div>
<div class="field-value">{{ data.transmissionType || '-' }}</div>
</div>
<div class="field">
<div class="field-label">排放标准</div>
<div class="field-value">{{ data.emissionStandard || '-' }}</div>
</div>
<div class="field">
<div class="field-label">车身颜色</div>
<div class="field-value">{{ data.color || '-' }}</div>
</div>
<div class="field field-span" v-if="data.seriesGroupName">
<div class="field-label">车系组名</div>
<div class="field-value">{{ data.seriesGroupName }}</div>
</div>
</div>
</div>
</template>
<div v-else class="empty">
<div class="icon"></div>
<div class="title">暂无估值结果</div>
<div class="sub">未查询到有效的估值数据请检查 VIN 与车辆信息是否正确</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useRiskNotifier } from '@/composables/useRiskNotifier';
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 hasData = computed(() => !!props.data && Object.keys(props.data).length > 0);
const riskScore = computed(() => 100);
useRiskNotifier(props, riskScore);
defineExpose({ riskScore });
</script>
<style scoped>
.card {
@apply bg-white rounded-2xl p-6 shadow-sm border border-gray-100;
}
.header-box {
@apply flex items-center justify-between mb-5 px-5 py-4 rounded-2xl bg-gradient-to-r from-amber-50 via-orange-50 to-amber-50;
}
.header-left {
@apply flex flex-col;
}
.header-title {
@apply text-2xl font-semibold m-0 text-amber-900;
}
.header-desc {
@apply text-base mt-3 m-0 text-amber-800 opacity-90;
}
.header-tag {
@apply inline-flex flex-col items-end gap-1 px-3 py-2 rounded-xl bg-white/80 shadow-sm;
}
.tag-label {
@apply text-sm text-gray-500;
}
.tag-value {
@apply text-2xl font-bold text-amber-700 leading-none whitespace-nowrap;
}
.summary-card {
@apply rounded-2xl border border-amber-100 bg-amber-50/60 px-5 py-5 mb-4 flex flex-col gap-4;
}
.summary-main {}
.summary-price {
@apply text-3xl font-extrabold text-amber-800 leading-tight;
}
.summary-sub {
@apply text-sm text-amber-700 opacity-90;
}
.summary-meta {
@apply space-y-2 text-base text-gray-800;
}
.badge {
@apply inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-amber-100 text-amber-800;
}
.meta-line {
@apply flex flex-wrap items-center gap-2 text-sm text-gray-700;
}
.meta-line .dot {
@apply w-1 h-1 rounded-full bg-gray-400;
}
.meta-small {
@apply text-sm text-gray-600;
}
.detail-card {
@apply rounded-2xl border border-gray-100 bg-gray-50/60 px-5 py-4 mb-4;
}
.section-title {
@apply text-base font-semibold text-gray-800 mb-4;
}
.field-grid {
@apply grid gap-y-3 gap-x-6;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}
.field-label {
@apply text-sm text-gray-500 mb-1.5;
}
.field-value {
@apply text-base text-gray-900;
}
.field-span {
grid-column: 1 / -1;
}
.empty {
@apply text-center py-12 text-gray-500;
}
.empty .icon {
@apply text-4xl mb-3;
}
.empty .title {
@apply text-lg font-medium mb-2;
}
.empty .sub {
@apply text-sm;
}
</style>

250
src/ui/CQCXGYTS2.vue Normal file
View File

@@ -0,0 +1,250 @@
<template>
<div class="card">
<div class="header-box">
<h3 class="header-title">人车核验详版</h3>
<p class="header-desc">展示人员与车辆的详细匹配结果及相关说明</p>
</div>
<div class="result-section" :class="resultSectionClass">
<div class="result-icon-wrap">
<span class="result-icon" :class="iconClass">
{{ iconChar }}
</span>
</div>
<div class="result-label">认证结果</div>
<div class="result-value" :class="resultTextClass">{{ resultText }}</div>
<p v-if="resultDesc" class="result-desc">{{ resultDesc }}</p>
</div>
<div v-if="hasParams" class="info-rows">
<div class="info-row">
<span class="info-label">姓名</span>
<span class="info-value">{{ maskedName }}</span>
</div>
<div class="info-row">
<span class="info-label">车牌号</span>
<span class="info-value font-mono">{{ params?.plate_no || params?.car_license || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">号牌类型</span>
<span class="info-value">{{ params?.carplate_type || params?.car_type || '-' }}</span>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useRiskNotifier } from '@/composables/useRiskNotifier';
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 maskedName = computed(() => {
const name = props.params?.name || '';
if (!name) return '-';
return name.length > 1 ? name[0] + '*'.repeat(name.length - 1) : '*';
});
// status: 0 一致, -1 不一致, -2 非法姓名, -4 无记录
const status = computed(() => {
const s = props.data?.status;
if (s === 0 || s === -1 || s === -2 || s === -4) return s;
return null;
});
const resultText = computed(() => {
const s = status.value;
if (s === 0) return '一致';
if (s === -1) return '不一致';
if (s === -2) return '非法姓名';
if (s === -4) return '无记录';
return '暂无结果';
});
const resultDesc = computed(() => {
const s = status.value;
if (s === -2) return '姓名长度或格式不正确,请核对后重试';
if (s === -4) return '未查询到相关核验记录';
return '';
});
const resultTextClass = computed(() => {
const s = status.value;
if (s === 0) return 'result-match';
if (s === -1) return 'result-mismatch';
if (s === -2) return 'result-invalid';
if (s === -4) return 'result-norecord';
return 'result-unknown';
});
const resultSectionClass = computed(() => {
const s = status.value;
if (s === 0) return 'result-section match';
if (s === -1) return 'result-section mismatch';
if (s === -2) return 'result-section invalid';
if (s === -4) return 'result-section norecord';
return 'result-section unknown';
});
const iconClass = computed(() => {
const s = status.value;
if (s === 0) return 'icon-match';
if (s === -1) return 'icon-mismatch';
if (s === -2) return 'icon-invalid';
if (s === -4) return 'icon-norecord';
return 'icon-unknown';
});
const iconChar = computed(() => {
const s = status.value;
if (s === 0) return '✓';
if (s === -1) return '✕';
if (s === -2) return '!';
if (s === -4) return '—';
return '?';
});
const hasParams = computed(() => {
const p = props.params || {};
return p.name || p.plate_no || p.car_license || p.carplate_type || p.car_type;
});
const riskScore = computed(() => 100);
useRiskNotifier(props, riskScore);
defineExpose({ riskScore });
</script>
<style scoped>
.card {
@apply bg-white rounded-lg p-4 shadow-sm border border-gray-100;
}
.header-box {
@apply rounded-lg mb-4 p-4;
background: linear-gradient(135deg, #5c6bc0 0%, #3949ab 100%);
color: #fff;
}
.header-title {
@apply text-lg font-semibold m-0;
}
.header-desc {
@apply text-sm mt-1 opacity-90 m-0;
}
.result-section {
@apply rounded-xl p-5 text-center mb-4;
}
.result-section.match {
background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);
border: 1px solid rgba(76, 175, 80, 0.3);
}
.result-section.mismatch {
background: linear-gradient(135deg, #ffebee 0%, #ffcdd2 100%);
border: 1px solid rgba(244, 67, 54, 0.3);
}
.result-section.invalid {
background: linear-gradient(135deg, #fff3e0 0%, #ffe0b2 100%);
border: 1px solid rgba(255, 152, 0, 0.4);
}
.result-section.norecord {
background: linear-gradient(135deg, #eceff1 0%, #cfd8dc 100%);
border: 1px solid rgba(96, 125, 139, 0.3);
}
.result-section.unknown {
@apply bg-gray-50 border border-gray-200;
}
.result-icon-wrap {
@apply mb-2;
}
.result-icon {
@apply inline-flex items-center justify-center w-12 h-12 rounded-full text-2xl font-bold text-white;
}
.result-icon.icon-match {
background: linear-gradient(135deg, #43a047 0%, #2e7d32 100%);
box-shadow: 0 2px 8px rgba(76, 175, 80, 0.4);
}
.result-icon.icon-mismatch {
background: linear-gradient(135deg, #e53935 0%, #c62828 100%);
box-shadow: 0 2px 8px rgba(244, 67, 54, 0.4);
}
.result-icon.icon-invalid {
background: linear-gradient(135deg, #fb8c00 0%, #ef6c00 100%);
box-shadow: 0 2px 8px rgba(255, 152, 0, 0.4);
}
.result-icon.icon-norecord {
background: linear-gradient(135deg, #78909c 0%, #546e7a 100%);
box-shadow: 0 2px 8px rgba(96, 125, 139, 0.3);
}
.result-icon.icon-unknown {
background: linear-gradient(135deg, #78909c 0%, #546e7a 100%);
box-shadow: 0 2px 8px rgba(96, 125, 139, 0.3);
}
.result-label {
@apply text-sm text-gray-500 mb-1;
}
.result-value {
@apply text-xl font-semibold;
}
.result-desc {
@apply text-sm mt-2 m-0 text-gray-600 max-w-xs mx-auto;
}
.result-match {
color: #2e7d32;
}
.result-mismatch {
color: #c62828;
}
.result-invalid {
color: #e65100;
}
.result-norecord {
color: #546e7a;
}
.result-unknown {
@apply text-gray-500;
}
.info-rows {
@apply space-y-3 pt-2 border-t border-gray-100;
}
.info-row {
@apply flex items-center text-sm;
}
.info-label {
@apply w-20 text-gray-500 shrink-0;
}
.info-value {
@apply font-medium text-gray-800;
}
</style>

View File

@@ -0,0 +1,60 @@
<template>
<div class="card">
<div class="bg-gray-100 text-gray-800 p-4 rounded-lg mb-4">
<h3 class="text-lg font-semibold">{{ title }}</h3>
<p class="text-sm mt-1">返回数据如下传参后续可按接口单独配置</p>
</div>
<div v-if="hasRawData" class="text-xs">
<pre
class="bg-gray-50 rounded p-3 overflow-x-auto whitespace-pre-wrap break-all border border-gray-200">{{ prettyData }}</pre>
</div>
<div v-else class="text-gray-500 text-sm">暂无数据</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useRiskNotifier } from '@/composables/useRiskNotifier';
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 titleMap = {
QCXG4D2E: '名下车辆(数量)',
QCXG5U0Z: '车辆静态信息查询',
QCXG1U4U: '车辆里程记录(混合查询)',
QCXGY7F2: '二手车VIN估值',
QCXG1H7Y: '车辆过户简版查询',
QCXG4I1Z: '车辆过户详版查询',
QCXG3Y6B: '车辆维保简版查询',
QCXG3Z3L: '车辆维保详细版查询',
QCXGP00W: '车辆出险详版查询',
QCXG6B4E: '车辆出险记录核验',
};
const title = computed(() => titleMap[props.apiId] || props.apiId || '车辆查询');
const hasRawData = computed(() => !!props.data && Object.keys(props.data).length > 0);
const prettyData = computed(() => {
try {
return JSON.stringify(props.data, null, 2);
} catch {
return String(props.data || '');
}
});
const riskScore = computed(() => 100);
useRiskNotifier(props, riskScore);
defineExpose({ riskScore });
</script>
<style scoped>
.card {
@apply bg-white rounded-lg p-4 shadow-sm border border-gray-100;
}
</style>

479
src/ui/CQYGL2S0W.vue Normal file
View File

@@ -0,0 +1,479 @@
<template>
<div class="card">
<div class="header-box">
<h3 class="header-title">失信被执行人</h3>
<p class="header-desc">
展示命中最高法院公布的失信被执行人信息用于识别严重违约风险
</p>
</div>
<div v-if="hasList" class="summary-section">
<div class="summary-row">
<div class="summary-card summary-risk">
<div class="summary-label">命中失信被执行人</div>
<div class="summary-value">{{ totalCount }}</div>
<div class="summary-sub">
{{ areaSummary }}
</div>
</div>
<div class="summary-card summary-status">
<div class="summary-label">履行情况</div>
<div class="summary-value">
{{ mainPerformance }}
</div>
<div class="summary-sub">
重点关注全部未履行部分履行等高风险状态
</div>
</div>
</div>
</div>
<div v-if="hasList" class="block">
<div class="block-title">失信记录详情</div>
<div class="record-list">
<div v-for="(item, index) in list" :key="item.id || index" class="record-wrapper">
<div class="record-card">
<!-- 摘要区域仿 FLXG7E8F 的案件卡片风格 -->
<div class="record-summary" @click="toggleRecordExpand(item.id || index)">
<div class="summary-top">
<div class="summary-title">
<span class="record-case-no">
{{ item.case_code || '暂无案号' }}
</span>
<span class="record-type-tag">
{{ item.disrupt_type_name || '失信被执行人' }}
</span>
</div>
<span class="relate-tag">
{{ item.relateType || '失信被执行人' }}
</span>
</div>
<div class="summary-middle">
<span class="summary-label">立案</span>
<span class="summary-value">{{ formatDate(item.reg_date) }}</span>
<span class="summary-label ml-2">法院</span>
<span class="summary-value">{{ item.court_name || item.gist_unit || '-' }}</span>
</div>
<div class="summary-bottom">
<span class="risk-tag">
{{ item.performance || '履行情况未知' }}
</span>
<span class="expand-indicator">
<span class="expand-text">
{{ isRecordExpanded(item.id || index) ? '收起详情' : '展开详情' }}
</span>
<img src="@/assets/images/report/zk.png" alt="展开" class="w-4 h-4"
:class="{ 'rotate-180': isRecordExpanded(item.id || index) }" />
</span>
</div>
</div>
<!-- 详情区域可展开/收起 -->
<div class="record-detail" :class="{
'detail-collapsed': !isRecordExpanded(item.id || index),
'detail-expanded': isRecordExpanded(item.id || index),
}">
<div class="record-body">
<div class="info-row">
<span class="info-label">被执行人</span>
<span class="info-value">
{{ item.entity_name || '-' }}
<span v-if="item.sexy || item.age" class="info-sub">
{{ [item.sexy, item.age && item.age + ''].filter(Boolean).join('') }}
</span>
</span>
</div>
<div class="info-row">
<span class="info-label">主体代码</span>
<span class="info-value">
{{ maskId(item.entity_id) }}
</span>
</div>
<div class="info-row">
<span class="info-label">身份类型</span>
<span class="info-value">
{{ item.party_type_name || '自然人/法人' }}
<span v-if="item.business_entity" class="info-sub">
法定代表人/负责人{{ item.business_entity }}
</span>
</span>
</div>
<div class="info-row">
<span class="info-label">涉案地域</span>
<span class="info-value">{{ item.area_name || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">执行法院</span>
<span class="info-value">{{ item.court_name || item.gist_unit || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">执行依据</span>
<span class="info-value">{{ item.gist_id || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">立案时间</span>
<span class="info-value">{{ formatDate(item.reg_date) }}</span>
</div>
<div class="info-row">
<span class="info-label">发布日期</span>
<span class="info-value">{{ formatDate(item.publish_date) }}</span>
</div>
<div class="info-row">
<span class="info-label">履行情况</span>
<span class="info-value">
{{ item.performance || '-' }}
<span v-if="item.performed_part || item.unPerform_part" class="info-sub">
{{ formatPerformDetail(item) }}
</span>
</span>
</div>
<div class="info-row">
<span class="info-label">履行义务</span>
<span class="info-value">
{{ item.duty || '-' }}
</span>
</div>
<div class="info-row">
<span class="info-label">下架状态</span>
<span class="info-value">
{{ item.case_status || '-' }}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div v-else class="empty-tip">
暂未命中失信被执行人记录
</div>
</div>
</template>
<script setup>
import { computed, ref, watchEffect } from "vue";
const props = defineProps({
data: { type: Object, default: () => ({}) },
params: { type: Object, default: () => ({}) },
apiId: { type: String, default: "" },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => { } },
});
// 统一 dataList 结构
const list = computed(() => {
const d = props.data || {};
if (Array.isArray(d.dataList)) return d.dataList;
if (d.data && Array.isArray(d.data.dataList)) return d.data.dataList;
return [];
});
const hasList = computed(() => list.value.length > 0);
const totalCount = computed(() => list.value.length);
const areaSummary = computed(() => {
if (!hasList.value) return "未命中失信被执行人";
const areas = Array.from(
new Set(
list.value
.map((i) => i.area_name)
.filter((v) => !!v)
)
);
if (!areas.length) return "地区信息未知";
if (areas.length === 1) return `集中在 ${areas[0]}`;
return `涉及 ${areas.length} 个地区,如 ${areas.slice(0, 2).join("、")}`;
});
const mainPerformance = computed(() => {
if (!hasList.value) return "暂无记录";
const perf = list.value
.map((i) => i.performance)
.filter((v) => !!v);
if (!perf.length) return "履行情况未知";
const first = perf[0];
if (perf.every((p) => p === first)) return first;
return `${first} 等多种情况`;
});
function formatPerformDetail(item) {
const parts = [];
if (item.performed_part) parts.push(`已履行:${item.performed_part}`);
if (item.unPerform_part) parts.push(`未履行:${item.unPerform_part}`);
return parts.join("");
}
function formatDate(str) {
if (!str) return "-";
// 支持 yyyy-MM-dd 或 yyyy-MM-dd HH:mm:ss
if (str.length >= 10) return str.slice(0, 10);
return str;
}
function maskId(id) {
if (!id) return "-";
const s = String(id);
if (s.length <= 8) return s[0] + "***" + s.slice(-1);
return s.slice(0, 4) + "****" + s.slice(-4);
}
// 展开/收起记录详情,参考 FLXG7E8F 的交互
const expandedRecords = ref({});
function toggleRecordExpand(key) {
const id = String(key);
expandedRecords.value[id] = !expandedRecords.value[id];
}
function isRecordExpanded(key) {
const id = String(key);
return !!expandedRecords.value[id];
}
// 上报风险命中情况(命中视为高风险)
watchEffect(() => {
if (!props.notifyRiskStatus) return;
const hit = hasList.value;
props.notifyRiskStatus(props.apiId || "QYGL2S0W", props.index || 0, {
hasRisk: hit,
});
});
</script>
<style scoped>
.card {
padding: 1rem;
}
.header-box {
margin-bottom: 1rem;
}
.header-title {
font-size: 1.1rem;
font-weight: 600;
color: #333333;
}
.header-desc {
margin-top: 0.25rem;
font-size: 0.85rem;
color: #666666;
}
.summary-section {
padding: 0.75rem 0;
border-top: 1px solid #f1f1f1;
border-bottom: 1px solid #f1f1f1;
margin-bottom: 1rem;
}
.summary-row {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
@media (min-width: 768px) {
.summary-row {
flex-direction: row;
}
}
.summary-card {
flex: 1;
padding: 0.75rem;
border-radius: 0.75rem;
}
.summary-risk {
background: rgba(235, 60, 60, 0.04);
border: 1px solid rgba(235, 60, 60, 0.3);
}
.summary-status {
background: rgba(214, 148, 62, 0.04);
border: 1px solid rgba(214, 148, 62, 0.3);
}
.summary-label {
font-size: 0.85rem;
color: #666666;
margin-bottom: 0.25rem;
}
.summary-value {
font-size: 1.4rem;
font-weight: 600;
color: #333333;
}
.summary-sub {
margin-top: 0.25rem;
font-size: 0.75rem;
color: #999999;
}
.block {
margin-top: 1rem;
}
.block-title {
font-size: 0.95rem;
font-weight: 600;
color: #333333;
margin-bottom: 0.5rem;
}
.record-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.record-wrapper {
width: 100%;
}
.record-card {
border-radius: 0.75rem;
border: 1px solid #dddddd;
background-color: #ffffff;
overflow: hidden;
}
.record-summary {
padding: 0.75rem 0.75rem 0.5rem;
cursor: pointer;
}
.summary-top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.25rem;
}
.summary-title {
display: flex;
align-items: center;
gap: 0.5rem;
min-width: 0;
}
.record-case-no {
font-size: 0.9rem;
font-weight: 600;
color: #333333;
}
.record-type-tag {
padding: 0.1rem 0.4rem;
border-radius: 999px;
font-size: 0.75rem;
background-color: #f9ecec;
color: #eb3c3c;
}
.relate-tag {
flex-shrink: 0;
padding: 0.2rem 0.5rem;
border-radius: 999px;
font-size: 0.75rem;
background-color: rgba(214, 148, 62, 0.08);
color: #d6943e;
}
.summary-middle {
font-size: 0.8rem;
padding-bottom: 0.25rem;
}
.summary-label {
color: #666666;
}
.summary-value {
color: #333333;
}
.summary-bottom {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
}
.risk-tag {
padding: 0.2rem 0.6rem;
border-radius: 999px;
font-size: 0.75rem;
font-weight: 500;
background-color: rgba(235, 60, 60, 0.08);
color: #eb3c3c;
}
.expand-indicator {
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 0.75rem;
color: #999999;
}
.expand-indicator img {
transition: transform 0.2s ease;
}
.expand-indicator img.rotate-180 {
transform: rotate(180deg);
}
.record-detail {
border-top: 1px dashed #e5e5e5;
padding: 0 0.75rem 0.5rem;
max-height: 0;
opacity: 0;
overflow: hidden;
transition: max-height 0.25s ease, opacity 0.25s ease;
}
.record-detail.detail-expanded {
max-height: 500px;
opacity: 1;
}
.info-row {
display: flex;
font-size: 0.8rem;
margin-bottom: 0.25rem;
}
.info-label {
width: 4.5rem;
color: #999999;
flex-shrink: 0;
}
.info-value {
color: #333333;
word-break: break-all;
}
.info-sub {
margin-left: 0.25rem;
font-size: 0.75rem;
color: #777777;
}
.empty-tip {
margin-top: 0.75rem;
padding: 0.75rem;
text-align: center;
font-size: 0.85rem;
color: #999999;
background-color: #fafafa;
border-radius: 0.75rem;
}
</style>

171
src/ui/CQYGL5F6A.vue Normal file
View File

@@ -0,0 +1,171 @@
<template>
<div class="card">
<div class="header-box">
<h3 class="header-title">名下企业关联</h3>
<p class="header-desc">展示查询对象作为法人/股东/高管关联的企业</p>
</div>
<div v-if="companyItems && companyItems.length" class="company-list">
<div v-for="(item, idx) in companyItems" :key="idx" class="company-card">
<div class="company-header">
<div class="company-name">{{ item.orgName || item.basicInfo?.name || '未知企业' }}</div>
<div class="company-tags">
<span v-for="tag in item.relationshipTags" :key="tag" class="tag">
{{ tag }}
</span>
</div>
</div>
<div class="company-body">
<div class="row">
<span class="label">企业状态</span>
<span class="value">{{ item.basicInfo?.regStatus || '-' }}</span>
</div>
<div class="row">
<span class="label">成立日期</span>
<span class="value">{{ item.basicInfo?.estiblishTime || '-' }}</span>
</div>
<div class="row">
<span class="label">注册资本</span>
<span class="value">{{ item.basicInfo?.regCapital || '-' }}</span>
</div>
<div class="row">
<span class="label">行业</span>
<span class="value">{{ item.basicInfo?.industry || '-' }}</span>
</div>
<div class="row">
<span class="label">企业类型</span>
<span class="value">{{ item.basicInfo?.companyOrgType || '-' }}</span>
</div>
<div class="row">
<span class="label">法定代表人</span>
<span class="value">{{ item.basicInfo?.legalPersonName || '-' }}</span>
</div>
</div>
</div>
</div>
<div v-if="!companyItems || !companyItems.length" class="empty-tip">
暂未查询到名下企业关联信息
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
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 rawItems = computed(() => {
const report = props.data?.ent_report_001;
const items = report?.queryResult?.items;
return Array.isArray(items) ? items : [];
});
const relationshipMap = {
lp: '法人',
sh: '股东',
tm: '高管',
};
const companyItems = computed(() =>
rawItems.value.map((item) => {
const relArr = Array.isArray(item.relationship) ? item.relationship : [];
const relationshipTags = relArr.map((r) => relationshipMap[r] || r);
return {
...item,
relationshipTags,
};
})
);
</script>
<style scoped>
.card {
padding: 1rem;
}
.header-box {
margin-bottom: 1rem;
}
.header-title {
font-size: 1.125rem;
font-weight: 600;
}
.header-desc {
font-size: 0.875rem;
color: #6b7280;
margin-top: 0.25rem;
}
.company-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.company-card {
border-radius: 0.75rem;
border: 1px solid #e5e7eb;
background: #f9fafb;
padding: 0.75rem 0.875rem;
}
.company-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.company-name {
font-size: 0.95rem;
font-weight: 600;
color: #111827;
}
.company-tags {
display: flex;
gap: 0.25rem;
flex-wrap: wrap;
}
.tag {
font-size: 0.75rem;
padding: 0.1rem 0.4rem;
border-radius: 999px;
background: #eef2ff;
color: #4f46e5;
}
.company-body .row {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
padding: 0.15rem 0;
}
.label {
color: #6b7280;
}
.value {
color: #111827;
margin-left: 1rem;
text-align: right;
}
.empty-tip {
color: #9ca3af;
font-size: 0.875rem;
padding: 1rem 0;
text-align: center;
}
</style>

542
src/ui/CQYGL66SL.vue Normal file
View File

@@ -0,0 +1,542 @@
<template>
<div class="card">
<div class="header-box">
<h3 class="header-title">企业司法涉诉</h3>
<p class="header-desc">展示企业在全国法院公开信息中的涉诉情况</p>
<p v-if="entName" class="header-ent">被查询企业{{ entName }}</p>
</div>
<div v-if="hasCivilData" class="summary-section">
<div class="summary-row">
<div class="summary-card summary-risk">
<div class="summary-label">民事案件总数</div>
<div class="summary-value">{{ totalCivilCases }}</div>
<div class="summary-sub">
已结案 {{ civilClosedCases }} · 未结案 {{ civilPendingCases }}
</div>
</div>
<div class="summary-card summary-money">
<div class="summary-label">案件地域/案由概览</div>
<div class="summary-value small">
{{ civilAreaStat || '地域分布未知' }}
</div>
<div class="summary-sub">
{{ civilAyStat || '案由分布暂无统计' }}
</div>
</div>
</div>
<div class="tag-row" v-if="civilAreaStat || civilAyStat || civilJafsStat">
<span v-if="civilAreaStat" class="stat-tag">
涉案地域{{ civilAreaStat }}
</span>
<span v-if="civilAyStat" class="stat-tag">
案由分布{{ civilAyStat }}
</span>
<span v-if="civilJafsStat" class="stat-tag">
结案方式{{ civilJafsStat }}
</span>
</div>
</div>
<div v-if="civilCases.length" class="block">
<div class="block-title">民事案件列表</div>
<div class="case-list">
<div v-for="(item, index) in civilCases" :key="item.c_id || index" class="case-wrapper">
<div class="case-card">
<!-- 可点击的摘要区域参考 FLXG7E8F 样式 -->
<div class="case-summary" @click="toggleCaseExpand(item.c_id || index)">
<div class="summary-top">
<div class="summary-title">
<span class="case-no-text">
{{ item.c_ah || '暂无案号' }}
</span>
<span class="case-type-tag">
{{ item.n_ajlx || item.n_laay_tag || item.n_laay || '民事案件' }}
</span>
</div>
</div>
<div class="summary-middle">
<span class="summary-label">立案</span>
<span class="summary-value">{{ formatDate(item.d_larq) }}</span>
</div>
<div class="summary-bottom">
<span class="risk-tag" :class="statusClass(item.n_ajjzjd)">
{{ item.n_ajjzjd || '未知进展' }}
</span>
<span v-if="item.n_pj_victory" class="victory-tag">
胜诉估计{{ item.n_pj_victory }}
</span>
<span class="expand-indicator">
<span class="expand-text">
{{ isCaseExpanded(item.c_id || index) ? '收起详情' : '展开详情' }}
</span>
<img src="@/assets/images/report/zk.png" alt="展开" class="w-4 h-4"
:class="{ 'rotate-180': isCaseExpanded(item.c_id || index) }" />
</span>
</div>
</div>
<!-- 详情区域可展开/收起 -->
<div class="case-detail" :class="{
'detail-collapsed': !isCaseExpanded(item.c_id || index),
'detail-expanded': isCaseExpanded(item.c_id || index),
}">
<div class="case-body">
<div class="info-row">
<span class="info-label">立案时间</span>
<span class="info-value">{{ formatDate(item.d_larq) }}</span>
</div>
<div class="info-row">
<span class="info-label">结案时间</span>
<span class="info-value">{{ formatDate(item.d_jarq) }}</span>
</div>
<div class="info-row">
<span class="info-label">经办法院</span>
<span class="info-value">
{{ item.n_jbfy || '-' }}
</span>
</div>
<div class="info-row">
<span class="info-label">审理程序</span>
<span class="info-value">
{{ item.n_slcx || '-' }}
</span>
</div>
<div class="info-row">
<span class="info-label">结案案由</span>
<span class="info-value">
{{ item.n_jaay || '-' }}
</span>
</div>
<div class="info-row">
<span class="info-label">立案案由</span>
<span class="info-value">
{{ item.n_laay || '-' }}
</span>
</div>
<div class="info-row" v-if="item.c_gkws_pjjg">
<span class="info-label">判决结果</span>
<span class="info-value">
{{ item.c_gkws_pjjg }}
</span>
</div>
<div class="info-row" v-if="casePartiesText(item)">
<span class="info-label">当事人</span>
<span class="info-value">
{{ casePartiesText(item) }}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div v-else-if="hasAnyData" class="empty-tip">
暂未检索到民事案件记录可留意其他风险模块
</div>
<div v-else class="empty-tip">
暂无企业涉诉信息
</div>
</div>
</template>
<script setup>
import { computed, ref } from "vue";
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 entName = computed(() => {
return props.params?.ent_name || props.params?.entName || "";
});
// 提取 entout.data 结构
const entoutData = computed(() => {
const d = props.data || {};
// 优先使用 entout.data
if (d.entout && d.entout.data) return d.entout.data;
if (d.entout) return d.entout;
// 兼容 data.entout.data 结构
if (d.data && d.data.entout && d.data.entout.data) return d.data.entout.data;
return null;
});
const civil = computed(() => entoutData.value?.civil || {});
const civilCases = computed(() => civil.value.cases || []);
const civilCount = computed(() => civil.value.count || {});
const totalCivilCases = computed(() => civilCount.value.count_total ?? civilCases.value.length ?? 0);
const civilClosedCases = computed(() => civilCount.value.count_jie_total ?? 0);
const civilPendingCases = computed(() => civilCount.value.count_wei_total ?? 0);
const civilAreaStat = computed(() => civilCount.value.area_stat || "");
const civilAyStat = computed(() => civilCount.value.ay_stat || "");
const civilJafsStat = computed(() => civilCount.value.jafs_stat || "");
const hasCivilData = computed(() => totalCivilCases.value > 0);
const hasAnyData = computed(() => !!entoutData.value);
// 展开/收起案件详情,参考 FLXG7E8F 的交互
const expandedCases = ref({});
function toggleCaseExpand(key) {
const id = String(key);
expandedCases.value[id] = !expandedCases.value[id];
}
function isCaseExpanded(key) {
const id = String(key);
return !!expandedCases.value[id];
}
const moneySummary = computed(() => {
const total = civilCount.value.money_jie_total ?? null;
if (total == null) return "暂无金额统计";
if (typeof total === "number" && total === 0) return "金额较小或未公开";
return "已结案金额估计 " + formatMoney(total) + " 元";
});
function formatMoney(val) {
if (val == null || val === "") return "-";
const num = Number(val);
if (Number.isNaN(num)) return String(val);
if (num >= 1e8) {
return (num / 1e8).toFixed(2) + " 亿";
}
if (num >= 1e4) {
return (num / 1e4).toFixed(2) + " 万";
}
return num.toFixed(2);
}
function formatDate(str) {
if (!str) return "-";
// 支持 yyyy-MM-dd 或 yyyy-MM-dd HH:mm:ss
if (str.length >= 10) {
return str.slice(0, 10);
}
return str;
}
function statusClass(status) {
if (!status) return "status-tag status-unknown";
if (String(status).includes("已结案")) return "status-tag status-closed";
if (String(status).includes("执行中") || String(status).includes("审理中")) {
return "status-tag status-processing";
}
return "status-tag status-unknown";
}
function casePartiesText(item) {
const list = item.c_dsrxx || [];
if (!Array.isArray(list) || list.length === 0) return "";
// 尝试优先提取与当前企业相关的角色
const name = entName.value;
const related = name
? list.filter((p) => typeof p.c_mc === "string" && p.c_mc.includes(name))
: [];
const targetList = related.length ? related : list;
const parts = targetList.map((p) => {
const n = p.c_mc || "";
const role = p.n_ssdw || "";
if (n && role) return `${n}${role}`;
return n || role || "";
}).filter(Boolean);
return parts.join("");
}
</script>
<style scoped>
.card {
padding: 1rem;
}
.header-box {
margin-bottom: 1rem;
}
.header-title {
font-size: 1.1rem;
font-weight: 600;
color: #333333;
}
.header-desc {
margin-top: 0.25rem;
font-size: 0.85rem;
color: #666666;
}
.header-ent {
margin-top: 0.25rem;
font-size: 0.85rem;
color: #333333;
}
.summary-section {
padding: 0.75rem 0;
border-top: 1px solid #f1f1f1;
border-bottom: 1px solid #f1f1f1;
margin-bottom: 1rem;
}
.summary-row {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
@media (min-width: 768px) {
.summary-row {
flex-direction: row;
}
}
.summary-card {
flex: 1;
padding: 0.75rem;
border-radius: 0.75rem;
}
.summary-risk {
background: rgba(235, 60, 60, 0.04);
border: 1px solid rgba(235, 60, 60, 0.3);
}
.summary-money {
background: rgba(214, 148, 62, 0.04);
border: 1px solid rgba(214, 148, 62, 0.3);
}
.summary-label {
font-size: 0.85rem;
color: #666666;
margin-bottom: 0.25rem;
}
.summary-value {
font-size: 1.4rem;
font-weight: 600;
color: #333333;
}
.summary-value.small {
font-size: 1rem;
}
.summary-sub {
margin-top: 0.25rem;
font-size: 0.75rem;
color: #999999;
}
.tag-row {
margin-top: 0.75rem;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.stat-tag {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
border-radius: 999px;
background-color: #f5f5f5;
color: #666666;
}
.block {
margin-top: 1rem;
}
.block-title {
font-size: 0.95rem;
font-weight: 600;
color: #333333;
margin-bottom: 0.5rem;
}
.case-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.case-wrapper {
width: 100%;
}
.case-card {
border-radius: 0.75rem;
border: 1px solid #dddddd;
background-color: #ffffff;
overflow: hidden;
}
.case-summary {
padding: 0.75rem 0.75rem 0.5rem;
cursor: pointer;
position: relative;
}
.summary-top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.25rem;
}
.summary-title {
display: flex;
align-items: center;
gap: 0.5rem;
min-width: 0;
}
.case-no-text {
font-size: 0.9rem;
font-weight: 600;
color: #333333;
}
.case-type-tag {
padding: 0.1rem 0.4rem;
border-radius: 999px;
font-size: 0.75rem;
background-color: #f9ecec;
color: #eb3c3c;
}
.summary-middle {
font-size: 0.8rem;
padding-bottom: 0.25rem;
}
.summary-label {
color: #666666;
}
.summary-value {
color: #333333;
}
.summary-bottom {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
}
.risk-tag {
padding: 0.2rem 0.6rem;
border-radius: 999px;
font-size: 0.75rem;
font-weight: 500;
}
.status-closed {
background-color: rgba(31, 190, 93, 0.1);
color: #1fbe5d;
}
.status-processing {
background-color: rgba(235, 60, 60, 0.08);
color: #eb3c3c;
}
.status-unknown {
background-color: rgba(153, 153, 153, 0.1);
color: #666666;
}
.victory-tag {
font-size: 0.75rem;
color: #d6943e;
}
.expand-indicator {
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 0.75rem;
color: #999999;
}
.expand-indicator img {
transition: transform 0.2s ease;
}
.expand-indicator img.rotate-180 {
transform: rotate(180deg);
}
.case-detail {
border-top: 1px dashed #eeeeee;
padding: 0 0.75rem 0.5rem;
max-height: 0;
opacity: 0;
overflow: hidden;
transition: max-height 0.25s ease, opacity 0.25s ease;
}
.case-detail.detail-expanded {
max-height: 500px;
opacity: 1;
}
.status-closed {
background-color: rgba(31, 190, 93, 0.1);
color: #1fbe5d;
}
.status-processing {
background-color: rgba(235, 60, 60, 0.08);
color: #eb3c3c;
}
.status-unknown {
background-color: rgba(153, 153, 153, 0.1);
color: #666666;
}
.case-body {
border-top: 1px dashed #e5e5e5;
padding-top: 0.5rem;
}
.info-row {
display: flex;
font-size: 0.8rem;
margin-bottom: 0.25rem;
}
.info-label {
width: 4.5rem;
color: #999999;
flex-shrink: 0;
}
.info-value {
color: #333333;
word-break: break-all;
}
.empty-tip {
margin-top: 0.75rem;
padding: 0.75rem;
text-align: center;
font-size: 0.85rem;
color: #999999;
background-color: #fafafa;
border-radius: 0.75rem;
}
</style>

212
src/ui/CYYSY3M8S.vue Normal file
View File

@@ -0,0 +1,212 @@
<template>
<div class="card">
<div class="header-box">
<h3 class="header-title">运营商二要素</h3>
<p class="header-desc">核验手机号与姓名是否一致</p>
</div>
<div v-if="hasData" class="result-section" :class="resultSectionClass">
<div class="result-main">
<div class="result-label">核验结果</div>
<div class="result-value" :class="resultClass">
{{ resultText }}
</div>
</div>
<div class="result-sub">
<span>计费状态</span>
<span class="font-medium">{{ feeText }}</span>
</div>
</div>
<div v-if="hasParams" class="info-block">
<div class="block-title">被核验号码</div>
<div class="info-row">
<span class="info-label">手机号</span>
<span class="info-value font-mono">{{ params.mobile || params.mobile_no || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">姓名</span>
<span class="info-value">{{ maskedName }}</span>
</div>
</div>
<div v-if="!hasData" class="empty-tip">暂无核验结果</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
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 hasData = computed(() => props.data && Object.keys(props.data).length > 0);
const resultCode = computed(() => {
const v = props.data?.result;
if (v === 0 || v === '0') return 0;
if (v === 1 || v === '1') return 1;
return null;
});
const resultText = computed(() => {
if (resultCode.value === 0) return '一致';
if (resultCode.value === 1) return '不一致';
return '暂无结果';
});
const fee = computed(() => {
const v = props.data?.fee;
if (v === 0 || v === '0') return 0;
if (v === 1 || v === '1') return 1;
return null;
});
const feeText = computed(() => {
if (fee.value === 0) return '不计费';
if (fee.value === 1) return '计费';
return '未知';
});
const resultClass = computed(() => {
if (resultCode.value === 0) return 'result-ok';
if (resultCode.value === 1) return 'result-bad';
return 'result-unknown';
});
const resultSectionClass = computed(() => {
if (resultCode.value === 0) return 'result-section ok';
if (resultCode.value === 1) return 'result-section bad';
return 'result-section unknown';
});
const hasParams = computed(() => {
const p = props.params || {};
return p.mobile || p.mobile_no || p.name;
});
const maskedName = computed(() => {
const name = props.params?.name || '';
if (!name) return '-';
return name.length > 1 ? name[0] + '*'.repeat(name.length - 1) : '*';
});
</script>
<style scoped>
.card {
padding: 1rem;
}
.header-box {
margin-bottom: 1rem;
}
.header-title {
font-size: 1.125rem;
font-weight: 600;
}
.header-desc {
font-size: 0.875rem;
color: #6b7280;
margin-top: 0.25rem;
}
.result-section {
padding: 0.75rem 0.875rem;
border-radius: 0.75rem;
margin-bottom: 1rem;
border: 1px solid #e5e7eb;
}
.result-section.ok {
background: #ecfdf3;
border-color: #22c55e33;
}
.result-section.bad {
background: #fef2f2;
border-color: #ef444433;
}
.result-section.unknown {
background: #f9fafb;
}
.result-main {
display: flex;
align-items: center;
justify-content: space-between;
}
.result-label {
font-size: 0.875rem;
color: #6b7280;
}
.result-value {
font-size: 1rem;
font-weight: 600;
}
.result-ok {
color: #16a34a;
}
.result-bad {
color: #dc2626;
}
.result-unknown {
color: #6b7280;
}
.result-sub {
margin-top: 0.5rem;
font-size: 0.875rem;
color: #4b5563;
}
.info-block {
margin-top: 1rem;
padding: 0.75rem 0.875rem;
border-radius: 0.75rem;
background: #f9fafb;
}
.block-title {
font-size: 0.875rem;
font-weight: 600;
color: #374151;
margin-bottom: 0.5rem;
}
.info-row {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
padding: 0.15rem 0;
}
.info-label {
color: #6b7280;
}
.info-value {
color: #111827;
margin-left: 1rem;
text-align: right;
}
.empty-tip {
color: #9ca3af;
font-size: 0.875rem;
padding: 1rem 0;
text-align: center;
}
</style>

162
src/ui/CYYSY6F2B.vue Normal file
View File

@@ -0,0 +1,162 @@
<template>
<div class="card">
<div class="header-box">
<h3 class="header-title">手机消费区间验证</h3>
<p class="header-desc">根据运营商数据评估消费能力档位</p>
</div>
<div v-if="hasData" class="result-section">
<div class="result-main">
<div class="result-label">消费档位</div>
<div class="result-value">
{{ stateText }}
</div>
</div>
<div class="result-sub">
<span>号码所属运营商{{ operatorText }}</span>
<span v-if="isPortedText" class="ml-2">是否携号转网{{ isPortedText }}</span>
<span v-if="operatorRealText" class="ml-2">实际运营商{{ operatorRealText }}</span>
</div>
</div>
<div v-if="hasParams" class="info-block">
<div class="block-title">被核验号码</div>
<div class="info-row">
<span class="info-label">手机号</span>
<span class="info-value font-mono">{{ params.mobile || params.mobile_no || '-' }}</span>
</div>
</div>
<div v-if="!hasData" class="empty-tip">暂无核验结果</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
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 hasData = computed(() => props.data && Object.keys(props.data).length > 0);
const state = computed(() => props.data?.state || '');
const operator = computed(() => props.data?.operator || '');
const operatorReal = computed(() => props.data?.operator_real || '');
const isXhzw = computed(() => props.data?.is_xhzw || '');
const operatorMap = {
'1': '移动',
'2': '联通',
'3': '电信',
};
const operatorText = computed(() => operatorMap[operator.value] || operator.value || '-');
const operatorRealText = computed(() => operatorMap[operatorReal.value] || '');
const isPortedText = computed(() => {
if (isXhzw.value === '0') return '否';
if (isXhzw.value === '1') return '是';
return '';
});
const stateText = computed(() => {
// 这里只提示“消费档位编号”,具体金额区间见注释说明
if (!state.value) return '未知';
return `消费档位:${state.value}(不同运营商对应区间不同)`;
});
</script>
<style scoped>
.card {
padding: 1rem;
}
.header-box {
margin-bottom: 1rem;
}
.header-title {
font-size: 1.125rem;
font-weight: 600;
}
.header-desc {
font-size: 0.875rem;
color: #6b7280;
margin-top: 0.25rem;
}
.result-section {
padding: 0.75rem 0.875rem;
border-radius: 0.75rem;
margin-bottom: 1rem;
border: 1px solid #e5e7eb;
background: #f9fafb;
}
.result-main {
display: flex;
align-items: center;
justify-content: space-between;
}
.result-label {
font-size: 0.875rem;
color: #6b7280;
}
.result-value {
font-size: 1rem;
font-weight: 600;
color: #111827;
}
.result-sub {
margin-top: 0.5rem;
font-size: 0.875rem;
color: #4b5563;
}
.info-block {
margin-top: 1rem;
padding: 0.75rem 0.875rem;
border-radius: 0.75rem;
background: #f9fafb;
}
.block-title {
font-size: 0.875rem;
font-weight: 600;
color: #374151;
margin-bottom: 0.5rem;
}
.info-row {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
padding: 0.15rem 0;
}
.info-label {
color: #6b7280;
}
.info-value {
color: #111827;
margin-left: 1rem;
text-align: right;
}
.empty-tip {
color: #9ca3af;
font-size: 0.875rem;
padding: 1rem 0;
text-align: center;
}
</style>

124
src/ui/CYYSY9E4A.vue Normal file
View File

@@ -0,0 +1,124 @@
<template>
<div class="card">
<div class="header-box">
<h3 class="header-title">手机号码归属地核验</h3>
<p class="header-desc">展示手机号码的省市运营商和区号等信息</p>
</div>
<div v-if="hasData" class="result-section">
<div class="info-row">
<span class="info-label">号段</span>
<span class="info-value font-mono">{{ data.mobilePrefix || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">省份</span>
<span class="info-value">{{ data.provinceName || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">城市</span>
<span class="info-value">{{ data.cityName || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">运营商</span>
<span class="info-value">{{ data.channel || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">区号</span>
<span class="info-value">{{ data.areaCode || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">邮编</span>
<span class="info-value">{{ data.postCode || '-' }}</span>
</div>
</div>
<div v-if="hasParams" class="info-block">
<div class="block-title">被核验号码</div>
<div class="info-row">
<span class="info-label">手机号</span>
<span class="info-value font-mono">{{ params.mobile || params.mobile_no || '-' }}</span>
</div>
</div>
<div v-if="!hasData" class="empty-tip">暂无核验结果</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
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 hasData = computed(() => props.data && Object.keys(props.data).length > 0);
</script>
<style scoped>
.card {
padding: 1rem;
}
.header-box {
margin-bottom: 1rem;
}
.header-title {
font-size: 1.125rem;
font-weight: 600;
}
.header-desc {
font-size: 0.875rem;
color: #6b7280;
margin-top: 0.25rem;
}
.result-section {
background: #f9fafb;
padding: 0.75rem 0.875rem;
border-radius: 0.75rem;
}
.info-block {
margin-top: 1rem;
padding: 0.75rem 0.875rem;
border-radius: 0.75rem;
background: #f9fafb;
}
.block-title {
font-size: 0.875rem;
font-weight: 600;
color: #374151;
margin-bottom: 0.5rem;
}
.info-row {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
padding: 0.15rem 0;
}
.info-label {
color: #6b7280;
}
.info-value {
color: #111827;
margin-left: 1rem;
text-align: right;
}
.empty-tip {
color: #9ca3af;
font-size: 0.875rem;
padding: 1rem 0;
text-align: center;
}
</style>

192
src/ui/CYYSYE7V5.vue Normal file
View File

@@ -0,0 +1,192 @@
<template>
<div class="card">
<div class="header-box">
<h3 class="header-title">手机在网状态</h3>
<p class="header-desc">判断号码当前是否在网可用</p>
</div>
<div v-if="hasData" class="result-section" :class="statusSectionClass">
<div class="result-main">
<div class="result-label">在网状态</div>
<div class="result-value" :class="statusClass">
{{ statusText }}
</div>
</div>
<div class="result-sub">
<span>运营商{{ channel || '-' }}</span>
<span v-if="status === 1 && desc" class="ml-2">原因{{ desc }}</span>
</div>
</div>
<div v-if="hasParams" class="info-block">
<div class="block-title">被核验号码</div>
<div class="info-row">
<span class="info-label">手机号</span>
<span class="info-value font-mono">{{ params.mobile || params.mobile_no || '-' }}</span>
</div>
</div>
<div v-if="!hasData" class="empty-tip">暂无核验结果</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
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 hasData = computed(() => props.data && Object.keys(props.data).length > 0);
const status = computed(() => {
const v = props.data?.status;
if (v === 0 || v === '0') return 0;
if (v === 1 || v === '1') return 1;
return null;
});
const statusText = computed(() => {
if (status.value === 0) return '在网';
if (status.value === 1) return '不在网';
return '未知';
});
const statusClass = computed(() => {
if (status.value === 0) return 'result-ok';
if (status.value === 1) return 'result-bad';
return 'result-unknown';
});
const statusSectionClass = computed(() => {
if (status.value === 0) return 'result-section ok';
if (status.value === 1) return 'result-section bad';
return 'result-section unknown';
});
const channel = computed(() => props.data?.channel || '');
const desc = computed(() => props.data?.desc || '');
const hasParams = computed(() => {
const p = props.params || {};
return p.mobile || p.mobile_no;
});
</script>
<style scoped>
.card {
padding: 1rem;
}
.header-box {
margin-bottom: 1rem;
}
.header-title {
font-size: 1.125rem;
font-weight: 600;
}
.header-desc {
font-size: 0.875rem;
color: #6b7280;
margin-top: 0.25rem;
}
.result-section {
padding: 0.75rem 0.875rem;
border-radius: 0.75rem;
margin-bottom: 1rem;
border: 1px solid #e5e7eb;
}
.result-section.ok {
background: #ecfdf3;
border-color: #22c55e33;
}
.result-section.bad {
background: #fef2f2;
border-color: #ef444433;
}
.result-section.unknown {
background: #f9fafb;
}
.result-main {
display: flex;
align-items: center;
justify-content: space-between;
}
.result-label {
font-size: 0.875rem;
color: #6b7280;
}
.result-value {
font-size: 1rem;
font-weight: 600;
}
.result-ok {
color: #16a34a;
}
.result-bad {
color: #dc2626;
}
.result-unknown {
color: #6b7280;
}
.result-sub {
margin-top: 0.5rem;
font-size: 0.875rem;
color: #4b5563;
}
.info-block {
margin-top: 1rem;
padding: 0.75rem 0.875rem;
border-radius: 0.75rem;
background: #f9fafb;
}
.block-title {
font-size: 0.875rem;
font-weight: 600;
color: #374151;
margin-bottom: 0.5rem;
}
.info-row {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
padding: 0.15rem 0;
}
.info-label {
color: #6b7280;
}
.info-value {
color: #111827;
margin-left: 1rem;
text-align: right;
}
.empty-tip {
color: #9ca3af;
font-size: 0.875rem;
padding: 1rem 0;
text-align: center;
}
</style>

210
src/ui/CYYSYF2T7.vue Normal file
View File

@@ -0,0 +1,210 @@
<template>
<div class="card">
<div class="header-box">
<h3 class="header-title">号码二次放号</h3>
<p class="header-desc">判断该手机号是否为二次放号</p>
</div>
<div v-if="hasData" class="result-section" :class="resultSectionClass">
<div class="result-main">
<div class="result-label">核验结果</div>
<div class="result-value" :class="resultClass">
{{ resultText }}
</div>
</div>
<div class="result-sub">
<span>运营商</span>
<span class="font-medium">{{ channelText }}</span>
</div>
</div>
<div v-if="hasParams" class="info-block">
<div class="block-title">被核验号码</div>
<div class="info-row">
<span class="info-label">手机号</span>
<span class="info-value font-mono">{{ params.mobile || params.mobile_no || '-' }}</span>
</div>
</div>
<div v-if="!hasData" class="empty-tip">暂无核验结果</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
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 hasData = computed(() => props.data && Object.keys(props.data).length > 0);
const result = computed(() => {
const v = props.data?.result;
if (v === 0 || v === '0') return 0;
if (v === 1 || v === '1') return 1;
if (v === 2 || v === '2') return 2;
return null;
});
const resultText = computed(() => {
switch (result.value) {
case 0:
return '是二次卡';
case 1:
return '不是二次卡';
case 2:
return '数据库中无信息';
default:
return '暂无结果';
}
});
const resultClass = computed(() => {
if (result.value === 0) return 'result-bad';
if (result.value === 1) return 'result-ok';
if (result.value === 2) return 'result-unknown';
return 'result-unknown';
});
const resultSectionClass = computed(() => {
if (result.value === 0) return 'result-section bad';
if (result.value === 1) return 'result-section ok';
if (result.value === 2) return 'result-section unknown';
return 'result-section unknown';
});
const channelRaw = computed(() => props.data?.channel || '');
const channelText = computed(() => {
const c = channelRaw.value;
if (!c) return '-';
if (c === 'cmcc') return '移动 (cmcc)';
if (c === 'cucc') return '联通 (cucc)';
if (c === 'ctcc') return '电信 (ctcc)';
return c;
});
const hasParams = computed(() => {
const p = props.params || {};
return p.mobile || p.mobile_no;
});
</script>
<style scoped>
.card {
padding: 1rem;
}
.header-box {
margin-bottom: 1rem;
}
.header-title {
font-size: 1.125rem;
font-weight: 600;
}
.header-desc {
font-size: 0.875rem;
color: #6b7280;
margin-top: 0.25rem;
}
.result-section {
padding: 0.75rem 0.875rem;
border-radius: 0.75rem;
margin-bottom: 1rem;
border: 1px solid #e5e7eb;
}
.result-section.ok {
background: #ecfdf3;
border-color: #22c55e33;
}
.result-section.bad {
background: #fef2f2;
border-color: #ef444433;
}
.result-section.unknown {
background: #f9fafb;
}
.result-main {
display: flex;
align-items: center;
justify-content: space-between;
}
.result-label {
font-size: 0.875rem;
color: #6b7280;
}
.result-value {
font-size: 1rem;
font-weight: 600;
}
.result-ok {
color: #16a34a;
}
.result-bad {
color: #dc2626;
}
.result-unknown {
color: #6b7280;
}
.result-sub {
margin-top: 0.5rem;
font-size: 0.875rem;
color: #4b5563;
}
.info-block {
margin-top: 1rem;
padding: 0.75rem 0.875rem;
border-radius: 0.75rem;
background: #f9fafb;
}
.block-title {
font-size: 0.875rem;
font-weight: 600;
color: #374151;
margin-bottom: 0.5rem;
}
.info-row {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
padding: 0.15rem 0;
}
.info-label {
color: #6b7280;
}
.info-value {
color: #111827;
margin-left: 1rem;
text-align: right;
}
.empty-tip {
color: #9ca3af;
font-size: 0.875rem;
padding: 1rem 0;
text-align: center;
}
</style>

218
src/ui/CYYSYK8R3.vue Normal file
View File

@@ -0,0 +1,218 @@
<template>
<div class="card">
<div class="header-box">
<h3 class="header-title">手机空号检测</h3>
<p class="header-desc">判断号码是空号实号还是沉默/风险号</p>
</div>
<div v-if="hasData" class="result-section" :class="statusSectionClass">
<div class="result-main">
<div class="result-label">号码状态</div>
<div class="result-value" :class="statusClass">
{{ statusText }}
</div>
</div>
<div class="result-sub" v-if="area || channel">
<span v-if="area">归属地{{ area }}</span>
<span v-if="channel" class="ml-2">运营商{{ channel }}</span>
</div>
</div>
<div v-if="hasParams" class="info-block">
<div class="block-title">被核验号码</div>
<div class="info-row">
<span class="info-label">手机号</span>
<span class="info-value font-mono">{{ params.mobile || params.mobile_no || '-' }}</span>
</div>
</div>
<div v-if="!hasData" class="empty-tip">暂无核验结果</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
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 hasData = computed(() => props.data && Object.keys(props.data).length > 0);
const statusCode = computed(() => {
const v = props.data?.status;
if (v == null) return null;
const n = Number(v);
return Number.isFinite(n) ? n : null;
});
const statusText = computed(() => {
switch (statusCode.value) {
case 0:
return '空号';
case 1:
return '实号';
case 2:
return '停机';
case 3:
return '库无(预留)';
case 4:
return '沉默号';
case 5:
return '风险号';
default:
return '未知';
}
});
const statusClass = computed(() => {
const s = statusCode.value;
if (s === 1) return 'result-ok';
if (s === 0 || s === 4 || s === 5) return 'result-bad';
if (s === 2 || s === 3) return 'result-warn';
return 'result-unknown';
});
const statusSectionClass = computed(() => {
const s = statusCode.value;
if (s === 1) return 'result-section ok';
if (s === 0 || s === 4 || s === 5) return 'result-section bad';
if (s === 2 || s === 3) return 'result-section warn';
return 'result-section unknown';
});
const area = computed(() => props.data?.area || '');
const channel = computed(() => props.data?.channel || '');
const hasParams = computed(() => {
const p = props.params || {};
return p.mobile || p.mobile_no;
});
</script>
<style scoped>
.card {
padding: 1rem;
}
.header-box {
margin-bottom: 1rem;
}
.header-title {
font-size: 1.125rem;
font-weight: 600;
}
.header-desc {
font-size: 0.875rem;
color: #6b7280;
margin-top: 0.25rem;
}
.result-section {
padding: 0.75rem 0.875rem;
border-radius: 0.75rem;
margin-bottom: 1rem;
border: 1px solid #e5e7eb;
}
.result-section.ok {
background: #ecfdf3;
border-color: #22c55e33;
}
.result-section.bad {
background: #fef2f2;
border-color: #ef444433;
}
.result-section.warn {
background: #fffbeb;
border-color: #f9731633;
}
.result-section.unknown {
background: #f9fafb;
}
.result-main {
display: flex;
align-items: center;
justify-content: space-between;
}
.result-label {
font-size: 0.875rem;
color: #6b7280;
}
.result-value {
font-size: 1rem;
font-weight: 600;
}
.result-ok {
color: #16a34a;
}
.result-bad {
color: #dc2626;
}
.result-warn {
color: #d97706;
}
.result-unknown {
color: #6b7280;
}
.result-sub {
margin-top: 0.5rem;
font-size: 0.875rem;
color: #4b5563;
}
.info-block {
margin-top: 1rem;
padding: 0.75rem 0.875rem;
border-radius: 0.75rem;
background: #f9fafb;
}
.block-title {
font-size: 0.875rem;
font-weight: 600;
color: #374151;
margin-bottom: 0.5rem;
}
.info-row {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
padding: 0.15rem 0;
}
.info-label {
color: #6b7280;
}
.info-value {
color: #111827;
margin-left: 1rem;
text-align: right;
}
.empty-tip {
color: #9ca3af;
font-size: 0.875rem;
padding: 1rem 0;
text-align: center;
}
</style>

216
src/ui/CYYSYK9R4.vue Normal file
View File

@@ -0,0 +1,216 @@
<template>
<div class="card">
<div class="header-box">
<h3 class="header-title">全网手机三要素验证周更</h3>
<p class="header-desc">核验手机号+身份证+姓名是否一致</p>
</div>
<div v-if="hasData" class="result-section" :class="resultSectionClass">
<div class="result-main">
<div class="result-label">核验结果</div>
<div class="result-value" :class="resultClass">
{{ resultText }}
</div>
</div>
</div>
<div v-if="hasParams" class="info-block">
<div class="block-title">被核验信息</div>
<div class="info-row">
<span class="info-label">手机号</span>
<span class="info-value font-mono">{{ params.mobile || params.mobile_no || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">姓名</span>
<span class="info-value">{{ maskedName }}</span>
</div>
<div class="info-row">
<span class="info-label">身份证号</span>
<span class="info-value font-mono">{{ maskedIdCard }}</span>
</div>
</div>
<div v-if="!hasData" class="empty-tip">暂无核验结果</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
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 hasData = computed(() => props.data && Object.keys(props.data).length > 0);
const state = computed(() => {
const v = props.data?.state;
if (v == null) return null;
return String(v);
});
const resultText = computed(() => {
switch (state.value) {
case '1':
return '验证一致';
case '2':
return '验证不一致';
case '3':
return '异常情况';
default:
return '暂无结果';
}
});
const resultClass = computed(() => {
if (state.value === '1') return 'result-ok';
if (state.value === '2') return 'result-bad';
if (state.value === '3') return 'result-warn';
return 'result-unknown';
});
const resultSectionClass = computed(() => {
if (state.value === '1') return 'result-section ok';
if (state.value === '2') return 'result-section bad';
if (state.value === '3') return 'result-section warn';
return 'result-section unknown';
});
const hasParams = computed(() => {
const p = props.params || {};
return p.mobile || p.mobile_no || p.name || p.id_card;
});
const maskedName = computed(() => {
const name = props.params?.name || '';
if (!name) return '-';
return name.length > 1 ? name[0] + '*'.repeat(name.length - 1) : '*';
});
const maskedIdCard = computed(() => {
const id = props.params?.id_card || '';
if (!id || id.length < 8) return id || '-';
return id.slice(0, 4) + '********' + id.slice(-4);
});
</script>
<style scoped>
.card {
padding: 1rem;
}
.header-box {
margin-bottom: 1rem;
}
.header-title {
font-size: 1.125rem;
font-weight: 600;
}
.header-desc {
font-size: 0.875rem;
color: #6b7280;
margin-top: 0.25rem;
}
.result-section {
padding: 0.75rem 0.875rem;
border-radius: 0.75rem;
margin-bottom: 1rem;
border: 1px solid #e5e7eb;
}
.result-section.ok {
background: #ecfdf3;
border-color: #22c55e33;
}
.result-section.bad {
background: #fef2f2;
border-color: #ef444433;
}
.result-section.warn {
background: #fffbeb;
border-color: #f9731633;
}
.result-section.unknown {
background: #f9fafb;
}
.result-main {
display: flex;
align-items: center;
justify-content: space-between;
}
.result-label {
font-size: 0.875rem;
color: #6b7280;
}
.result-value {
font-size: 1rem;
font-weight: 600;
}
.result-ok {
color: #16a34a;
}
.result-bad {
color: #dc2626;
}
.result-warn {
color: #d97706;
}
.result-unknown {
color: #6b7280;
}
.info-block {
margin-top: 1rem;
padding: 0.75rem 0.875rem;
border-radius: 0.75rem;
background: #f9fafb;
}
.block-title {
font-size: 0.875rem;
font-weight: 600;
color: #374151;
margin-bottom: 0.5rem;
}
.info-row {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
padding: 0.15rem 0;
}
.info-label {
color: #6b7280;
}
.info-value {
color: #111827;
margin-left: 1rem;
text-align: right;
}
.empty-tip {
color: #9ca3af;
font-size: 0.875rem;
padding: 1rem 0;
text-align: center;
}
</style>

172
src/ui/CYYSYP0T4.vue Normal file
View File

@@ -0,0 +1,172 @@
<template>
<div class="card">
<div class="header-box">
<h3 class="header-title">手机号码在网时长</h3>
<p class="header-desc">展示号码在当前运营商下的在网时长区间</p>
</div>
<div v-if="hasData" class="result-section">
<div class="result-main">
<div class="result-label">在网时长</div>
<div class="result-value">
{{ timeText }}
</div>
</div>
<div class="result-sub">
<span>运营商{{ channelText }}</span>
</div>
</div>
<div v-if="hasParams" class="info-block">
<div class="block-title">被核验号码</div>
<div class="info-row">
<span class="info-label">手机号</span>
<span class="info-value font-mono">{{ params.mobile || params.mobile_no || '-' }}</span>
</div>
</div>
<div v-if="!hasData" class="empty-tip">暂无核验结果</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
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 hasData = computed(() => props.data && Object.keys(props.data).length > 0);
const time = computed(() => props.data?.time || '');
const timeText = computed(() => {
const t = time.value;
if (!t) return '-';
switch (t) {
case '[0,3)':
return '03 个月';
case '[3,6)':
return '36 个月';
case '[6,12)':
return '612 个月';
case '[12,24)':
return '1224 个月';
case '[24,-1)':
return '24 个月及以上';
default:
return t;
}
});
const channelRaw = computed(() => props.data?.channel || '');
const channelText = computed(() => {
const c = channelRaw.value;
if (!c) return '-';
if (c === 'cmcc') return '移动 (cmcc)';
if (c === 'cucc') return '联通 (cucc)';
if (c === 'ctcc') return '电信 (ctcc)';
if (c === 'gdcc') return '广电 (gdcc)';
return c;
});
const hasParams = computed(() => {
const p = props.params || {};
return p.mobile || p.mobile_no;
});
</script>
<style scoped>
.card {
padding: 1rem;
}
.header-box {
margin-bottom: 1rem;
}
.header-title {
font-size: 1.125rem;
font-weight: 600;
}
.header-desc {
font-size: 0.875rem;
color: #6b7280;
margin-top: 0.25rem;
}
.result-section {
padding: 0.75rem 0.875rem;
border-radius: 0.75rem;
margin-bottom: 1rem;
border: 1px solid #e5e7eb;
background: #f9fafb;
}
.result-main {
display: flex;
align-items: center;
justify-content: space-between;
}
.result-label {
font-size: 0.875rem;
color: #6b7280;
}
.result-value {
font-size: 1rem;
font-weight: 600;
color: #111827;
}
.result-sub {
margin-top: 0.5rem;
font-size: 0.875rem;
color: #4b5563;
}
.info-block {
margin-top: 1rem;
padding: 0.75rem 0.875rem;
border-radius: 0.75rem;
background: #f9fafb;
}
.block-title {
font-size: 0.875rem;
font-weight: 600;
color: #374151;
margin-bottom: 0.5rem;
}
.info-row {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
padding: 0.15rem 0;
}
.info-label {
color: #6b7280;
}
.info-value {
color: #111827;
margin-left: 1rem;
text-align: right;
}
.empty-tip {
color: #9ca3af;
font-size: 0.875rem;
padding: 1rem 0;
text-align: center;
}
</style>

151
src/ui/CYYSYS9W1.vue Normal file
View File

@@ -0,0 +1,151 @@
<template>
<div class="card">
<div class="header-box">
<h3 class="header-title">手机携号转网</h3>
<p class="header-desc">查询号码是否发生携号转网及前后运营商</p>
</div>
<div v-if="hasData" class="result-section">
<div class="result-main">
<div class="result-label">携号转网情况</div>
<div class="result-value">
{{ portabilityText }}
</div>
</div>
<div class="result-sub">
<span>原运营商{{ ispType || '-' }}</span>
<span class="ml-2">当前运营商{{ newIspType || '-' }}</span>
</div>
</div>
<div v-if="hasParams" class="info-block">
<div class="block-title">被核验号码</div>
<div class="info-row">
<span class="info-label">提交手机号</span>
<span class="info-value font-mono">{{ params.mobile || params.mobile_no || '-' }}</span>
</div>
</div>
<div v-if="!hasData" class="empty-tip">暂无核验结果</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
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 hasData = computed(() => props.data && Object.keys(props.data).length > 0);
const ispType = computed(() => props.data?.ispType || '');
const newIspType = computed(() => props.data?.newIspType || '');
const portabilityText = computed(() => {
if (!ispType.value && !newIspType.value) return '暂无结果';
if (ispType.value && newIspType.value && ispType.value !== newIspType.value) {
return `已发生携号转网(从 ${ispType.value} 转到 ${newIspType.value}`;
}
return '未发现运营商变更';
});
const hasParams = computed(() => {
const p = props.params || {};
return p.mobile || p.mobile_no;
});
</script>
<style scoped>
.card {
padding: 1rem;
}
.header-box {
margin-bottom: 1rem;
}
.header-title {
font-size: 1.125rem;
font-weight: 600;
}
.header-desc {
font-size: 0.875rem;
color: #6b7280;
margin-top: 0.25rem;
}
.result-section {
padding: 0.75rem 0.875rem;
border-radius: 0.75rem;
margin-bottom: 1rem;
border: 1px solid #e5e7eb;
background: #f9fafb;
}
.result-main {
display: flex;
align-items: center;
justify-content: space-between;
}
.result-label {
font-size: 0.875rem;
color: #6b7280;
}
.result-value {
font-size: 0.95rem;
font-weight: 600;
color: #111827;
}
.result-sub {
margin-top: 0.5rem;
font-size: 0.875rem;
color: #4b5563;
}
.info-block {
margin-top: 1rem;
padding: 0.75rem 0.875rem;
border-radius: 0.75rem;
background: #f9fafb;
}
.block-title {
font-size: 0.875rem;
font-weight: 600;
color: #374151;
margin-bottom: 0.5rem;
}
.info-row {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
padding: 0.15rem 0;
}
.info-label {
color: #6b7280;
}
.info-value {
color: #111827;
margin-left: 1rem;
text-align: right;
}
.empty-tip {
color: #9ca3af;
font-size: 0.875rem;
padding: 1rem 0;
text-align: center;
}
</style>

View File

@@ -0,0 +1,261 @@
<template>
<div class="tyzm-section">
<div class="section-title">司法案件信息</div>
<!-- 案件概览 -->
<div class="sub-title">
案件概览
<span class="tag" :class="riskLevelCls(courtInfoRiskLevel)">{{ riskLevelText(courtInfoRiskLevel) }}</span>
</div>
<table class="info-table">
<tbody>
<tr>
<th>被告案件数</th>
<td>{{ fmtCount(co.beigaoTotalCasesCounts, '件') }}</td>
<th>未结案数</th>
<td>{{ fmtCount(co.beigaoTotalWeiCaseCounts, '件') }}</td>
</tr>
<tr>
<th>执行案件数</th>
<td>{{ fmtCount(co.executionCaseCounts, '件') }}</td>
<th>涉案金额被告</th>
<td>{{ co.beigaoTotalCaseAmounts != null ? `${co.beigaoTotalCaseAmounts} 万元` : '—' }}</td>
</tr>
<tr>
<th>失信命中</th>
<td>
<span class="tag" :class="co.disinCaseCounts ? 'tag-danger' : 'tag-success'">
{{ co.disinCaseCounts ? '命中' : '未命中' }}
</span>
</td>
<th>限高命中</th>
<td>
<span class="tag" :class="co.limitCaseCounts ? 'tag-danger' : 'tag-success'">
{{ co.limitCaseCounts ? '命中' : '未命中' }}
</span>
</td>
</tr>
<tr>
<th>关联企业</th>
<td>{{ co.affiliateCompany ?? '—' }}</td>
<th>最近案件时间</th>
<td>{{ co.leastCaseTime || '—' }}</td>
</tr>
</tbody>
</table>
<!-- 案件统计 -->
<div class="sub-title">案件统计</div>
<div class="grid-4">
<div class="mini-card" v-for="item in caseTypeMetrics" :key="item.label"
:class="{ 'card-danger': item.count > 0 }">
<div class="mini-label">{{ item.label }}</div>
<div class="mini-value" :class="item.count > 0 ? 'text-danger' : ''">{{ item.count }} </div>
</div>
</div>
<!-- 案件详细列表 -->
<div v-for="section in caseSections" :key="section.key">
<div class="sub-title" v-if="section.items && section.items.length">
{{ section.label }}
<span class="tag tag-info">{{ section.items.length }} </span>
</div>
<div v-if="section.items && section.items.length" class="case-list">
<div v-for="(c, i) in section.items" :key="i"
class="case-item"
:class="{ danger: c.nssdw === '被告' || c.nssdw === '被执行人' }">
<div class="case-header">
<span class="case-number">{{ c.caseNumber || '—' }}</span>
<span class="tag" :class="statusCls(c.caseStatus)">{{ c.caseStatus || '—' }}</span>
</div>
<div class="case-body">
<p>
<span class="case-label">诉讼地位</span>{{ c.nssdw || '—' }}
<span class="case-label ml-4">案件类型</span>{{ c.najlx || '—' }}
</p>
<p v-if="c.nlaayTree">
<span class="case-label">案由</span>{{ c.nlaayTree }}
</p>
<p>
<span class="case-label">立案时间</span>{{ c.dlarq || '—' }}
<span class="case-label ml-4">结案时间</span>{{ c.djarq || '—' }}
</p>
<p>
<span class="case-label">经办法院</span>{{ c.njbfy || '—' }}
</p>
<p v-if="c.njabdje">
<span class="case-label">结案标的金额</span>{{ c.njabdje }}
</p>
<p v-if="c.njafs">
<span class="case-label">结案方式</span>{{ c.njafs }}
</p>
<p v-if="c.nssdw === '被告' || c.nssdw === '被执行人'">
<span class="case-label">胜诉估计</span>
<span class="tag" :class="victoryCls(c.npjVictory)">{{ c.npjVictory || '—' }}</span>
</p>
<p v-if="c.nsqzxbdje">
<span class="case-label">申请执行标的金额</span>{{ c.nsqzxbdje }}
</p>
<p v-if="c.nwzxje">
<span class="case-label">未执行金额</span>{{ c.nwzxje }}
</p>
<p v-if="c.cahHx">
<span class="case-label">后续案件</span>{{ c.cahHx }}
</p>
<p v-if="c.cdsrxx" class="text-xs text-gray-400 mt-2" style="word-break:break-all">
<span class="case-label">当事人</span>{{ truncate(c.cdsrxx, 200) }}
</p>
</div>
</div>
</div>
</div>
<!-- 案件分布统计 -->
<div v-if="caseCounts" class="sub-title">案件分布统计</div>
<table v-if="caseCounts" class="info-table">
<thead>
<tr>
<th>统计维度</th>
<th>被告案件数</th>
<th>被告案件金额</th>
<th>原告案件数</th>
<th>原告案件金额</th>
</tr>
</thead>
<tbody>
<tr>
<td>总计</td>
<td>{{ caseCounts.beigaoTotalCasesCounts || '—' }}</td>
<td>{{ caseCounts.beigaoTotalCaseAmounts || '—' }}</td>
<td>{{ caseCounts.yuangaoTotalCasesCounts || '—' }}</td>
<td>{{ caseCounts.yuangaoTotalCaseAmounts || '—' }}</td>
</tr>
<tr v-if="caseCounts.caseActionDistribution && caseCounts.caseActionDistribution !== '-'">
<td>案由分布</td>
<td colspan="4">{{ caseCounts.caseActionDistribution }}</td>
</tr>
<tr v-if="caseCounts.localDistribution && caseCounts.localDistribution !== '-'">
<td>地区分布</td>
<td colspan="4">{{ caseCounts.localDistribution }}</td>
</tr>
<tr v-if="caseCounts.timeDistribution && caseCounts.timeDistribution !== '-'">
<td>时间分布</td>
<td colspan="4">{{ caseCounts.timeDistribution }}</td>
</tr>
</tbody>
</table>
<div v-if="!hasData" class="empty-hint">暂无司法案件数据</div>
</div>
</template>
<script setup>
const props = defineProps({
data: { type: Object, default: () => ({}) },
});
const co = computed(() => props.data.caseOverviewInfo || {});
const ci = computed(() => props.data.courtInfo || {});
const courtInfoRiskLevel = computed(() => props.data.courtInfoRiskLevel || '');
const mci = computed(() => ci.value.newMultCourtInfo || {});
const caseCounts = computed(() => mci.value.caseCounts || null);
const hasData = computed(() => Object.keys(props.data).length > 0);
const riskLevelCls = (lv) => {
if (lv === '1' || lv === 1) return 'tag-danger';
if (lv === '2' || lv === 2) return 'tag-warning';
return 'tag-success';
};
const riskLevelText = (lv) => {
if (lv === '1' || lv === 1) return '高风险';
if (lv === '2' || lv === 2) return '中风险';
return '低风险';
};
const fmtCount = (v, unit) => {
if (v == null || v === '') return '—';
return `${v} ${unit}`;
};
const statusCls = (s) => {
if (!s) return '';
if (s.includes('未结')) return 'tag-warning';
if (s.includes('已结')) return 'tag-success';
return 'tag-info';
};
const victoryCls = (v) => {
if (v === '胜诉') return 'tag-success';
if (v === '败诉') return 'tag-danger';
if (v === '部分胜诉') return 'tag-warning';
return 'tag-info';
};
const truncate = (str, len) => {
if (!str) return '—';
return str.length > len ? str.slice(0, len) + '...' : str;
};
const caseTypeMetrics = computed(() => [
{ label: '民事案件', count: ci.value.civilCasesCount || 0 },
{ label: '刑事案件', count: ci.value.criminalCasesCount || 0 },
{ label: '执行案件', count: ci.value.enforcementCasesCount || 0 },
{ label: '行政案件', count: ci.value.administrativeCasesCount || 0 },
{ label: '非诉保全', count: ci.value.preservationCasesCount || 0 },
{ label: '赔偿案件', count: ci.value.compensationCasesCount || 0 },
{ label: '破产案件', count: ci.value.bankruptcyCasesCount || 0 },
{ label: '管辖案件', count: ci.value.supervisionCasesCount || 0 },
]);
const caseSections = computed(() => {
const m = mci.value;
return [
{ key: 'civil', label: '民事案件', items: m.civilCases || [] },
{ key: 'enforcement', label: '执行案件', items: m.enforcementCases || [] },
{ key: 'criminal', label: '刑事案件', items: m.criminalCases || [] },
{ key: 'administrative', label: '行政案件', items: m.administrativeCases || [] },
{ key: 'preservation', label: '非诉保全审查', items: m.preservationCases || [] },
{ key: 'supervision', label: '管辖案件', items: m.supervisionCases || [] },
{ key: 'compensation', label: '赔偿案件', items: m.compensationCases || [] },
{ key: 'bankruptcy', label: '破产案件', items: m.bankruptcyCases || [] },
{ key: 'disin', label: '失信公告', items: m.disinCases || [] },
{ key: 'limit', label: '限高公告', items: m.limitCases || [] },
];
});
</script>
<style scoped lang="scss">
@import '../shared.scss';
.grid-4 {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin-bottom: 12px;
}
.mini-card {
background: #f9fafb;
border-radius: 6px;
padding: 10px;
text-align: center;
&.card-danger { background: #fff5f5; }
}
.mini-label {
font-size: 12px;
color: #6b7280;
margin-bottom: 4px;
}
.mini-value {
font-size: 16px;
font-weight: 700;
}
.text-danger { color: #d32f2f; }
.tag-info { background: #e8f0fe; color: #1a73e8; }
.ml-4 { margin-left: 16px; }
.text-xs { font-size: 12px; }
.text-gray-400 { color: #9ca3af; }
.mt-2 { margin-top: 8px; }
@media (max-width: 768px) {
.grid-4 { grid-template-columns: repeat(2, 1fr); }
}
</style>

View File

@@ -0,0 +1,111 @@
<template>
<div class="tyzm-section">
<div class="section-title">租赁行为</div>
<!-- 租赁行为 -->
<div class="sub-title">
租赁行为
<span class="tag" :class="riskLevelCls(reRiskLevel)">{{ riskLevelText(reRiskLevel) }}</span>
</div>
<table class="info-table">
<thead>
<tr>
<th>时间范围</th>
<th>申请机构数</th>
<th>申请次数</th>
<th>周末申请机构数</th>
<th>周末申请次数</th>
<th>夜间申请机构数</th>
<th>夜间申请次数</th>
</tr>
</thead>
<tbody>
<tr v-for="item in rentalRows" :key="item.label">
<td>{{ item.label }}</td>
<td>{{ item.inst }}</td>
<td>{{ item.cnt }}</td>
<td>{{ item.wkInst }}</td>
<td>{{ item.wkCnt }}</td>
<td>{{ item.ntInst }}</td>
<td>{{ item.ntCnt }}</td>
</tr>
</tbody>
</table>
<!-- 关联风险监督 -->
<div class="sub-title">
关联风险监督
<span class="tag" :class="riskLevelCls(supRiskLevel)">{{ riskLevelText(supRiskLevel) }}</span>
</div>
<table class="info-table">
<tbody>
<tr>
<th>最后申请时间</th>
<td>{{ rs.leastTime || '—' }}</td>
<th>同一身份证关联手机号数</th>
<td>{{ rs.rentalRPhones ?? '—' }}</td>
</tr>
<tr>
<th>同一手机号关联身份证数</th>
<td>{{ rs.rentalRCards ?? '—' }}</td>
<th>详情</th>
<td>{{ rs.details || '—' }}</td>
</tr>
</tbody>
</table>
<div v-if="!hasData" class="empty-hint">暂无租赁行为数据</div>
</div>
</template>
<script setup>
const props = defineProps({
data: { type: Object, default: () => ({}) },
});
const reRiskLevel = computed(() => props.data.reRiskLevel || '');
const supRiskLevel = computed(() => props.data.supRiskLevel || '');
const rb = computed(() => props.data.rentalBehavior || {});
const rs = computed(() => props.data.riskSupervision || {});
const hasData = computed(() => Object.keys(props.data).length > 0);
const riskLevelCls = (lv) => {
if (lv === '1' || lv === 1) return 'tag-danger';
if (lv === '2' || lv === 2) return 'tag-warning';
return 'tag-success';
};
const riskLevelText = (lv) => {
if (lv === '1' || lv === 1) return '高风险';
if (lv === '2' || lv === 2) return '中风险';
return '低风险';
};
const periods = [
{ key: '3d', label: '近3天' },
{ key: '7d', label: '近7天' },
{ key: '14d', label: '近14天' },
{ key: '1m', label: '近1个月' },
{ key: '3m', label: '近3个月' },
{ key: '6m', label: '近6个月' },
{ key: '12m', label: '近12个月' },
];
const rentalRows = computed(() => {
const b = rb.value;
return periods.map(p => ({
label: p.label,
inst: b[`rentInst${p.key}`] || '—',
cnt: b[`rentCnt${p.key}`] || '—',
wkInst: b[`rentInst${p.key}Wk`] || '—',
wkCnt: b[`rentCnt${p.key}Wk`] || '—',
ntInst: b[`rentInst${p.key}Nt`] || '—',
ntCnt: b[`rentCnt${p.key}Nt`] || '—',
}));
});
</script>
<style scoped lang="scss">
@import '../shared.scss';
</style>

View File

@@ -0,0 +1,209 @@
<template>
<div class="tyzm-section">
<div class="section-title">借贷评估产品 - 命中详情</div>
<!-- 查询多头概览 -->
<div class="sub-title">
查询多头概览
<span class="tag" :class="riskLevelCls(qmriskLevel)">{{ riskLevelText(qmriskLevel) }}</span>
</div>
<!-- 命中次数统计 -->
<div class="sub-section-label">命中次数统计</div>
<div class="grid-3">
<div class="mini-card" v-for="item in hitMetrics" :key="item.label">
<div class="mini-label">{{ item.label }}</div>
<div class="mini-value" :class="Number(item.value) > 0 ? 'text-danger' : ''">{{ item.value }}</div>
</div>
</div>
<!-- 近6个月贷款机构数 -->
<div class="sub-section-label">近6个月贷款机构数</div>
<table class="info-table">
<thead>
<tr>
<th>时间范围</th>
<th>贷款机构数</th>
</tr>
</thead>
<tbody>
<tr v-for="item in loanOrgMetrics" :key="item.label">
<td>{{ item.label }}</td>
<td>{{ item.value || '—' }}</td>
</tr>
</tbody>
</table>
<!-- 各机构多头查询表现 -->
<div class="sub-title">
各机构多头查询表现
<span class="tag" :class="riskLevelCls(qriskLevel)">{{ riskLevelText(qriskLevel) }}</span>
</div>
<!-- 机构类型汇总 -->
<div class="sub-section-label">机构查询汇总机构数 / 次数</div>
<table class="info-table">
<thead>
<tr>
<th>机构类型</th>
<th>近7天</th>
<th>近30天</th>
<th>近90天</th>
<th>近180天</th>
<th>近1年</th>
</tr>
</thead>
<tbody>
<tr v-for="row in institutionSummary" :key="row.name">
<td>{{ row.name }}</td>
<td>{{ row.d7 }}</td>
<td>{{ row.d30 }}</td>
<td>{{ row.d90 }}</td>
<td>{{ row.d180 }}</td>
<td>{{ row.d365 }}</td>
</tr>
</tbody>
</table>
<!-- 夜间查询 -->
<div class="sub-section-label">夜间查询汇总机构数 / 次数</div>
<table class="info-table">
<thead>
<tr>
<th>机构类型</th>
<th>近7天</th>
<th>近30天</th>
<th>近90天</th>
<th>近180天</th>
<th>近1年</th>
</tr>
</thead>
<tbody>
<tr v-for="row in nightSummary" :key="row.name">
<td>{{ row.name }}</td>
<td>{{ row.d7 }}</td>
<td>{{ row.d30 }}</td>
<td>{{ row.d90 }}</td>
<td>{{ row.d180 }}</td>
<td>{{ row.d365 }}</td>
</tr>
</tbody>
</table>
<div v-if="!hasData" class="empty-hint">暂无借贷评估数据</div>
</div>
</template>
<script setup>
const props = defineProps({
data: { type: Object, default: () => ({}) },
});
const mm = computed(() => props.data.moreMuti || {});
const lease = computed(() => props.data.leaseIndexLoanSituation || {});
const qmriskLevel = computed(() => props.data.qmriskLevel || '');
const qriskLevel = computed(() => props.data.qriskLevel || '');
const hasData = computed(() => Object.keys(props.data).length > 0);
const riskLevelCls = (lv) => {
if (lv === '1' || lv === 1) return 'tag-danger';
if (lv === '2' || lv === 2) return 'tag-warning';
return 'tag-success';
};
const riskLevelText = (lv) => {
if (lv === '1' || lv === 1) return '高风险';
if (lv === '2' || lv === 2) return '中风险';
return '低风险';
};
const hitMetrics = computed(() => {
const m = mm.value;
return [
{ label: '历史关注名单命中', value: m.hallOrgHit || '0' },
{ label: '近180天关注名单', value: m.hallOrg180Days || '0' },
{ label: '近365天关注名单', value: m.hallOrg365Days || '0' },
{ label: '历史反欺诈命中', value: m.hallOrgFraudHit || '0' },
{ label: '近180天反欺诈', value: m.hallOrgF180Days || '0' },
{ label: '近365天反欺诈', value: m.hallOrgF365Days || '0' },
];
});
const loanOrgMetrics = computed(() => {
const l = lease.value;
return [
{ label: '近1天', value: l.loanOrgCountDay },
{ label: '近7天', value: l.loanOrgCount7Days },
{ label: '近14天', value: l.loanOrgCount14Days },
{ label: '近21天', value: l.loanOrgCount21Days },
{ label: '近30天', value: l.loanOrgCount30Days },
{ label: '近90天', value: l.loanOrgCount90Days },
{ label: '近180天', value: l.loanOrgCount180Days },
];
});
const pair = (m, orgKey, numKey) => {
const org = m[orgKey] || '0';
const num = m[numKey] || '0';
return `${org} / ${num}`;
};
const institutionSummary = computed(() => {
const m = mm.value;
return [
{ name: '银行', d7: pair(m, 'mk7DaysOrg', 'mk7DaysNum'), d30: pair(m, 'mk30DaysOrg', 'mk30DaysNum'), d90: pair(m, 'mk90DaysOrg', 'mk90DaysNum'), d180: pair(m, 'mk180DaysOrg', 'mk180DaysNum'), d365: pair(m, 'mk1YearOrg', 'mk1YearNum') },
{ name: '大型商业银行', d7: pair(m, 'mdk7DaysOrg', 'mdk7DaysOrg'), d30: pair(m, 'mdk30DaysOrg', 'mdk30DaysOrg'), d90: pair(m, 'mdk90DaysOrg', 'mdk90DaysOrg'), d180: pair(m, 'mdk180DaysOrg', 'mdk180DaysOrg'), d365: pair(m, 'mdk1YearOrg', 'mdk1YearOrg') },
{ name: '城市商业银行', d7: pair(m, 'mu7DaysOrg', 'mu7DaysNum'), d30: pair(m, 'mu30DaysOrg', 'mu30DaysNum'), d90: pair(m, 'mu90DaysOrg', 'mu90DaysNum'), d180: pair(m, 'mu180DaysOrg', 'mu180DaysNum'), d365: pair(m, 'mu1YearOrg', 'mu1YearNum') },
{ name: '消金公司', d7: pair(m, 'muC7DaysOrg', 'muA7DaysNum'), d30: pair(m, 'muC30DaysOrg', 'muA30DaysNum'), d90: pair(m, 'muC90DaysOrg', 'muA90DaysNum'), d180: pair(m, 'muC180DaysOrg', 'muA180DaysNum'), d365: pair(m, 'muC1YearOrg', 'muA1YearNum') },
{ name: '小额贷款公司', d7: pair(m, 'muS7DaysOrg', 'muS7DaysNum'), d30: pair(m, 'muS30DaysOrg', 'muS30DaysNum'), d90: pair(m, 'muS90DaysOrg', 'muS90DaysNum'), d180: pair(m, 'muS180DaysOrg', 'muS180DaysNum'), d365: pair(m, 'muS1YearOrg', 'muS1YearNum') },
{ name: '网络小额贷款', d7: pair(m, 'muN7DaysOrg', 'muN7DaysNum'), d30: pair(m, 'muN30DaysOrg', 'muN30DaysNum'), d90: pair(m, 'muN90DaysOrg', 'muN90DaysNum'), d180: pair(m, 'muN180DaysOrg', 'muN180DaysNum'), d365: pair(m, 'muN1YearOrg', 'muN1YearNum') },
{ name: '金融平台', d7: pair(m, 'muF7DaysOrg', 'muF7DaysNum'), d30: pair(m, 'muF30DaysOrg', 'muF30DaysNum'), d90: pair(m, 'muF90DaysOrg', 'muF90DaysNum'), d180: pair(m, 'muF180DaysOrg', 'muF180DaysNum'), d365: pair(m, 'muF1YearOrg', 'muF1YearNum') },
{ name: '汽车金融', d7: pair(m, 'muQ7DaysOrg', 'muQ7DaysNum'), d30: pair(m, 'muQ30DaysOrg', 'muQ30DaysNum'), d90: pair(m, 'muQ90DaysOrg', 'muQ90DaysNum'), d180: pair(m, 'muQ180DaysOrg', 'muQ180DaysNum'), d365: pair(m, 'muQ1YearOrg', 'muQ1YearNum') },
{ name: '共计', d7: pair(m, 'muT7DaysOrg', 'muT7DaysNum'), d30: pair(m, 'muT30DaysOrg', 'muT30DaysNum'), d90: pair(m, 'muT90DaysOrg', 'muT90DaysNum'), d180: pair(m, 'muT180DaysOrg', 'muT180DaysNum'), d365: pair(m, 'muT1YearOrg', 'muT1YearNum') },
];
});
const nightSummary = computed(() => {
const m = mm.value;
return [
{ name: '大型商业银行', d7: pair(m, 'muBBank7DaysOrg', 'muBBank7DaysNum'), d30: pair(m, 'muBBank30DaysOrg', 'muBBank30DaysNum'), d90: pair(m, 'muBBank90DaysOrg', 'muBBank90DaysNum'), d180: pair(m, 'muBBank180DaysOrg', 'muBBank180DaysNum'), d365: pair(m, 'muBBank1YearOrg', 'muBBank1YearNum') },
{ name: '城市商业银行', d7: pair(m, 'muCBank7DaysOrg', 'muCBank7DaysNum'), d30: pair(m, 'muCBank30DaysOrg', 'muCBank30DaysNum'), d90: pair(m, 'muCBank90DaysOrg', 'muCBank90DaysNum'), d180: pair(m, 'muCBank180DaysOrg', 'muCBank180DaysNum'), d365: pair(m, 'muCBank1YearOrg', 'muCBank1YearNum') },
{ name: '非银', d7: pair(m, 'muFBank7DaysOrg', 'muFBank7DaysNum'), d30: pair(m, 'muFBank30DaysOrg', 'muFBank30DaysNum'), d90: pair(m, 'muFBank90DaysOrg', 'muFBank90DaysNum'), d180: pair(m, 'muFBank180DaysOrg', 'muFBank180DaysNum'), d365: pair(m, 'muFBank1YearOrg', 'muFBank1YearNum') },
{ name: '共计', d7: pair(m, 'muTotal7DaysOrg', 'muTotal7DaysNum'), d30: pair(m, 'muTotal30DaysOrg', 'muTotal30DaysNum'), d90: pair(m, 'muTotal90DaysOrg', 'muTotal90DaysNum'), d180: pair(m, 'muTotal180DaysOrg', 'muTotal180DaysNum'), d365: pair(m, 'muTotal1YearOrg', 'muTotal1YearNum') },
];
});
</script>
<style scoped lang="scss">
@import '../shared.scss';
.grid-3 {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 12px;
}
.mini-card {
background: #f9fafb;
border-radius: 6px;
padding: 10px;
text-align: center;
}
.mini-label {
font-size: 12px;
color: #6b7280;
margin-bottom: 4px;
}
.mini-value {
font-size: 16px;
font-weight: 700;
}
.text-danger { color: #d32f2f; }
.sub-section-label {
font-size: 13px;
font-weight: 600;
color: #6b7280;
margin: 10px 0 8px;
}
@media (max-width: 768px) {
.grid-3 { grid-template-columns: repeat(2, 1fr); }
}
</style>

View File

@@ -0,0 +1,205 @@
<template>
<div class="tyzm-section">
<div class="section-title">逾期风险详情</div>
<!-- 逾期概览 -->
<table class="info-table">
<tbody>
<tr>
<th>风险等级</th>
<td>
<span class="tag" :class="riskLevel(overRiskLevel).cls">{{ riskLevel(overRiskLevel).text }}</span>
</td>
<th>当前逾期状态</th>
<td>
<span class="tag" :class="overProduct.hasUnsetDue === '逾期' ? 'tag-danger' : 'tag-success'">
{{ overProduct.hasUnsetDue || '—' }}
</span>
</td>
</tr>
<tr>
<th>逾期机构数探查</th>
<td>{{ fmtCount(probe.currentOverdueOrgCount, '家') }}</td>
<th>当前逾期金额</th>
<td>{{ overProduct.curDueAmt || '—' }}</td>
</tr>
<tr>
<th>当前逾期机构数</th>
<td>{{ overProduct.curDueCnt || '—' }}</td>
<th>已结清机构数</th>
<td>{{ overProduct.settlCnt || '—' }}</td>
</tr>
<tr>
<th>贷款总机构</th>
<td>{{ overProduct.totalLoanInst || '—' }}</td>
<th>贷款已还款总金额</th>
<td>{{ overProduct.totalRepayAmt || '—' }}</td>
</tr>
</tbody>
</table>
<!-- 逾期天数/期数概览 -->
<div class="sub-title">逾期天数 / 期数概览</div>
<div class="grid-3">
<div class="mini-card" v-for="item in overdueDayMetrics" :key="item.label">
<div class="mini-label">{{ item.label }}</div>
<div class="mini-value" :class="item.isZero ? '' : 'text-danger'">{{ item.value }}</div>
</div>
</div>
<!-- 近期逾期分布 -->
<div class="sub-title">近期逾期分布</div>
<div class="grid-4">
<div class="mini-card" v-for="item in recentOverdueMetrics" :key="item.label">
<div class="mini-label">{{ item.label }}</div>
<div class="mini-value" :class="item.danger ? 'text-danger' : 'text-success'">{{ item.value }}</div>
</div>
</div>
<!-- 探查逾期 -->
<div class="sub-title">探查逾期</div>
<table class="info-table">
<tbody>
<tr>
<th>风险等级</th>
<td>
<span class="tag" :class="riskLevel(proRiskLevel).cls">{{ riskLevel(proRiskLevel).text }}</span>
</td>
<th>当前逾期机构数</th>
<td>{{ fmtCount(probe.currentOverdueOrgCount, '家') }}</td>
</tr>
<tr>
<th>异常还款机构数</th>
<td>{{ probe.accExcCount ?? '—' }}</td>
<th>睡眠机构数</th>
<td>{{ probe.accSleepCount ?? '—' }}</td>
</tr>
<tr>
<th>当前履约机构数</th>
<td>{{ probe.currentlyPerformanceCount ?? '—' }}</td>
<th>当前履约笔数</th>
<td>{{ probe.countPerformanceCount ?? '—' }}</td>
</tr>
<tr>
<th>最大逾期金额</th>
<td>{{ probe.maxOverdueAmt || '—' }}</td>
<th>最大逾期天数</th>
<td>{{ probe.maxOverdueDays || '—' }}</td>
</tr>
<tr>
<th>最大履约金额</th>
<td>{{ probe.maxPerformanceAmt || '—' }}</td>
<th>最近逾期时间</th>
<td>{{ probe.latestOverdueTime || '—' }}</td>
</tr>
<tr>
<th>最近履约时间</th>
<td colspan="3">{{ probe.latestPerformanceTime || '—' }}</td>
</tr>
</tbody>
</table>
<!-- 特殊名单验证 -->
<div v-if="specialList && specialList.length" class="sub-title">特殊名单验证
<span class="tag ml-2" :class="riskLevel(specialRiskLevel).cls">{{ riskLevel(specialRiskLevel).text }}</span>
</div>
<table v-if="specialList && specialList.length" class="info-table">
<thead>
<tr>
<th style="width:60%">命中内容</th>
<th style="width:40%">命中结果</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, i) in specialList" :key="i">
<td>{{ item.mz || '—' }}</td>
<td>{{ item.jg || '—' }}</td>
</tr>
</tbody>
</table>
<div v-else class="empty-hint">暂无特殊名单验证数据</div>
</div>
</template>
<script setup>
const props = defineProps({
data: { type: Object, default: () => ({}) },
});
const overRiskLevel = computed(() => props.data.overRiskLevel || '');
const proRiskLevel = computed(() => props.data.proRiskLevel || '');
const specialRiskLevel = computed(() => props.data.specialRiskLevel || '');
const overProduct = computed(() => props.data.overdueRiskProduct || {});
const probe = computed(() => props.data.probeOverdueRecord || {});
const specialList = computed(() => props.data.specialListVerification || []);
const riskLevel = (level) => {
if (level === '1' || level === 1) return { text: '高风险', cls: 'tag-danger' };
if (level === '2' || level === 2) return { text: '中风险', cls: 'tag-warning' };
return { text: '低风险', cls: 'tag-success' };
};
const fmtCount = (v, unit) => {
if (v == null || v === '') return '—';
return `${v} ${unit}`;
};
const isZero = (v) => v == null || v === '' || String(v) === '0';
const overdueDayMetrics = computed(() => {
const p = overProduct.value;
return [
{ label: '当前最大天数(所有)', value: isZero(p.cuAllMaxDay) ? '—' : `${p.cuAllMaxDay}`, isZero: isZero(p.cuAllMaxDay) },
{ label: '当前最大天数(非循环贷)', value: isZero(p.cuNlMaxDay) ? '—' : `${p.cuNlMaxDay}`, isZero: isZero(p.cuNlMaxDay) },
{ label: '历史最大天数(所有)', value: isZero(p.hiAllMaxDay) ? '—' : `${p.hiAllMaxDay}`, isZero: isZero(p.hiAllMaxDay) },
{ label: '当前最大期数(所有)', value: isZero(p.cuAllMaxIssue) ? '—' : `${p.cuAllMaxIssue}`, isZero: isZero(p.cuAllMaxIssue) },
{ label: '当前最大期数(非循环贷)', value: isZero(p.cuNlMaxIssue) ? '—' : `${p.cuNlMaxIssue}`, isZero: isZero(p.cuNlMaxIssue) },
];
});
const recentOverdueMetrics = computed(() => {
const p = overProduct.value;
return [
{ label: '近1天', value: p.overdue1d || '—', danger: p.overdue1d === '逾期' },
{ label: '近7天', value: p.overdue7d || '—', danger: p.overdue7d === '逾期' },
{ label: '近14天', value: p.overdue14d || '—', danger: p.overdue14d === '逾期' },
{ label: '近30天', value: p.overdue30d || '—', danger: p.overdue30d === '逾期' },
];
});
</script>
<style scoped lang="scss">
@import '../shared.scss';
.grid-3 {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 12px;
}
.grid-4 {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin-bottom: 12px;
}
.mini-card {
background: #f9fafb;
border-radius: 6px;
padding: 10px;
text-align: center;
}
.mini-label {
font-size: 12px;
color: #6b7280;
margin-bottom: 4px;
}
.mini-value {
font-size: 16px;
font-weight: 700;
}
.text-danger { color: #d32f2f; }
.text-success { color: #008000; }
@media (max-width: 768px) {
.grid-3, .grid-4 { grid-template-columns: repeat(2, 1fr); }
}
</style>

View File

@@ -0,0 +1,346 @@
<template>
<div class="tyzm-section">
<div class="section-title">履约放款产品</div>
<!-- 海纳履约 - 风险等级 -->
<div class="sub-title">
海纳履约
<span class="tag" :class="riskLevelCls(hnRiskLevel)">{{ riskLevelText(hnRiskLevel) }}</span>
</div>
<!-- 放款统计 -->
<div class="sub-section-label">放款统计</div>
<table class="info-table">
<thead>
<tr>
<th>指标</th>
<th>近1月</th>
<th>近3月</th>
<th>近6月</th>
<th>近12月</th>
<th>近24月</th>
</tr>
</thead>
<tbody>
<tr>
<td>放贷次数</td>
<td>{{ ls.lendLastMonth ?? '—' }}</td>
<td>{{ ls.lendCoLast3Months ?? '—' }}</td>
<td>{{ ls.lendCoLast6Month ?? '—' }}</td>
<td>{{ ls.lendCoLast12Month ?? '—' }}</td>
<td>{{ ls.lendCoLast24Month ?? '—' }}</td>
</tr>
<tr>
<td>放款机构数</td>
<td>{{ ls.lendOrgLastMonth ?? '—' }}</td>
<td>{{ ls.lendOrgLast3Months ?? '—' }}</td>
<td>{{ ls.lendOrgLast6Month ?? '—' }}</td>
<td>{{ ls.lendOrgLast12Month ?? '—' }}</td>
<td>{{ ls.lendOrgLast24Month ?? '—' }}</td>
</tr>
<tr>
<td>放贷金额</td>
<td>{{ ls.lendAmLastMonth || '—' }}</td>
<td>{{ ls.lendAmLast3Month || '—' }}</td>
<td>{{ ls.lendAmLast6Month || '—' }}</td>
<td>{{ ls.lendAmLast12Month || '—' }}</td>
<td>{{ ls.lendAmLast24Month || '—' }}</td>
</tr>
</tbody>
</table>
<!-- 放款详情 -->
<div class="sub-section-label">放款详情</div>
<div class="grid-3">
<div class="mini-card">
<div class="mini-label">最近放款日期</div>
<div class="mini-value">{{ ls.lastLendDate || '—' }}</div>
</div>
<div class="mini-card">
<div class="mini-label">距最近放款天数</div>
<div class="mini-value">{{ ls.daysSinceLastLend || '—' }}</div>
</div>
<div class="mini-card">
<div class="mini-label">24月分期类机构</div>
<div class="mini-value">{{ ls.installmenCount ?? '—' }}</div>
</div>
<div class="mini-card">
<div class="mini-label">12月分期类机构</div>
<div class="mini-value">{{ ls.installmen12Count ?? '—' }}</div>
</div>
<div class="mini-card">
<div class="mini-label">24月网络贷款机构</div>
<div class="mini-value">{{ ls.onlineLendCount ?? '—' }}</div>
</div>
<div class="mini-card">
<div class="mini-label">12月网络贷款机构</div>
<div class="mini-value">{{ ls.onlineLend12Count ?? '—' }}</div>
</div>
</div>
<!-- 近12个月贷款金额分布 -->
<div class="sub-section-label">近12个月贷款金额分布</div>
<div class="grid-4">
<div class="mini-card">
<div class="mini-label">1000以下</div>
<div class="mini-value">{{ ls.loan12Count1000F ?? '—' }} </div>
</div>
<div class="mini-card">
<div class="mini-label">1000-3000</div>
<div class="mini-value">{{ ls.loan12Count3000F ?? '—' }} </div>
</div>
<div class="mini-card">
<div class="mini-label">3000-10000</div>
<div class="mini-value">{{ ls.loan12Count10000F ?? '—' }} </div>
</div>
<div class="mini-card">
<div class="mini-label">10000以上</div>
<div class="mini-value">{{ ls.loan12Count10000M ?? '—' }} </div>
</div>
</div>
<!-- 履约情况 -->
<div class="sub-section-label">履约情况</div>
<table class="info-table">
<thead>
<tr>
<th>指标</th>
<th>近1月</th>
<th>近3月</th>
<th>近6月</th>
<th>近12月</th>
<th>近24月</th>
</tr>
</thead>
<tbody>
<tr>
<td>履约次数</td>
<td>{{ perf.perLastMonth ?? '—' }}</td>
<td>{{ perf.perLast3Months ?? '—' }}</td>
<td>{{ perf.perLast6Months ?? '—' }}</td>
<td>{{ perf.perLast12Months ?? '—' }}</td>
<td>{{ perf.perLast24Months ?? '—' }}</td>
</tr>
<tr>
<td>履约金额</td>
<td>{{ perf.perAmLastMonth || '—' }}</td>
<td>{{ perf.perAmLast3Months || '—' }}</td>
<td>{{ perf.perAmLast6Months || '—' }}</td>
<td>{{ perf.perAmLast12Months || '—' }}</td>
<td>{{ perf.perAmLast24Months || '—' }}</td>
</tr>
<tr>
<td>还款异常次数</td>
<td>{{ perf.perExLastMonth ?? '—' }}</td>
<td>{{ perf.perExLast3Months ?? '—' }}</td>
<td>{{ perf.perExLast6Months ?? '—' }}</td>
<td>{{ perf.perExLast12Months ?? '—' }}</td>
<td>{{ perf.perExLast24Months ?? '—' }}</td>
</tr>
</tbody>
</table>
<div class="grid-3" style="margin-top:12px">
<div class="mini-card">
<div class="mini-label">正常还款占比</div>
<div class="mini-value">{{ perf.normalRatio || '—' }}</div>
</div>
<div class="mini-card">
<div class="mini-label">信用贷款时长</div>
<div class="mini-value">{{ perf.creditLoDuration ? `${perf.creditLoDuration}` : '—' }}</div>
</div>
<div class="mini-card">
<div class="mini-label">已结清订单数</div>
<div class="mini-value">{{ perf.settledOrderCount ?? '—' }}</div>
</div>
<div class="mini-card">
<div class="mini-label">距最近履约</div>
<div class="mini-value">{{ perf.daysPerformance || '—' }}</div>
</div>
</div>
<!-- 逾期记录 -->
<div class="sub-section-label">逾期记录</div>
<table class="info-table">
<thead>
<tr>
<th>指标</th>
<th>近6月</th>
<th>近12月</th>
<th>近24月</th>
</tr>
</thead>
<tbody>
<tr>
<td>M0+ 笔数</td>
<td>{{ od.m0PLast6Months ?? '—' }}</td>
<td>{{ od.m0PlusCoLast12Month ?? '—' }}</td>
<td>{{ od.m0PlusCoLast24Month ?? '—' }}</td>
</tr>
<tr>
<td>M1+ 笔数</td>
<td>{{ od.m1PLast6Months ?? '—' }}</td>
<td>{{ od.m1PlusCoLast12Month ?? '—' }}</td>
<td>{{ od.m1PlusCoLast24Month ?? '—' }}</td>
</tr>
<tr>
<td>累计金额</td>
<td>{{ od.totalAmLast6Month || '—' }}</td>
<td>{{ od.totalAmLast12Month || '—' }}</td>
<td>{{ od.totalAmLast24Month || '—' }}</td>
</tr>
</tbody>
</table>
<!-- 洞察履约 - 当前授信额度 -->
<div class="sub-title">洞察履约</div>
<div class="sub-section-label">当前授信额度 / 放款时间间隔</div>
<table class="info-table">
<tbody>
<tr>
<th>授信总额度所有机构</th>
<td>{{ cld.curAllLpAcct || '—' }}</td>
<th>最高授信额度所有机构</th>
<td>{{ cld.maxAllLpAcct || '—' }}</td>
</tr>
<tr>
<th>放款时间间隔所有机构</th>
<td>{{ cld.intvlAll || '—' }} </td>
<th>放款时间间隔非循环贷</th>
<td>{{ cld.intvlAllNl || '—' }} </td>
</tr>
</tbody>
</table>
<div class="sub-section-label">授信放款情况</div>
<table class="info-table">
<thead>
<tr>
<th>指标</th>
<th>近90天</th>
<th>近180天</th>
<th>近365天</th>
</tr>
</thead>
<tbody>
<tr>
<td>授信机构数所有</td>
<td>{{ cld.crAllNum90 || '—' }}</td>
<td>{{ cld.crAllNum180 || '—' }}</td>
<td>{{ cld.crAllNum365 || '—' }}</td>
</tr>
<tr>
<td>放款机构数所有</td>
<td>{{ cld.loOr90Days || '—' }}</td>
<td>{{ cld.loOr180Days || '—' }}</td>
<td>{{ cld.loOr365Days || '—' }}</td>
</tr>
<tr>
<td>放款金额所有</td>
<td>{{ cld.loOrgAm90Days || '—' }}</td>
<td>{{ cld.loOrgAm180Days || '—' }}</td>
<td>{{ cld.loOrgAm365Days || '—' }}</td>
</tr>
</tbody>
</table>
<!-- 结清借据 -->
<div class="sub-section-label">结清借据情况</div>
<table class="info-table">
<thead>
<tr>
<th>指标</th>
<th>近90天</th>
<th>近180天</th>
<th>近365天</th>
</tr>
</thead>
<tbody>
<tr>
<td>结清借据数所有机构</td>
<td>{{ rld.rpyAll90dEndNum || '—' }}</td>
<td>{{ rld.rpyAll180dEndNum || '—' }}</td>
<td>{{ rld.rpyAll365dEndNum || '—' }}</td>
</tr>
<tr>
<td>结清金额所有机构</td>
<td>{{ rld.rpyAll90dEndAmt || '—' }}</td>
<td>{{ rld.rpyAll180dEndAmt || '—' }}</td>
<td>{{ rld.rpyAll365dEndAmt || '—' }}</td>
</tr>
<tr>
<td>累计还款金额所有机构</td>
<td>{{ rld.rpyAll90d || '—' }}</td>
<td>{{ rld.rpyAll180d || '—' }}</td>
<td>{{ rld.rpyAll365d || '—' }}</td>
</tr>
</tbody>
</table>
<div v-if="!hasData" class="empty-hint">暂无履约放款数据</div>
</div>
</template>
<script setup>
const props = defineProps({
data: { type: Object, default: () => ({}) },
});
const hnRiskLevel = computed(() => props.data.hnRiskLevel || '');
const ls = computed(() => props.data.lendingStatistics || {});
const perf = computed(() => props.data.performanceStatistics || {});
const od = computed(() => props.data.overdueRecord || {});
const cld = computed(() => props.data.creditLoanDetails || {});
const rld = computed(() => props.data.repayLoanDetails || {});
const hasData = computed(() => Object.keys(props.data).length > 0);
const riskLevelCls = (lv) => {
if (lv === '1' || lv === 1) return 'tag-danger';
if (lv === '2' || lv === 2) return 'tag-warning';
return 'tag-success';
};
const riskLevelText = (lv) => {
if (lv === '1' || lv === 1) return '高风险';
if (lv === '2' || lv === 2) return '中风险';
return '低风险';
};
</script>
<style scoped lang="scss">
@import '../shared.scss';
.grid-3 {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 12px;
}
.grid-4 {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin-bottom: 12px;
}
.mini-card {
background: #f9fafb;
border-radius: 6px;
padding: 10px;
text-align: center;
}
.mini-label {
font-size: 12px;
color: #6b7280;
margin-bottom: 4px;
}
.mini-value {
font-size: 16px;
font-weight: 700;
}
.sub-section-label {
font-size: 13px;
font-weight: 600;
color: #6b7280;
margin: 10px 0 8px;
}
@media (max-width: 768px) {
.grid-3, .grid-4 { grid-template-columns: repeat(2, 1fr); }
}
</style>

804
src/ui/DWBG5SAM/index.vue Normal file
View File

@@ -0,0 +1,804 @@
<template>
<div class="tyzm-report">
<div class="report-header">
<h1 class="report-title">天远指谜报告</h1>
</div>
<!-- 基础信息 -->
<div class="card">
<div class="card-title">个人基础信息</div>
<table class="table">
<tbody>
<tr>
<th>姓名</th>
<td>{{ userInfo.name || '—' }}</td>
<th>性别</th>
<td>{{ userInfo.sex || '—' }}</td>
</tr>
<tr>
<th>年龄</th>
<td>{{ userInfo.age != null && userInfo.age !== '' ? userInfo.age : '—' }}</td>
<th>身份证</th>
<td>{{ userInfo.idCard || '—' }}</td>
</tr>
<tr>
<th>手机号</th>
<td>{{ userInfo.phone || '—' }}</td>
<th>归属地</th>
<td>{{ userInfo.phonePlace || '—' }}</td>
</tr>
<tr v-if="userInfo.location">
<th>户籍</th>
<td colspan="3">{{ userInfo.location }}</td>
</tr>
<tr>
<th>三要素验证</th>
<td colspan="3">
<span v-if="threeElText.cls" class="tag" :class="threeElText.cls">{{ threeElText.text }}</span>
<span v-else>{{ threeElText.text }}</span>
</td>
</tr>
<tr>
<th>在网状态</th>
<td>{{ phoneStatusText }}</td>
<th>在网时长</th>
<td>{{ phoneOnlineText }}</td>
</tr>
</tbody>
</table>
</div>
<!-- 信用等级 -->
<div class="card">
<div class="card-title">信用等级评估</div>
<div class="grid-4">
<div class="metric">
<div class="metric-label">信用评分</div>
<div class="metric-value accent">{{ creditLevel.creditScore || '—' }}</div>
</div>
<div class="metric">
<div class="metric-label">信用等级</div>
<div>
<span class="tag" :class="['D', 'E', 'F'].includes((creditLevel.level || '').toUpperCase()) ? 'tag-danger' : 'tag-success'">
{{ creditLevelDisplay }}
</span>
</div>
</div>
<div class="metric">
<div class="metric-label">逾期率</div>
<div class="metric-value accent">{{ creditLevel.overdueRate || '—' }}</div>
</div>
<div class="metric">
<div class="metric-label">评分/验证风险</div>
<div class="flex flex-wrap gap-1">
<span class="tag" :class="overallCreditRisk.cls">{{ overallCreditRisk.text }}</span>
<span class="tag" :class="overallOverdueRisk.cls">{{ overallOverdueRisk.text }}</span>
</div>
</div>
</div>
</div>
<!-- 用户风险名单画像 -->
<div class="card card--flush-top">
<div class="card-header-red">用户风险名单画像</div>
<div class="grid-2">
<div class="sub-block">
<div class="card-title row-title">
<span class="idx-badge">01</span>
用户风险画像
<span class="tag" :class="portraitRisk.cls">{{ portraitRisk.text }}</span>
</div>
<p class="hint">该用户命中{{ portraitHitCount }} </p>
<div class="risk-portrait-container">
<div class="risk-person" aria-hidden="true" />
<template v-for="t in ANTI_FRAUD_TAGS" :key="t.code">
<div
class="risk-tag"
:class="[t.tagClass, { hit: antiFraudSet.has(t.code) }]"
>
{{ t.label }}
</div>
<div
class="risk-tag line"
:class="[t.line, { hit: antiFraudSet.has(t.code) }]"
/>
</template>
</div>
</div>
<div class="sub-block">
<div class="card-title row-title">
<span class="idx-badge">02</span>
风险名单命中
<span class="tag" :class="listHitRisk.cls">{{ listHitRisk.text }}</span>
</div>
<p class="hint">该用户命中{{ listHitCount }} </p>
<div class="risk-list-container">
<div class="risk-center">风险名单命中</div>
<div
v-for="item in RISK_LIST_ITEMS"
:key="item.key"
class="risk-list-tag"
:class="[item.tagClass, { hit: isHit(riskListHitObj[item.key]) }]"
>
{{ item.label }}
</div>
</div>
</div>
</div>
</div>
<!-- 公安不良 -->
<div class="card card--flush-top">
<div class="card-header-red">公安不良</div>
<div class="card-title row-title">
<span class="idx-badge">01</span>
公安不良命中
<span class="tag" :class="policeRisk.cls">{{ policeRisk.text }}</span>
</div>
<div class="police-bad-container">
<div
v-for="p in POLICE_ITEMS"
:key="p.key"
class="police-card"
:class="{ unhit: !isHit(publicSecurity[p.key]) }"
>
<div class="police-card-header">
<div class="police-icon" :class="{ unhit: !isHit(publicSecurity[p.key]) }">{{ p.icon }}</div>
<div>
<div class="police-title">{{ p.title }}</div>
<div class="police-status">{{ isHit(publicSecurity[p.key]) ? '命中' : '未命中' }}</div>
</div>
</div>
<div class="police-note">{{ p.note }}</div>
</div>
</div>
</div>
<!-- 逾期风险详情 -->
<div class="card">
<OverdueRiskSection :data="overdueRisk" />
</div>
<!-- 借贷评估产品 -->
<div class="card">
<LoanEvaluationSection :data="loanEvaluation" />
</div>
<!-- 履约放款产品 -->
<div class="card">
<PerLoanDimentSection :data="perLoanDiment" />
</div>
<!-- 租赁行为 -->
<div class="card">
<LeasingBehaviorSection :data="leasingBehavior" />
</div>
<!-- 司法案件 -->
<div class="card">
<JudiciaryCaseSection :data="judiciaryCase" />
</div>
<!-- 审核结论 -->
<div class="card conclusion">
<div class="card-title">风控审核结论</div>
<div class="conclusion-content">
<p v-for="(line, i) in conclusionLines" :key="i">
<template v-if="line.ok === true">&#x2705;</template>
<template v-else-if="line.ok === false">&#x274C;</template>
<template v-else>&#x1F3AF;</template>
<span v-if="line.strong"><strong class="accent-text">{{ line.text }}</strong></span>
<span v-else>{{ line.text }}</span>
</p>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import {
LEVEL_LABEL,
ANTI_FRAUD_TAGS,
RISK_LIST_ITEMS,
POLICE_ITEMS,
isHit,
riskLevelLabel,
} from './riskReportHelper';
import OverdueRiskSection from './components/OverdueRiskSection.vue';
import LoanEvaluationSection from './components/LoanEvaluationSection.vue';
import PerLoanDimentSection from './components/PerLoanDimentSection.vue';
import LeasingBehaviorSection from './components/LeasingBehaviorSection.vue';
import JudiciaryCaseSection from './components/JudiciaryCaseSection.vue';
const props = defineProps({
data: {
type: Object,
default: () => ({}),
},
apiId: {
type: String,
default: 'TEST_API_001',
},
index: {
type: Number,
default: 0,
},
notifyRiskStatus: {
type: Function,
default: () => {},
},
});
// 根数据处理(兼容嵌套结构)
const root = computed(() => {
const d = props.data;
if (!d || typeof d !== 'object') return {};
const inner = d.data;
const isReportBody = (o) =>
o &&
typeof o === 'object' &&
('creditLevel' in o || 'userInfo' in o || 'judiciaryCase' in o);
if (inner && typeof inner === 'object' && isReportBody(inner) && !isReportBody(d)) {
return inner;
}
return d;
});
// 各模块数据提取
const userInfo = computed(() => root.value.userInfo || {});
const creditLevel = computed(() => root.value.creditLevel || {});
const userRiskPortrait = computed(() => root.value.userRiskPortrait || {});
const publicSecurity = computed(() => root.value.publicSecurity || {});
const overdueRisk = computed(() => root.value.overdueRisk || {});
const judiciaryCase = computed(() => root.value.judiciaryCase || {});
const overdueProduct = computed(() => overdueRisk.value.overdueRiskProduct || {});
const probeOverdue = computed(() => overdueRisk.value.probeOverdueRecord || {});
const caseOverview = computed(() => judiciaryCase.value.caseOverviewInfo || {});
const loanEvaluation = computed(() => root.value.loanEvaluation || {});
const perLoanDiment = computed(() => root.value.perLoanDiment || {});
const leasingBehavior = computed(() => root.value.leasingBehavior || {});
// 三要素验证文本
const threeElText = computed(() => {
const s = String(userInfo.value.statusThreeEl ?? '');
if (s === '0') return { text: '一致', cls: 'tag-success' };
if (s === '1') return { text: '不一致', cls: 'tag-danger' };
if (s === '3') return { text: '异常', cls: 'tag-warning' };
return { text: '\u2014', cls: '' };
});
// 手机号状态文本
const phoneStatusText = computed(() => {
const m = {
'1': '正常在网',
'2': '不在网(空号)',
'3': '无短信能力',
'4': '欠费',
'5': '长时间关机',
'6': '关机',
'7': '通话中',
'-1': '查询失败',
};
const k = String(userInfo.value.phoneStatus ?? '');
return m[k] || (k ? `状态码 ${k}` : '\u2014');
});
// 手机号在网时长
const phoneOnlineText = computed(() => {
const m = {
'0': '[0,3) 月',
'3': '[3,6) 月',
'6': '[6,12) 月',
'12': '[12,24) 月',
'24': '24 月及以上',
'99': '已离网/新入网/异常',
'-1': '查无记录',
};
const k = String(userInfo.value.phoneOnlie ?? userInfo.value.phoneOnline ?? '');
return m[k] || (k ? `${k}(月区间编码)` : '\u2014');
});
// 信用等级展示
const creditLevelDisplay = computed(() => {
const lv = (creditLevel.value.level || '').toString().toUpperCase().charAt(0);
const sub = LEVEL_LABEL[lv] || '';
return sub ? `${lv}${sub}` : lv || '\u2014';
});
// 各类风险等级
const overallCreditRisk = computed(() => riskLevelLabel(creditLevel.value.creditScoreRisk));
const overallOverdueRisk = computed(() => riskLevelLabel(creditLevel.value.overdueRisk));
const portraitRisk = computed(() => riskLevelLabel(userRiskPortrait.value.anRiskLevel));
const listHitRisk = computed(() => riskLevelLabel(userRiskPortrait.value.riRiskLevel));
const policeRisk = computed(() => riskLevelLabel(publicSecurity.value.riskLevel));
// 反欺诈命中集合
const antiFraudSet = computed(() => {
const arr = userRiskPortrait.value.antiFraud;
if (!Array.isArray(arr)) return new Set();
return new Set(arr.map(String));
});
// 反欺诈命中数量
const portraitHitCount = computed(() => ANTI_FRAUD_TAGS.filter((t) => antiFraudSet.value.has(t.code)).length);
// 风险名单命中对象
const riskListHitObj = computed(() => userRiskPortrait.value.riskListHit || {});
// 风险名单命中数量
const listHitCount = computed(() => RISK_LIST_ITEMS.filter((item) => isHit(riskListHitObj.value[item.key])).length);
// 未结清逾期文本
const unsetDueText = computed(() => overdueProduct.value.hasUnsetDue || '\u2014');
// 审核结论
const conclusionLines = computed(() => {
const lines = [];
const te = threeElText.value;
lines.push({
ok: te.cls === 'tag-success',
text: `身份验证:三要素${te.text === '\u2014' ? '未返回' : te.text}${userInfo.value.phoneStatus === '1' ? ',手机号正常在网' : ''}`,
});
const lv = (creditLevel.value.level || '').toString().toUpperCase();
const badLevel = ['D', 'E', 'F'].includes(lv);
lines.push({
ok: !badLevel,
text: `信用等级:${creditLevelDisplay.value},逾期率 ${creditLevel.value.overdueRate || '\u2014'}`,
});
const bg = Number(caseOverview.value.beigaoTotalCasesCounts) || 0;
const ex = Number(caseOverview.value.executionCaseCounts) || 0;
lines.push({
ok: bg === 0 && ex === 0,
text: `司法风险:被告案件 ${bg} 件,执行案件 ${ex}`,
});
const overdueBad = overdueRisk.value.overRiskLevel === '1' || unsetDueText.value === '逾期';
lines.push({
ok: !overdueBad,
text: `逾期风险:当前状态 ${unsetDueText.value},最近逾期 ${probeOverdue.value.latestOverdueTime || '\u2014'}`,
});
const hits = [];
if (portraitHitCount.value) hits.push('风险画像命中');
if (listHitCount.value) hits.push('风险名单命中');
lines.push({
ok: portraitHitCount.value === 0 && listHitCount.value === 0,
text: `名单与画像:${hits.length ? hits.join('、') : '无命中'}(画像 ${portraitHitCount.value} 项,名单 ${listHitCount.value} 项)`,
});
let suggest = '建议结合业务规则综合判断';
if (
creditLevel.value.creditScoreRisk === '1' ||
userRiskPortrait.value.riRiskLevel === '1' ||
publicSecurity.value.riskLevel === '1'
) {
suggest = '高风险客户,建议谨慎授信或拒绝';
} else if (creditLevel.value.creditScoreRisk === '2' || overdueRisk.value.overRiskLevel === '1') {
suggest = '存在明显风险信号,建议加强审核';
} else {
suggest = '整体风险可控,可按常规流程审核';
}
lines.push({ ok: null, text: `审核建议:${suggest}`, strong: true });
return lines;
});
// 风险分数计算(基于信用等级 A-F 五档)
const LEVEL_SCORE_MAP = { A: 95, B: 80, C: 65, D: 45, E: 25, F: 10 };
const riskScore = computed(() => {
const lv = (creditLevel.value.level || '').toString().toUpperCase().charAt(0);
const base = LEVEL_SCORE_MAP[lv];
if (base != null) return base;
// 无信用等级时,根据综合风险因子兜底
let s = 70;
const penalize = (cond, amount) => { if (cond) s -= amount; };
penalize(creditLevel.value.creditScoreRisk === '1', 20);
penalize(creditLevel.value.overdueRisk === '1', 12);
penalize(userRiskPortrait.value.riRiskLevel === '1', 14);
penalize(publicSecurity.value.riskLevel === '1', 16);
penalize(overdueRisk.value.overRiskLevel === '1', 12);
return Math.max(8, Math.min(100, Math.round(s)));
});
// 透传风险分数给 BaseReport
watch(riskScore, (v) => {
if (props.apiId && props.notifyRiskStatus) {
props.notifyRiskStatus(props.apiId, props.index, v);
}
}, { immediate: false });
onMounted(() => {
if (props.apiId && props.notifyRiskStatus) {
props.notifyRiskStatus(props.apiId, props.index, riskScore.value);
}
});
// 暴露风险分数
defineExpose({ riskScore });
</script>
<style scoped lang="scss">
.tyzm-report {
--tyzm-bg: #f5f7fa;
--tyzm-card: #fff;
--tyzm-text: #333;
--tyzm-muted: #6b7280;
--tyzm-border: #e5e7eb;
--tyzm-danger: #f43f5e;
--tyzm-warn: #d89614;
--tyzm-success: #008000;
--tyzm-head: #1f2937;
font-family: 'Microsoft YaHei', system-ui, sans-serif;
color: var(--tyzm-text);
max-width: 1200px;
margin: 0 auto;
padding: 16px;
background: var(--tyzm-bg);
}
.report-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 10px;
border-bottom: 1px solid #e4e7ed;
}
.report-title {
font-size: 1.125rem;
font-weight: 700;
color: var(--tyzm-head);
margin: 0;
}
.card {
background: var(--tyzm-card);
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.card--flush-top {
padding-top: 0;
}
.card-header-red {
background: linear-gradient(90deg, #f43f5e, #fb923c);
color: #fff;
padding: 10px 16px;
border-radius: 12px 12px 0 0;
margin: 0 -16px 16px;
font-size: 15px;
font-weight: 700;
}
.card-title {
font-size: 15px;
font-weight: 700;
margin-bottom: 12px;
color: var(--tyzm-head);
display: flex;
align-items: center;
gap: 6px;
}
.row-title {
flex-wrap: wrap;
}
.idx-badge {
background: var(--tyzm-danger);
color: #fff;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 700;
}
.hint {
font-size: 13px;
color: var(--tyzm-muted);
margin-bottom: 10px;
margin-top: 0;
}
.grid-4 {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.flex {
display: flex;
}
.flex-wrap {
flex-wrap: wrap;
}
.gap-1 {
gap: 4px;
}
@media (max-width: 768px) {
.grid-4 {
grid-template-columns: repeat(2, 1fr);
}
.grid-2 {
grid-template-columns: 1fr;
}
}
.metric {
padding: 8px;
}
.metric-label {
font-size: 13px;
color: var(--tyzm-muted);
margin-bottom: 4px;
}
.metric-value {
font-size: 22px;
font-weight: 700;
}
.metric-value.accent {
color: var(--tyzm-danger);
}
.tag {
display: inline-block;
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: 700;
}
.tag-success {
background: #e6f7e6;
color: var(--tyzm-success);
}
.tag-warning {
background: #fff7e6;
color: var(--tyzm-warn);
}
.tag-danger {
background: #ffe6e6;
color: #d32f2f;
}
.table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
th, td {
padding: 8px 10px;
border: 1px solid var(--tyzm-border);
text-align: left;
}
th {
background: #f9fafb;
width: 22%;
}
}
.conclusion {
background: #fff8f8;
border-left: 4px solid var(--tyzm-danger);
}
.conclusion-content {
line-height: 1.75;
font-size: 14px;
p {
margin-bottom: 6px;
margin-top: 0;
}
}
.accent-text {
color: var(--tyzm-danger);
}
/* 风险画像 */
.risk-portrait-container {
position: relative;
min-height: 260px;
display: flex;
align-items: center;
justify-content: center;
padding: 16px 0;
}
.risk-person {
width: 80px;
height: 160px;
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 200'%3E%3Crect width='100' height='200' fill='white'/%3E%3Ccircle cx='50' cy='30' r='20' fill='%232563eb'/%3E%3Crect x='35' y='55' width='30' height='80' rx='5' fill='%232563eb'/%3E%3Crect x='30' y='60' width='15' height='60' rx='3' fill='%232563eb'/%3E%3Crect x='55' y='60' width='15' height='60' rx='3' fill='%232563eb'/%3E%3Crect x='38' y='135' width='10' height='50' rx='3' fill='%232563eb'/%3E%3Crect x='52' y='135' width='10' height='50' rx='3' fill='%232563eb'/%3E%3C/svg%3E")
center / contain no-repeat;
}
.risk-tag {
position: absolute;
background: #e2e8f0;
color: #475569;
padding: 6px 10px;
border-radius: 4px;
font-size: 11px;
white-space: nowrap;
z-index: 2;
}
.risk-tag.hit {
background: var(--tyzm-danger);
color: #fff;
}
.risk-tag.line {
z-index: 1;
padding: 0;
width: 56px;
height: 2px;
min-height: 0;
background: #cbd5e1;
}
.risk-tag.line.hit {
background: var(--tyzm-danger);
}
.tag-1 { top: 12px; left: 4px; }
.tag-2 { top: 72px; left: 4px; }
.tag-3 { top: 132px; left: 4px; }
.tag-4 { top: 192px; left: 4px; }
.tag-5 { top: 12px; right: 4px; }
.tag-6 { top: 72px; right: 4px; }
.tag-7 { top: 132px; right: 4px; }
.tag-8 { top: 192px; right: 4px; }
.line-1 { top: 28px; left: 88px; }
.line-2 { top: 88px; left: 88px; }
.line-3 { top: 148px; left: 88px; }
.line-4 { top: 208px; left: 88px; }
.line-5 { top: 28px; right: 88px; }
.line-6 { top: 88px; right: 88px; }
.line-7 { top: 148px; right: 88px; }
.line-8 { top: 208px; right: 88px; }
/* 风险名单 */
.risk-list-container {
position: relative;
min-height: 300px;
display: flex;
align-items: center;
justify-content: center;
padding: 16px 0;
}
.risk-center {
width: 112px;
height: 112px;
border-radius: 50%;
background: radial-gradient(circle, #e0f2fe, #bae6fd);
display: flex;
align-items: center;
justify-content: center;
color: #0369a1;
font-weight: 700;
font-size: 13px;
text-align: center;
line-height: 1.3;
padding: 8px;
z-index: 1;
}
.risk-list-tag {
position: absolute;
background: #e2e8f0;
color: #475569;
padding: 6px 10px;
border-radius: 4px;
font-size: 11px;
z-index: 2;
max-width: 38%;
text-align: center;
line-height: 1.25;
white-space: nowrap;
}
.risk-list-tag.hit {
background: var(--tyzm-danger);
color: #fff;
}
.list-tag-1 { top: 0; left: 50%; transform: translateX(-50%); }
.list-tag-2 { top: 44px; right: 4px; }
.list-tag-3 { top: 110px; right: 4px; }
.list-tag-4 { bottom: 0; left: 50%; transform: translateX(-50%); }
.list-tag-5 { bottom: 44px; left: 4px; }
.list-tag-6 { top: 178px; left: 4px; }
.list-tag-7 { top: 44px; left: 4px; }
.list-tag-extra { bottom: 110px; right: 4px; }
/* 公安卡片 */
.police-bad-container {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 12px;
}
@media (max-width: 1024px) {
.police-bad-container {
grid-template-columns: repeat(2, 1fr);
}
}
.police-card {
background: #fff;
border-radius: 8px;
padding: 12px;
border-left: 3px solid var(--tyzm-danger);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
}
.police-card.unhit {
border-left-color: #cbd5e1;
}
.police-card-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
.police-icon {
width: 22px;
height: 22px;
background: var(--tyzm-danger);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
flex-shrink: 0;
}
.police-icon.unhit {
background: #cbd5e1;
}
.police-title {
font-weight: 700;
font-size: 13px;
}
.police-status {
font-size: 12px;
color: #64748b;
}
.police-note {
font-size: 11px;
color: #94a3b8;
margin-top: 6px;
line-height: 1.4;
}
</style>

View File

@@ -0,0 +1,158 @@
/**
* 风控报告工具函数
*/
// 信用等级映射
export const LEVEL_LABEL = {
A: '极好',
B: '优质',
C: '良好',
D: '一般',
E: '较差',
F: '极差',
};
// 反欺诈标签配置
export const ANTI_FRAUD_TAGS = [
{ code: '21001', label: '疑似恶意借贷', side: 'left', line: 'line-1', tagClass: 'tag-1' },
{ code: '11002', label: '疑似网络投机', side: 'left', line: 'line-2', tagClass: 'tag-2' },
{ code: '11003', label: '疑似营销欺诈', side: 'left', line: 'line-3', tagClass: 'tag-3' },
{ code: '11005', label: '疑似恶意套现', side: 'left', line: 'line-4', tagClass: 'tag-4' },
{ code: '21002', label: '疑似职业撸口子', side: 'right', line: 'line-5', tagClass: 'tag-5' },
{ code: '11001', label: '疑似涉黑涉赌', side: 'right', line: 'line-6', tagClass: 'tag-6' },
{ code: '11004', label: '疑似黑中介包装', side: 'right', line: 'line-7', tagClass: 'tag-7' },
{ code: '12002', label: '疑似黑产账号', side: 'right', line: 'line-8', tagClass: 'tag-8' },
];
// 风险名单配置
export const RISK_LIST_ITEMS = [
{ key: 'bankOverdueRecord', label: '银行逾期记录', tagClass: 'list-tag-1' },
{ key: 'overdueRecords', label: '百行逾期记录', tagClass: 'list-tag-2' },
{ key: 'disappearance', label: '疑似恶意失联', tagClass: 'list-tag-3' },
{ key: 'creditAndHighConstraint', label: '失信限高被执行人', tagClass: 'list-tag-4' },
{ key: 'groupFraudList', label: '团伙欺诈名单', tagClass: 'list-tag-5' },
{ key: 'judicialRecord', label: '司法涉诉记录', tagClass: 'list-tag-6' },
{ key: 'creditOverdueRecord', label: '信贷逾期记录', tagClass: 'list-tag-7' },
{ key: 'vehicleLeaseViolation', label: '车辆租赁违约', tagClass: 'list-tag-extra' },
];
// 公安不良配置
export const POLICE_ITEMS = [
{ key: 'front', title: '前科', icon: '🔒', note: '注:在逃、盗窃、诈骗、抢劫、故意伤害、强奸、在刑或前科等' },
{ key: 'economyFront', title: '经济类前科', icon: '💳', note: '注:破坏金融秩序、非法吸存、连发贷款、金融诈骗、在刑或前科等' },
{ key: 'disrupSocial', title: '妨害社会管理秩序', icon: '🌐', note: '注:扰乱社会公共秩序、妨害司法、涉毒、涉黄、在刑或前科等' },
{ key: 'trafficRelated', title: '涉交通案件', icon: '🚗', note: '注:危险驾驶、交通肇事等' },
{ key: 'ikey', title: '重点', icon: '🔍', note: '注:危害国家、公共安全,涉恐、涉爆、涉稳、涉黑、涉及境外等' },
];
/**
* 判断是否命中1/true/'1' 为命中)
* @param {any} v
* @returns {boolean}
*/
export function isHit(v) {
return v === 1 || v === '1' || v === true;
}
/**
* 风险等级标签转换
* @param {string|number} level
* @returns { { text: string, cls: string } }
*/
export function riskLevelLabel(level) {
if (level === '1' || level === 1) return { text: '高风险', cls: 'tag-danger' };
if (level === '2' || level === 2) return { text: '中风险', cls: 'tag-warning' };
return { text: '低风险', cls: 'tag-success' };
}
/**
* 格式化单元格文本空值转—对象转JSON
* @param {any} v
* @returns {string}
*/
export function cellText(v) {
if (v === null || v === undefined || v === '') return '—';
if (typeof v === 'object') {
try {
const s = JSON.stringify(v);
return s.length > 400 ? `${s.slice(0, 400)}` : s;
} catch {
return String(v);
}
}
return String(v);
}
/**
* 深度展平对象为路径-值列表
* @param {object} obj
* @param {string} prefix
* @returns {Array<{ key: string, value: string }>}
*/
export function deepFlattenRows(obj, prefix = '') {
const rows = [];
if (obj === null || obj === undefined) {
if (prefix) rows.push({ key: prefix, value: '—' });
return rows;
}
if (typeof obj !== 'object') {
rows.push({ key: prefix || '值', value: cellText(obj) });
return rows;
}
if (Array.isArray(obj)) {
rows.push({ key: prefix || '[]', value: cellText(obj) });
return rows;
}
const keys = Object.keys(obj).sort();
if (keys.length === 0) {
rows.push({ key: prefix || '(空对象)', value: '{}' });
return rows;
}
for (const k of keys) {
const v = obj[k];
const path = prefix ? `${prefix}.${k}` : k;
if (v !== null && typeof v === 'object' && !Array.isArray(v)) {
rows.push(...deepFlattenRows(v, path));
} else {
rows.push({ key: path, value: cellText(v) });
}
}
return rows;
}
/**
* 提取顶级分段
* @param {object} obj
* @returns {Array<{ heading: string, rows: Array }>}
*/
export function topLevelSections(obj) {
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return [];
return Object.keys(obj)
.sort()
.map((heading) => {
const val = obj[heading];
if (val !== null && typeof val === 'object' && !Array.isArray(val)) {
return { heading, rows: deepFlattenRows(val) };
}
return { heading, rows: [{ key: heading, value: cellText(val) }] };
})
.filter((s) => s.rows.length);
}
/**
* 模拟风险通知钩子(简化版)
* @param {object} props
* @param {import('vue').ComputedRef<number>} riskScore
*/
export function useRiskNotifier(props, riskScore) {
// 模拟风险通知逻辑
if (riskScore.value < 60) {
console.log(`[风险通知] 接口ID:${props.apiId} 索引:${props.index} 风险分数:${riskScore.value} 高风险`);
props.notifyRiskStatus?.({
apiId: props.apiId,
index: props.index,
riskScore: riskScore.value,
riskLevel: 'high'
});
}
}

137
src/ui/DWBG5SAM/shared.scss Normal file
View File

@@ -0,0 +1,137 @@
// 共享样式变量和基础类
.tyzm-section {
margin-bottom: 16px;
}
.section-title {
font-size: 15px;
font-weight: 700;
color: #1f2937;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 6px;
}
.sub-title {
font-size: 14px;
font-weight: 600;
color: #374151;
margin: 14px 0 10px;
padding-left: 8px;
border-left: 3px solid #f43f5e;
display: flex;
align-items: center;
gap: 6px;
}
.info-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
margin-bottom: 8px;
th, td {
padding: 8px 10px;
border: 1px solid #e5e7eb;
text-align: left;
}
th {
background: #f9fafb;
width: 22%;
font-weight: 600;
color: #4b5563;
}
thead th {
background: #f3f4f6;
color: #374151;
}
}
.tag {
display: inline-block;
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: 700;
}
.tag-success {
background: #e6f7e6;
color: #008000;
}
.tag-warning {
background: #fff7e6;
color: #d89614;
}
.tag-danger {
background: #ffe6e6;
color: #d32f2f;
}
.ml-2 { margin-left: 8px; }
.empty-hint {
text-align: center;
color: #9ca3af;
padding: 20px 0;
font-size: 14px;
}
.case-list {
margin-top: 8px;
}
.case-item {
background: #fafafa;
border-radius: 8px;
padding: 12px;
margin-bottom: 10px;
border-left: 3px solid #6366f1;
&.danger {
border-left-color: #f43f5e;
}
}
.case-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
flex-wrap: wrap;
gap: 6px;
}
.case-number {
font-weight: 700;
color: #1f2937;
font-size: 14px;
}
.case-body {
font-size: 13px;
color: #4b5563;
line-height: 1.6;
p {
margin: 4px 0;
}
}
.case-label {
color: #6b7280;
margin-right: 4px;
}
.expand-btn {
background: none;
border: none;
color: #6366f1;
cursor: pointer;
font-size: 12px;
padding: 2px 6px;
&:hover { text-decoration: underline; }
}

View File

@@ -256,11 +256,129 @@ const props = defineProps({
},
})
// 从data中解构出需要的字段
// 从data中解构出需要的字段 - 优先使用新数据结构
const courtRiskInfos = computed(() => {
return props.data?.judiciaRiskInfos || []
// 检查是否存在新数据结构 (judicialLeaseReport)
if (props.data?.judicialLeaseReport) {
return transformJudicialLeaseReport(props.data.judicialLeaseReport)
}
// 兼容旧数据结构 (judiciaRiskInfos)
if (props.data?.judiciaRiskInfos && Array.isArray(props.data.judiciaRiskInfos)) {
return props.data.judiciaRiskInfos
}
return []
})
// 转换司法数据报告为新格式
const transformJudicialLeaseReport = (judicialLeaseReport) => {
const result = []
const newMultCourtInfo = judicialLeaseReport?.courtInfo?.newMultCourtInfo || {}
// 定义案件类型映射
const caseTypeMappings = [
{ key: 'criminalCases', type: '刑事案件', caseTypeLabel: '涉案公告' },
{ key: 'enforcementCases', type: '执行案件', caseTypeLabel: '执行公告' },
{ key: 'civilCases', type: '民事案件', caseTypeLabel: '涉案公告' },
{ key: 'administrativeCases', type: '行政案件', caseTypeLabel: '涉案公告' },
{ key: 'preservationCases', type: '非诉保全审查案件', caseTypeLabel: '非诉保全审查案件' },
{ key: 'bankruptcyCases', type: '强制清算与破产案件', caseTypeLabel: '强制清算与破产案件' },
]
// 转换各类案件
caseTypeMappings.forEach(mapping => {
const cases = newMultCourtInfo[mapping.key]
if (cases && Array.isArray(cases)) {
cases.forEach(caseItem => {
result.push(transformCourtCaseItem(caseItem, mapping.caseTypeLabel))
})
}
})
// 转换失信公告
const disinCases = newMultCourtInfo.disinCases
if (disinCases && Array.isArray(disinCases)) {
disinCases.forEach(disinItem => {
result.push(transformDisinCaseItem(disinItem))
})
}
// 转换限高公告
const limitCases = newMultCourtInfo.limitCases
if (limitCases && Array.isArray(limitCases)) {
limitCases.forEach(limitItem => {
result.push(transformLimitCaseItem(limitItem))
})
}
return result
}
// 转换普通案件项
const transformCourtCaseItem = (caseItem, typeLabel) => {
return {
caseNumber: caseItem.caseNumber || '',
caseReason: caseItem.nlaayTree || '',
caseStatus: caseItem.caseStatus || '',
caseType: caseItem.najlx || '',
court: caseItem.njbfy || caseItem.zxfy || '',
disposalMethod: caseItem.njafs || '',
disposalTime: caseItem.djarq || '',
executionAmount: caseItem.nsqzxbdje || caseItem.njabdje || '',
filingTime: caseItem.dlarq || '',
judgmentResult: caseItem.cgkwsPjjg || '',
litigantType: caseItem.nssdw || '',
type: typeLabel,
oldCaseNumber: caseItem.cahHx || '',
repaidAmount: caseItem.nsjdwje || '',
matchScore: null
}
}
// 转换失信公告项
const transformDisinCaseItem = (disinItem) => {
return {
caseNumber: disinItem.ah || '',
caseReason: '失信被执行人',
caseStatus: '失信中',
caseType: '失信公告',
court: disinItem.zxfy || '',
disposalMethod: '失信',
disposalTime: disinItem.fbrq || '',
executionAmount: disinItem.pjjeGj || '',
filingTime: disinItem.larq || '',
judgmentResult: disinItem.yw || '',
litigantType: '被执行人',
type: '失信公告',
oldCaseNumber: disinItem.zxyjwh || '',
repaidAmount: '',
contentSummary: `失信情形: ${disinItem.xwqx || ''}\n履行情况: ${disinItem.lxqk || ''}`,
matchScore: null
}
}
// 转换限高公告项
const transformLimitCaseItem = (limitItem) => {
return {
caseNumber: limitItem.ah || '',
caseReason: '限制高消费',
caseStatus: '限高中',
caseType: '限高公告',
court: limitItem.zxfy || '',
disposalMethod: '限高',
disposalTime: limitItem.fbrq || '',
executionAmount: '',
filingTime: limitItem.larq || '',
judgmentResult: '',
litigantType: '被执行人',
type: '限高公告',
oldCaseNumber: '',
repaidAmount: '',
matchScore: null
}
}
// 折叠面板状态
const activeCases = ref({})

View File

@@ -205,6 +205,7 @@ export function splitDWBG6A2CForTabs(reportData) {
}
// 14. 法院风险信息
// 检查旧数据结构 (judiciaRiskInfos)
if (originalData.judiciaRiskInfos && originalData.judiciaRiskInfos.length > 0) {
splitModules.push({
data: {
@@ -217,6 +218,19 @@ export function splitDWBG6A2CForTabs(reportData) {
}
});
}
// 检查新数据结构 (judicialLeaseReport)
else if (originalData.judicialLeaseReport) {
splitModules.push({
data: {
apiID: 'DWBG6A2C_CourtRiskInfo',
data: {
judicialLeaseReport: originalData.judicialLeaseReport
},
success: true,
timestamp: baseTimestamp
}
});
}
// 移除原始的DWBG6A2C数据添加拆分后的模块
const otherData = reportData.filter(item => item.data?.apiID !== 'DWBG6A2C');

View File

@@ -0,0 +1,301 @@
<template>
<div class="charts-grid">
<!-- 雷达图 -->
<div class="hy-card">
<div class="hy-card-title">风险维度评分</div>
<div class="radar-chart">
<svg width="260" height="230" viewBox="-40 -8 300 236">
<polygon
v-for="(grid, gi) in radarGrids"
:key="gi"
:points="grid"
fill="none"
stroke="#e2e8f0"
stroke-width="1"
/>
<polygon
:points="radarPoints"
fill="rgba(229, 62, 62, 0.2)"
stroke="#e53e3e"
stroke-width="2"
/>
<circle
v-for="(pt, pi) in radarPointList"
:key="pi"
:cx="pt.x"
:cy="pt.y"
r="3"
fill="#e53e3e"
/>
<text
v-for="(pt, pi) in radarPointList"
:key="`label-${pi}`"
:x="pt.labelX"
:y="pt.labelY"
font-size="10"
:text-anchor="pt.anchor"
>
{{ pt.label }}
</text>
<text
v-for="(pt, pi) in radarPointList"
:key="`score-${pi}`"
:x="pt.labelCenterX"
:y="pt.labelY + 12"
font-size="10"
text-anchor="middle"
>
{{ pt.score }}
</text>
</svg>
</div>
<div class="chart-legend">
<div class="legend-item">
<span class="legend-line red" />
<span>用户得分</span>
</div>
<div class="legend-item">
<span class="legend-line blue" />
<span>行业平均</span>
</div>
</div>
</div>
<!-- 趋势图 -->
<div class="hy-card">
<div class="hy-card-title">风险趋势分析近6个月</div>
<div class="line-chart-container">
<svg width="320" height="200" viewBox="0 0 320 200">
<line x1="40" y1="20" x2="40" y2="160" stroke="#e2e8f0" stroke-width="1" />
<line v-for="y in [60, 100, 140]" :key="y" x1="40" :y1="y" x2="300" :y2="y" stroke="#e2e8f0" stroke-width="1" />
<line x1="40" y1="180" x2="300" y2="180" stroke="#e2e8f0" stroke-width="1" />
<text x="30" y="25" font-size="10" text-anchor="end">1000</text>
<text x="30" y="65" font-size="10" text-anchor="end">750</text>
<text x="30" y="105" font-size="10" text-anchor="end">500</text>
<text x="30" y="145" font-size="10" text-anchor="end">250</text>
<text x="30" y="185" font-size="10" text-anchor="end">0</text>
<polyline :points="trendPolyline" fill="rgba(229, 62, 62, 0.15)" stroke="#e53e3e" stroke-width="2" />
<circle
v-for="(pt, ti) in trendPoints"
:key="ti"
:cx="pt.x"
:cy="pt.y"
r="4"
fill="#e53e3e"
/>
<text
v-for="(pt, ti) in trendPoints"
:key="`tv-${ti}`"
:x="pt.x"
:y="pt.y - 10"
font-size="10"
text-anchor="middle"
>
{{ pt.value }}
</text>
</svg>
<div class="chart-axis">
<span v-for="(m, mi) in trend.months" :key="mi">{{ m }}</span>
</div>
</div>
</div>
<!-- 饼图 -->
<div class="hy-card pie-card">
<div class="hy-card-title">风险因素分布</div>
<div class="pie-chart-wrap">
<div class="pie-chart-area">
<svg width="180" height="180" viewBox="0 0 180 180">
<circle
v-for="(seg, si) in pieSegments"
:key="si"
cx="90"
cy="90"
r="70"
fill="none"
:stroke="seg.color"
stroke-width="40"
:stroke-dasharray="seg.dasharray"
:stroke-dashoffset="seg.dashoffset"
/>
<text x="90" y="80" font-size="14" text-anchor="middle">风险因素</text>
<text x="90" y="100" font-size="18" font-weight="bold" text-anchor="middle">{{ totalFactors }}</text>
</svg>
</div>
<div class="pie-legend">
<div v-for="(seg, si) in pieSegments" :key="si" class="legend-row">
<span class="legend-color" :style="{ backgroundColor: seg.color }" />
<span class="legend-label">{{ seg.label }}</span>
<span class="legend-value">{{ seg.percent }}%{{ seg.count }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import {
buildRadarScores,
buildRadarPolygon,
buildRiskTrend,
buildRiskFactorDistribution,
riskFactorTotal,
trendPointY,
} from '../reportHelper';
const props = defineProps({
root: { type: Object, default: () => ({}) },
});
const radarScores = computed(() => buildRadarScores(props.root));
const radarPointList = computed(() => buildRadarPolygon(radarScores.value));
const radarPoints = computed(() => radarPointList.value.map((p) => `${p.x},${p.y}`).join(' '));
const radarGrids = computed(() => {
const cx = 110;
const cy = 110;
const radii = [90, 60, 30];
const count = radarScores.value.length || 6;
return radii.map((r) => {
const pts = Array.from({ length: count }, (_, i) => {
const angle = (Math.PI * 2 * i) / count - Math.PI / 2;
return `${cx + r * Math.cos(angle)},${cy + r * Math.sin(angle)}`;
});
return pts.join(' ');
});
});
const trend = computed(() => buildRiskTrend(props.root));
const trendPoints = computed(() => {
const xs = [60, 105, 150, 195, 240, 285];
return trend.value.values.map((value, i) => ({
x: xs[i],
y: trendPointY(value, trend.value.max),
value,
}));
});
const trendPolyline = computed(() => trendPoints.value.map((p) => `${p.x},${p.y}`).join(' '));
const pieSegments = computed(() => buildRiskFactorDistribution(props.root));
const totalFactors = computed(() => riskFactorTotal(props.root));
</script>
<style lang="scss" scoped>
.charts-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
margin-bottom: 20px;
}
.radar-chart {
height: 230px;
display: flex;
justify-content: center;
overflow: visible;
svg {
overflow: visible;
}
}
.chart-legend {
display: flex;
align-items: center;
gap: 16px;
margin-top: 16px;
font-size: 12px;
color: #718096;
}
.legend-item {
display: flex;
align-items: center;
gap: 4px;
}
.legend-line {
width: 20px;
height: 2px;
&.red { background-color: #e53e3e; }
&.blue { background-color: #3182ce; }
}
.line-chart-container {
height: 220px;
position: relative;
}
.chart-axis {
display: flex;
justify-content: space-between;
font-size: 11px;
color: #718096;
margin-top: 8px;
padding: 0 8px;
}
.pie-card {
display: flex;
flex-direction: column;
}
.pie-chart-wrap {
display: flex;
flex-direction: column;
flex: 1;
}
.pie-chart-area {
display: flex;
justify-content: center;
align-items: center;
min-height: 180px;
}
.pie-legend {
margin-top: auto;
padding-top: 16px;
border-top: 1px solid #e2e8f0;
font-size: 12px;
color: #4a5568;
}
.legend-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
&:last-child {
margin-bottom: 0;
}
}
.legend-color {
width: 10px;
height: 10px;
border-radius: 2px;
flex-shrink: 0;
}
.legend-label {
flex: 1;
color: #4a5568;
}
.legend-value {
color: #1a202c;
font-weight: 500;
white-space: nowrap;
}
@media (max-width: 900px) {
.charts-grid {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,124 @@
<template>
<div class="hy-card findings-card">
<div class="hy-card-title">核心风险发现</div>
<div
v-for="(item, i) in findings"
:key="i"
class="risk-item"
:class="item.tone === 'high' ? 'high-risk' : 'medium-risk'"
>
<div class="risk-left">
<div class="risk-icon" :class="item.tone === 'high' ? 'red' : 'orange'">👤</div>
<div>
<div class="risk-title">{{ item.title }}</div>
<div class="risk-desc">{{ item.desc }}</div>
</div>
</div>
<div class="risk-details">
<div v-for="(line, li) in item.details" :key="li">{{ line }}</div>
</div>
<div class="risk-level" :class="item.tone === 'high' ? 'high' : 'medium'">
{{ item.level }}
</div>
</div>
<div v-if="!findings.length" class="empty-tip">暂无核心风险发现</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { buildCoreRiskFindings } from '../reportHelper';
const props = defineProps({
root: { type: Object, default: () => ({}) },
});
const findings = computed(() => buildCoreRiskFindings(props.root));
</script>
<style lang="scss" scoped>
.risk-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-radius: 8px;
margin-bottom: 12px;
gap: 16px;
flex-wrap: wrap;
&.high-risk { background-color: #fff5f5; }
&.medium-risk { background-color: #fffaf0; }
}
.risk-left {
display: flex;
align-items: center;
gap: 12px;
min-width: 200px;
}
.risk-icon {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 16px;
flex-shrink: 0;
&.red { background-color: #e53e3e; }
&.orange { background-color: #ed8936; }
}
.risk-title {
font-weight: 600;
color: #1a202c;
}
.risk-desc {
font-size: 13px;
color: #4a5568;
margin-top: 4px;
}
.risk-details {
font-size: 13px;
color: #4a5568;
line-height: 1.6;
flex: 1;
}
.risk-level {
padding: 4px 12px;
border-radius: 4px;
font-size: 13px;
font-weight: 600;
flex-shrink: 0;
&.high {
background-color: #feb2b2;
color: #c53030;
}
&.medium {
background-color: #feebc8;
color: #dd6b20;
}
}
.empty-tip {
text-align: center;
color: #718096;
padding: 24px;
}
@media (max-width: 768px) {
.risk-item {
flex-direction: column;
align-items: flex-start;
}
}
</style>

View File

@@ -0,0 +1,423 @@
<template>
<div class="judicial-wrap">
<div class="top-grid">
<!-- 司法风险概览 -->
<div class="sf-card">
<div class="sf-card-title">司法风险概览</div>
<div class="stat-grid">
<div v-for="(item, i) in overviewStats" :key="i" class="stat-item">
<div class="stat-label">{{ item.label }}</div>
<div class="stat-value">{{ item.value }}{{ item.unit }}</div>
</div>
</div>
<div class="pie-block">
<div class="sf-card-subtitle">主要案件类型分布</div>
<div class="pie-section">
<div class="pie-chart">
<div class="pie-ring" :style="{ background: caseTypePie.gradient }">
<div class="pie-inner">{{ caseTypePie.total }}</div>
</div>
</div>
<div class="pie-legend">
<div
v-for="(item, i) in caseTypePie.items"
:key="i"
class="legend-item"
>
<span class="legend-dot" :style="{ backgroundColor: item.color }" />
<span>{{ item.label }} {{ item.percent }}%{{ item.count }}</span>
</div>
<div v-if="!caseTypePie.items.length" class="legend-item">
<span class="legend-dot" style="background:#cbd5e0" />
<span>暂无案件类型数据</span>
</div>
</div>
</div>
</div>
</div>
<!-- 案件详情表格 -->
<div class="sf-card">
<div class="sf-card-title">
<span>案件详情</span>
<button
v-if="tableRows.length > displayLimit"
type="button"
class="view-more"
@click="showAllCases = !showAllCases"
>
{{ showAllCases ? '收起' : '查看更多' }} &gt;
</button>
</div>
<div class="table-container">
<table v-if="visibleRows.length" class="case-table">
<thead>
<tr>
<th>案件编号</th>
<th>案件类型</th>
<th>案由</th>
<th>角色</th>
<th>案发时间</th>
<th>案件状态</th>
</tr>
</thead>
<tbody>
<tr
v-for="row in visibleRows"
:key="row.id"
:class="{ active: selectedCase?.c_ah === row.raw.c_ah }"
@click="selectCase(row.raw)"
>
<td>{{ row.caseNo }}</td>
<td>
<span class="tag" :class="row.typeTag">{{ row.typeLabel }}</span>
</td>
<td>{{ row.cause }}</td>
<td>{{ row.role }}</td>
<td>{{ row.incidentDate }}</td>
<td>{{ row.status }}</td>
</tr>
</tbody>
</table>
<div v-else class="empty-data">暂无司法案件记录</div>
</div>
</div>
</div>
<!-- 案件详细信息 -->
<div v-if="selectedCase" class="sf-card detail-card">
<div class="sf-card-title">
案件详细信息
<span class="detail-case-no">{{ selectedCase.c_ah }}</span>
</div>
<div class="detail-section">
<div v-for="(col, ci) in detailColumns" :key="ci" class="detail-col">
<div v-for="(field, fi) in col" :key="fi" class="detail-item">
<span class="detail-label">{{ field.label }}:</span>
<span class="detail-value">{{ field.value }}</span>
</div>
</div>
</div>
<div v-if="selectedCase.c_gkws_pjjg" class="judgment-block">
<div class="judgment-title">完整判决结果</div>
<div class="judgment-text">{{ selectedCase.c_gkws_pjjg }}</div>
</div>
<div class="scale-icon"></div>
</div>
</div>
</template>
<script setup>
import { computed, ref, watch } from 'vue';
import {
buildJudicialOverviewStats,
buildMainCaseTypePie,
buildCaseTableRows,
buildCaseDetailColumns,
buildAllCaseList,
} from '../reportHelper';
const props = defineProps({
data: { type: Object, default: () => ({}) },
courtRisk: { type: Object, default: () => ({}) },
});
const displayLimit = 4;
const showAllCases = ref(false);
const selectedCase = ref(null);
const overviewStats = computed(() => buildJudicialOverviewStats(props.data, props.courtRisk));
const caseTypePie = computed(() => buildMainCaseTypePie(props.data));
const tableRows = computed(() => buildCaseTableRows(props.data));
const visibleRows = computed(() =>
showAllCases.value ? tableRows.value : tableRows.value.slice(0, displayLimit),
);
const detailColumns = computed(() => buildCaseDetailColumns(selectedCase.value));
function selectCase(caseItem) {
selectedCase.value = caseItem;
}
watch(
() => props.data,
() => {
const cases = buildAllCaseList(props.data);
selectedCase.value = cases[0] || null;
showAllCases.value = false;
},
{ immediate: true, deep: true },
);
</script>
<style lang="scss" scoped>
.judicial-wrap {
margin-bottom: 20px;
}
.top-grid {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 20px;
margin-bottom: 20px;
}
.sf-card {
background: #fff;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.04);
border: 1px solid #e2e8f0;
}
.sf-card-title {
font-size: 18px;
font-weight: 600;
color: #1a202c;
margin-bottom: 20px;
display: flex;
align-items: center;
justify-content: space-between;
}
.sf-card-subtitle {
font-size: 16px;
font-weight: 600;
color: #1a202c;
margin-bottom: 16px;
}
.detail-case-no {
font-size: 14px;
color: #4a5568;
font-weight: 400;
margin-left: 8px;
}
.stat-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.stat-item {
background-color: #f7fafc;
border-radius: 8px;
padding: 16px;
text-align: center;
}
.stat-label {
font-size: 14px;
color: #4a5568;
margin-bottom: 8px;
}
.stat-value {
font-size: 28px;
font-weight: 700;
color: #3182ce;
}
.pie-section {
display: flex;
align-items: center;
gap: 24px;
}
.pie-chart {
width: 120px;
height: 120px;
flex-shrink: 0;
}
.pie-ring {
width: 100%;
height: 100%;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.pie-inner {
width: 80px;
height: 80px;
background-color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
font-weight: bold;
color: #1a202c;
}
.pie-legend {
font-size: 14px;
color: #4a5568;
}
.legend-item {
display: flex;
align-items: flex-start;
gap: 8px;
margin-bottom: 8px;
}
.legend-dot {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
margin-top: 5px;
}
.table-container {
width: 100%;
overflow-x: auto;
}
.case-table {
width: 100%;
border-collapse: collapse;
}
.case-table th,
.case-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #e2e8f0;
font-size: 14px;
}
.case-table th {
background-color: #f7fafc;
color: #4a5568;
font-weight: 500;
}
.case-table tbody tr {
cursor: pointer;
transition: background 0.15s;
&:hover { background: #f7fafc; }
&.active { background: #ebf8ff; }
}
.tag {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
white-space: nowrap;
&.criminal {
background-color: #fed7d7;
color: #c53030;
}
&.civil {
background-color: #bee3f8;
color: #2b6cb0;
}
}
.view-more {
color: #3182ce;
font-size: 14px;
background: none;
border: none;
cursor: pointer;
padding: 0;
}
.detail-card {
position: relative;
overflow: hidden;
}
.detail-section {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 20px;
}
.detail-item {
margin-bottom: 12px;
display: flex;
gap: 4px;
}
.detail-label {
width: 88px;
flex-shrink: 0;
color: #4a5568;
font-size: 14px;
}
.detail-value {
color: #1a202c;
font-size: 14px;
flex: 1;
word-break: break-all;
}
.judgment-block {
margin-top: 20px;
padding-top: 16px;
border-top: 1px dashed #e2e8f0;
position: relative;
z-index: 1;
}
.judgment-title {
font-size: 14px;
font-weight: 600;
color: #4a5568;
margin-bottom: 8px;
}
.judgment-text {
font-size: 13px;
color: #4a5568;
line-height: 1.7;
}
.scale-icon {
position: absolute;
right: 40px;
bottom: 20px;
opacity: 0.1;
font-size: 100px;
pointer-events: none;
}
.empty-data {
text-align: center;
padding: 40px 0;
color: #718096;
font-size: 14px;
}
@media (max-width: 900px) {
.top-grid {
grid-template-columns: 1fr;
}
.stat-grid {
grid-template-columns: repeat(2, 1fr);
}
.detail-section {
grid-template-columns: 1fr;
}
.pie-section {
flex-direction: column;
align-items: flex-start;
}
}
</style>

View File

@@ -0,0 +1,141 @@
<template>
<div class="grid-row">
<div class="hy-card">
<div class="hy-card-title hy-card-title--lg">
查询记录 <span class="subtitle">(近12个月)</span>
</div>
<div class="stat-grid">
<div class="stat-item">
<div class="stat-label">机构查询</div>
<div class="stat-value">{{ queryStats.orgQueries }}</div>
</div>
<div class="stat-item">
<div class="stat-label">个人查询</div>
<div class="stat-value">{{ queryStats.personalQueries }}</div>
</div>
<div class="stat-item">
<div class="stat-label">查询机构数</div>
<div class="stat-value">{{ queryStats.orgCount }}</div>
</div>
<div class="stat-item">
<div class="stat-label">最近查询时间</div>
<div class="stat-value stat-value--sm">{{ queryStats.latestQuery }}</div>
</div>
</div>
</div>
<div class="hy-card">
<div class="hy-card-title hy-card-title--lg">黑名单检测</div>
<div class="blacklist-section">
<div class="blacklist-icon">🛡</div>
<div class="blacklist-info">
<div class="title">命中记录</div>
<div class="count">{{ blacklist.hits }}</div>
<div class="status">当前状态{{ blacklist.status }}{{ blacklist.gradeText }}</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { buildQueryStats, buildBlacklistStatus } from '../reportHelper';
const props = defineProps({
root: { type: Object, default: () => ({}) },
});
const queryStats = computed(() => buildQueryStats(props.root));
const blacklist = computed(() => buildBlacklistStatus(props.root));
</script>
<style lang="scss" scoped>
.grid-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
.subtitle {
font-size: 14px;
color: #718096;
font-weight: 400;
}
.stat-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
.stat-item {
background-color: #f7fafc;
border-radius: 8px;
padding: 16px;
text-align: center;
}
.stat-label {
font-size: 14px;
color: #4a5568;
margin-bottom: 8px;
}
.stat-value {
font-size: 28px;
font-weight: 700;
color: #3182ce;
&--sm {
font-size: 16px;
}
}
.blacklist-section {
display: flex;
align-items: center;
gap: 16px;
}
.blacklist-icon {
width: 48px;
height: 48px;
border-radius: 50%;
background-color: #fff5f5;
color: #e53e3e;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
flex-shrink: 0;
}
.blacklist-info .title {
font-weight: 600;
color: #1a202c;
}
.blacklist-info .count {
font-size: 24px;
font-weight: 700;
color: #1a202c;
}
.blacklist-info .status {
font-size: 14px;
color: #4a5568;
margin-top: 4px;
}
@media (max-width: 900px) {
.grid-row {
grid-template-columns: 1fr;
}
.stat-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>

View File

@@ -0,0 +1,114 @@
<template>
<div class="header-section" :style="bannerStyle">
<div class="header-left">
<h1>海宇个人风险报告</h1>
<p>多维度大数据风险分析</p>
</div>
<div class="header-right">
<div>报告编号: {{ reportNo }}</div>
<div>生成时间: {{ displayTime }}</div>
<div>报告版本: V1.0</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import personBanner from '@/assets/images/reporthead/person-banner.png';
const props = defineProps({
reportTime: { type: String, default: '' },
});
const bannerStyle = computed(() => ({
backgroundImage: `url(${personBanner})`,
}));
const reportNo = computed(() => {
const d = new Date();
return `RPT${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, '0')}${String(d.getDate()).padStart(2, '0')}001`;
});
const displayTime = computed(() => {
if (!props.reportTime) {
return formatDateTime(new Date());
}
const parsed = new Date(props.reportTime);
if (!Number.isNaN(parsed.getTime())) {
return formatDateTime(parsed);
}
return props.reportTime;
});
function formatDateTime(date) {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
const h = String(date.getHours()).padStart(2, '0');
const min = String(date.getMinutes()).padStart(2, '0');
const s = String(date.getSeconds()).padStart(2, '0');
return `${y}-${m}-${d} ${h}:${min}:${s}`;
}
</script>
<style lang="scss" scoped>
.header-section {
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding: 28px 32px;
border-radius: 12px;
overflow: hidden;
aspect-ratio: 8 / 1;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.header-left,
.header-right {
position: relative;
z-index: 1;
}
.header-left h1 {
font-size: 32px;
font-weight: 700;
color: #1a202c;
margin: 0;
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.6);
}
.header-left p {
font-size: 16px;
color: #2d3748;
margin: 8px 0 0;
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.6);
}
.header-right {
text-align: right;
font-size: 12px;
color: #2d3748;
line-height: 1.8;
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.6);
}
@media (max-width: 768px) {
.header-section {
flex-direction: column;
align-items: flex-start;
justify-content: center;
gap: 16px;
padding: 24px 20px;
aspect-ratio: 6 / 1;
}
.header-right {
text-align: left;
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,85 @@
<template>
<div class="hy-card overview-card">
<div class="hy-card-title">风险概览</div>
<div class="risk-overview-grid">
<div v-for="(item, i) in items" :key="i" class="risk-item">
<div class="risk-icon" :class="item.iconClass">{{ item.icon }}</div>
<div class="risk-label">{{ item.label }}</div>
<div class="risk-status" :class="item.statusClass">{{ item.status }}</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { buildRiskOverviewItems } from '../reportHelper';
const props = defineProps({
root: { type: Object, default: () => ({}) },
});
const items = computed(() => buildRiskOverviewItems(props.root));
</script>
<style lang="scss" scoped>
.risk-overview-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 12px;
}
.risk-item {
background: #fff;
border-radius: 8px;
padding: 16px 12px;
text-align: center;
border: 1px solid #e2e8f0;
}
.risk-icon {
width: 40px;
height: 40px;
border-radius: 50%;
margin: 0 auto 8px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 20px;
&.orange { background-color: #ed8936; }
&.red { background-color: #c53030; }
&.user { background-color: #ed8936; }
&.purple { background-color: #9c27b0; }
&.blue { background-color: #4299e1; }
}
.risk-label {
font-size: 13px;
color: #4a5568;
margin-bottom: 6px;
}
.risk-status {
font-size: 15px;
font-weight: 600;
&.red { color: #c53030; }
&.orange { color: #ed8936; }
&.purple { color: #9c27b0; }
&.blue { color: #4299e1; }
}
@media (max-width: 900px) {
.risk-overview-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 600px) {
.risk-overview-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>

View File

@@ -0,0 +1,167 @@
<template>
<div class="hy-card rating-card">
<div class="hy-card-title">综合风险评级</div>
<div class="risk-rating">
<div class="rating-circle">
<div class="circle-bg" :style="circleStyle">
<div class="circle-inner">
<div class="rating-letter" :style="{ color: levelMeta.color }">{{ riskLevel }}</div>
<div class="rating-text" :style="{ color: levelMeta.color }">{{ levelMeta.label }}</div>
</div>
</div>
</div>
<div class="rating-details">
<div class="score" :style="{ color: levelMeta.color }">
{{ riskScore }} <span>/ 1000</span>
</div>
<div class="level">
风险水平: <strong :style="{ color: levelMeta.color }">{{ levelMeta.levelText }}</strong>
</div>
<div class="progress-bar" :style="progressStyle" />
<div class="progress-labels">
<span>0</span><span>250</span><span>500</span><span>750</span><span>1000</span>
</div>
</div>
</div>
<div class="risk-note">
<span></span>
{{ levelMeta.note }}
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { riskLevelInfo, riskScorePercent } from '../reportHelper';
const props = defineProps({
riskLevel: { type: String, default: '—' },
riskScore: { type: [String, Number], default: '—' },
});
const levelMeta = computed(() => riskLevelInfo(props.riskLevel));
const scorePercent = computed(() => riskScorePercent(props.riskScore));
const circleStyle = computed(() => ({
background: `conic-gradient(${levelMeta.value.color} 0%, ${levelMeta.value.color} ${scorePercent.value}%, #fff ${scorePercent.value}%)`,
}));
const progressStyle = computed(() => ({
'--marker-pos': `${scorePercent.value}%`,
background: `linear-gradient(to right, ${levelMeta.value.color} 0%, ${levelMeta.value.color} ${scorePercent.value}%, #f6e05e ${scorePercent.value}%, #f6e05e 50%, #48bb78 50%, #48bb78 100%)`,
}));
</script>
<style lang="scss" scoped>
.risk-rating {
display: flex;
align-items: center;
gap: 24px;
}
.rating-circle {
position: relative;
width: 140px;
height: 140px;
flex-shrink: 0;
}
.circle-bg {
width: 100%;
height: 100%;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.circle-inner {
width: 110px;
height: 110px;
background: #fff;
border-radius: 50%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.rating-letter {
font-size: 72px;
font-weight: 700;
line-height: 1;
}
.rating-text {
font-size: 18px;
font-weight: 600;
margin-top: 4px;
}
.rating-details .score {
font-size: 32px;
font-weight: 700;
span {
font-size: 16px;
color: #4a5568;
font-weight: 400;
}
}
.rating-details .level {
font-size: 14px;
color: #4a5568;
margin: 8px 0;
}
.progress-bar {
width: 200px;
max-width: 100%;
height: 8px;
border-radius: 4px;
position: relative;
&::after {
content: '';
position: absolute;
left: var(--marker-pos, 17%);
top: -4px;
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 8px solid #e53e3e;
transform: translateX(-50%);
}
}
.progress-labels {
display: flex;
justify-content: space-between;
width: 200px;
max-width: 100%;
font-size: 10px;
color: #718096;
margin-top: 4px;
}
.risk-note {
margin-top: 20px;
padding: 10px;
background-color: #fff5f5;
border-radius: 6px;
color: #c53030;
font-size: 13px;
display: flex;
align-items: center;
gap: 6px;
}
@media (max-width: 768px) {
.risk-rating {
flex-direction: column;
align-items: flex-start;
}
}
</style>

View File

@@ -0,0 +1,137 @@
<template>
<div class="hy-card">
<div class="hy-card-title hy-card-title--lg">风险事件时间线</div>
<div class="timeline-container">
<div v-if="events.length" class="timeline-line" />
<div class="timeline-items">
<div v-for="(item, i) in events" :key="i" class="timeline-item">
<div class="timeline-date" :class="item.tone">{{ item.date }}</div>
<div class="timeline-dot" :class="item.tone" />
<div class="timeline-icon" :class="item.tone">{{ iconMap[item.tone] }}</div>
<div class="timeline-title">{{ item.title }}</div>
<div class="timeline-desc" v-html="item.desc.replace(/\n/g, '<br>')" />
</div>
</div>
<div v-if="!events.length" class="empty-tip">暂无风险事件记录</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { buildTimelineEvents } from '../reportHelper';
const props = defineProps({
root: { type: Object, default: () => ({}) },
});
const iconMap = {
red: '❗',
blue: '👤',
orange: '👥',
};
const events = computed(() => buildTimelineEvents(props.root));
</script>
<style lang="scss" scoped>
.timeline-container {
position: relative;
padding-top: 8px;
}
.timeline-line {
position: absolute;
top: 38px;
left: 10%;
right: 10%;
height: 2px;
background: linear-gradient(to right, #feb2b2 0%, #90cdf4 33%, #feebc8 66%, #feb2b2 100%);
z-index: 1;
}
.timeline-items {
display: flex;
justify-content: space-between;
position: relative;
z-index: 2;
gap: 12px;
}
.timeline-item {
width: 23%;
text-align: center;
min-width: 0;
}
.timeline-dot {
width: 12px;
height: 12px;
border-radius: 50%;
margin: 0 auto 10px;
&.red { background-color: #e53e3e; }
&.blue { background-color: #3182ce; }
&.orange { background-color: #ed8936; }
}
.timeline-date {
font-size: 16px;
font-weight: 600;
margin-bottom: 10px;
&.red { color: #e53e3e; }
&.blue { color: #3182ce; }
&.orange { color: #ed8936; }
}
.timeline-icon {
width: 48px;
height: 48px;
border-radius: 50%;
margin: 0 auto 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
&.red { background-color: #fff5f5; color: #e53e3e; }
&.blue { background-color: #ebf8ff; color: #3182ce; }
&.orange { background-color: #fffaf0; color: #ed8936; }
}
.timeline-title {
font-weight: 600;
color: #1a202c;
margin-bottom: 6px;
}
.timeline-desc {
font-size: 13px;
color: #4a5568;
line-height: 1.6;
}
.empty-tip {
text-align: center;
color: #718096;
padding: 24px;
}
@media (max-width: 768px) {
.timeline-line {
display: none;
}
.timeline-items {
flex-direction: column;
}
.timeline-item {
width: 100%;
text-align: left;
padding-bottom: 16px;
border-bottom: 1px solid #e2e8f0;
}
}
</style>

View File

@@ -0,0 +1,101 @@
<template>
<div class="hy-card">
<div class="hy-card-title hy-card-title--lg">综合建议</div>
<div class="intro">基于当前风险评估结果建议</div>
<div class="suggestion-grid">
<div
v-for="(item, i) in suggestions"
:key="i"
class="suggestion-item"
:class="item.tone"
>
<div class="suggestion-header">
<div class="suggestion-icon">{{ item.icon }}</div>
<div class="suggestion-title">{{ item.title }}</div>
</div>
<div class="suggestion-desc">{{ item.desc }}</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { buildSuggestions } from '../reportHelper';
const props = defineProps({
riskLevel: { type: String, default: 'F' },
});
const suggestions = computed(() => buildSuggestions(props.riskLevel));
</script>
<style lang="scss" scoped>
.intro {
font-size: 14px;
color: #4a5568;
margin-bottom: 16px;
}
.suggestion-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
.suggestion-item {
border-radius: 8px;
padding: 16px;
&.red { background-color: #fff5f5; }
&.orange { background-color: #fffaf0; }
&.blue { background-color: #ebf8ff; }
&.green { background-color: #f0fff4; }
}
.suggestion-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.suggestion-icon {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
}
.suggestion-item.red .suggestion-icon { background-color: #fed7d7; color: #c53030; }
.suggestion-item.orange .suggestion-icon { background-color: #feebc8; color: #dd6b20; }
.suggestion-item.blue .suggestion-icon { background-color: #bee3f8; color: #2b6cb0; }
.suggestion-item.green .suggestion-icon { background-color: #c6f6d5; color: #276749; }
.suggestion-title { font-weight: 600; }
.suggestion-item.red .suggestion-title { color: #c53030; }
.suggestion-item.orange .suggestion-title { color: #dd6b20; }
.suggestion-item.blue .suggestion-title { color: #2b6cb0; }
.suggestion-item.green .suggestion-title { color: #276749; }
.suggestion-desc {
font-size: 13px;
color: #4a5568;
margin-left: 40px;
}
@media (max-width: 900px) {
.suggestion-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 600px) {
.suggestion-grid {
grid-template-columns: 1fr;
}
}
</style>

115
src/ui/DWBG9FB2/index.vue Normal file
View File

@@ -0,0 +1,115 @@
<template>
<div class="hy-report">
<div ref="reportRef" class="hy-container">
<ReportHeaderSection :report-time="reportTime" />
<div class="main-grid">
<RiskRatingSection
:risk-level="root.riskLevel"
:risk-score="root.riskScore"
/>
<RiskOverviewSection :root="root" />
</div>
<ChartsSection :root="root" />
<CoreRiskFindingsSection :root="root" />
<JudicialCaseSection :data="root.personalLawsuit" :court-risk="root.courtRisk" />
<RiskTimelineSection :root="root" />
<QueryBlacklistSection :root="root" />
<SuggestionSection :risk-level="root.riskLevel" />
<div class="footer-note">{{ REPORT_USAGE_NOTICE }}</div>
</div>
</div>
</template>
<script setup>
import { computed, ref } from 'vue';
import { parseRoot, extractReportUrl, REPORT_USAGE_NOTICE } from './reportHelper';
import { printReportAsPdf } from './reportExport';
import ReportHeaderSection from './components/ReportHeaderSection.vue';
import RiskRatingSection from './components/RiskRatingSection.vue';
import RiskOverviewSection from './components/RiskOverviewSection.vue';
import ChartsSection from './components/ChartsSection.vue';
import CoreRiskFindingsSection from './components/CoreRiskFindingsSection.vue';
import JudicialCaseSection from './components/JudicialCaseSection.vue';
import RiskTimelineSection from './components/RiskTimelineSection.vue';
import QueryBlacklistSection from './components/QueryBlacklistSection.vue';
import SuggestionSection from './components/SuggestionSection.vue';
const props = defineProps({
data: { type: Object, default: () => ({}) },
params: { type: Object, default: () => ({}) },
apiId: { type: String, default: 'DWBG9FB2' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => {} },
reportDateTime: { type: String, default: '' },
});
const root = computed(() => parseRoot(props.data));
const reportUrl = computed(() => extractReportUrl(props.data));
const reportRef = ref(null);
const exporting = ref(false);
const reportTime = computed(
() => props.reportDateTime || new Date().toLocaleString('zh-CN'),
);
async function handleExport() {
exporting.value = true;
try {
await printReportAsPdf(reportRef.value);
} finally {
exporting.value = false;
}
}
defineExpose({ reportRef, handleExport });
</script>
<style lang="scss">
@import './shared.scss';
</style>
<style lang="scss" scoped>
.main-grid {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 20px;
margin-bottom: 20px;
}
.footer-note {
font-size: 13px;
color: #718096;
margin-top: 4px;
}
@media (max-width: 900px) {
.main-grid {
grid-template-columns: 1fr;
}
}
@media print {
.no-print {
display: none !important;
}
.hy-report {
padding: 0;
background: #fff;
}
.hy-container {
box-shadow: none;
border-radius: 0;
padding: 0;
}
.hy-card {
break-inside: avoid;
box-shadow: none;
}
}
</style>

View File

@@ -0,0 +1,18 @@
/**
* 通过浏览器打印对话框导出 PDF目标打印机选「另存为 PDF」
*/
export async function printReportAsPdf(reportElement) {
if (!reportElement) return;
document.body.classList.add('dwbg9fb2-printing');
await new Promise((resolve) => requestAnimationFrame(resolve));
try {
window.print();
} finally {
window.setTimeout(() => {
document.body.classList.remove('dwbg9fb2-printing');
}, 500);
}
}

View File

@@ -0,0 +1,680 @@
/** DWBG9FB2 海宇个人风险报告工具函数 */
export const RISK_LEVEL_INFO = {
A: { label: '低风险', levelText: '低风险', color: '#48bb78', note: '综合多维度大数据分析,用户当前风险较低,建议正常评估。' },
B: { label: '较低风险', levelText: '较低风险关注', color: '#4299e1', note: '综合多维度大数据分析,用户存在少量风险信号,建议关注。' },
C: { label: '中低风险', levelText: '中低风险关注', color: '#f6e05e', note: '综合多维度大数据分析,用户存在一定风险,建议审核。' },
D: { label: '中风险', levelText: '中风险关注', color: '#ed8936', note: '综合多维度大数据分析,用户风险偏高,建议严格审核。' },
E: { label: '较高风险', levelText: '较高风险关注', color: '#e53e3e', note: '综合多维度大数据分析,用户风险较高,建议谨慎评估。' },
F: { label: '高风险', levelText: '高风险关注', color: '#e53e3e', note: '综合多维度大数据分析,用户当前风险较高,建议谨慎评估。' },
};
export const RADAR_DIMENSIONS = [
{ key: 'identity', label: '身份真实性' },
{ key: 'credit', label: '信用表现' },
{ key: 'loanBehavior', label: '信贷行为' },
{ key: 'multiLoan', label: '多头借贷' },
{ key: 'device', label: '设备环境' },
{ key: 'judicial', label: '司法风险' },
];
export const RISK_FACTOR_CATEGORIES = [
{ key: 'credit', label: '信贷风险', color: '#3182ce', riskKeys: ['loanRiskTagV11'] },
{ key: 'judicial', label: '司法风险', color: '#ed8936', riskKeys: ['personalLawsuit'] },
{ key: 'multiLoan', label: '多头借贷', color: '#48bb78', riskKeys: ['loanRiskTagV11'] },
{ key: 'overdue', label: '逾期风险', color: '#f56565', riskKeys: ['loanRiskTagV10'] },
{ key: 'other', label: '其他风险', color: '#cbd5e0', riskKeys: ['mobile4Verify'] },
];
export const SUGGESTION_TEMPLATES = [
{ key: 'caution', icon: '❗', title: '谨慎授信', desc: '建议谨慎评估授信额度及期限', tone: 'red', levels: ['D', 'E', 'F'] },
{ key: 'reduce', icon: '2', title: '降低额度', desc: '建议降低授信额度', tone: 'orange', levels: ['D', 'E', 'F'] },
{ key: 'review', icon: '3', title: '人工审核', desc: '建议人工核实相关信息', tone: 'blue', levels: ['B', 'C', 'D', 'E', 'F'] },
{ key: 'monitor', icon: '🛡', title: '持续监控', desc: '建议持续关注风险变化', tone: 'green', levels: ['A', 'B', 'C', 'D', 'E', 'F'] },
];
export function parseRoot(data) {
if (!data || typeof data !== 'object') return {};
if (Array.isArray(data)) {
const first = data[0];
return first && typeof first === 'object' ? parseRoot(first) : {};
}
if (data.result && typeof data.result === 'object') {
if (data.result.riskLevel !== undefined || data.result.riskScore !== undefined || data.result.risks) {
return data.result;
}
if (data.result.result && typeof data.result.result === 'object') {
return data.result.result;
}
return data.result;
}
if (data.data?.result) return parseRoot({ result: data.data.result });
return data;
}
export function extractReportParams(data) {
if (!data || typeof data !== 'object') return {};
if (data.reportParams && typeof data.reportParams === 'object') return data.reportParams;
if (data.params && typeof data.params === 'object') return data.params;
if (data.data?.reportParams) return data.data.reportParams;
return {};
}
export function extractReportTime(data) {
if (!data || typeof data !== 'object') return '';
return data.reportDateTime || data.timestamp || data.data?.timestamp || '';
}
export function extractReportUrl(data) {
const root = parseRoot(data);
return root.reportUrl || data?.reportUrl || '';
}
export function cellText(v) {
if (v === null || v === undefined || v === '') return '—';
return String(v);
}
export function isHit(v) {
return v === 1 || v === '1' || v === true;
}
/** 当前逾期:字段可能是 0/1 标志,也可能是机构数量字符串 */
export function hasCurrentOverdue(overdue = {}, risks = {}) {
if (isHit(overdue.currently_overdue)) return true;
if (overdue.result_code === '1' || overdue.result_code === 1) return true;
const count = Number(overdue.currently_overdue);
if (!Number.isNaN(count) && count > 0) return true;
return (risks.loanRiskTagV10 || []).length > 0;
}
const RISK_KEY_CATEGORY = {
loanRiskTagV10: 'overdue',
loanRiskTagV21: 'overdue',
personalLawsuit: 'judicial',
mobile4Verify: 'other',
loanRiskTagV11: 'multiLoan',
loanRiskTagV12: 'credit',
loanRiskTagV18: 'credit',
};
export function riskLevelInfo(level) {
return RISK_LEVEL_INFO[level] || { label: '未知', levelText: '未知', color: '#718096', note: '暂无风险评级说明。' };
}
export function riskScorePercent(score) {
const n = Number(score);
if (Number.isNaN(n)) return 0;
return Math.min(100, Math.max(0, (n / 1000) * 100));
}
/** 风险概览五项 */
export function buildRiskOverviewItems(root) {
const overdue = root.loanRiskTagV10 || {};
const court = root.courtRisk || {};
const v11 = root.loanRiskTagV11 || {};
const duration = root.mobileDuration || {};
const risks = root.risks || {};
const hasOverdue = hasCurrentOverdue(overdue, risks);
const hasJudicial = court.shean || court.beigao || court.xingshi || (risks.personalLawsuit || []).length > 0;
const multiLoanLevel = (risks.loanRiskTagV11 || [])[0]?.includes('中度')
? '中度'
: (risks.loanRiskTagV11 || []).length ? '存在' : '正常';
const nbankLevel = v11.Rule_name_QJF040?.includes('中度') ? '中度' : (risks.loanRiskTagV11 || []).length ? '存在' : '正常';
let mobileText = '—';
if (duration.range) {
mobileText = duration.range.replace('[', '').replace(')', '').replace('+', '个月以上');
}
return [
{ icon: '❗', iconClass: 'orange', label: '当前逾期', status: hasOverdue ? '存在' : '无', statusClass: hasOverdue ? 'red' : 'blue' },
{ icon: '⚖️', iconClass: 'red', label: '司法风险', status: hasJudicial ? '命中' : '未命中', statusClass: hasJudicial ? 'red' : 'blue' },
{ icon: '👥', iconClass: 'user', label: '多头借贷风险', status: multiLoanLevel, statusClass: multiLoanLevel === '中度' ? 'orange' : 'blue' },
{ icon: '📋', iconClass: 'purple', label: '非银多头申请', status: nbankLevel, statusClass: nbankLevel === '中度' ? 'purple' : 'blue' },
{ icon: '📱', iconClass: 'blue', label: '手机使用时长', status: mobileText, statusClass: 'blue' },
];
}
/** 雷达图维度得分0-100 */
export function buildRadarScores(root) {
const auth = root.realNameAuth || {};
const m3 = root.mobile3Verify || {};
const m4 = root.mobile4Verify || {};
const overdue = root.loanRiskTagV10 || {};
const v11 = root.loanRiskTagV11 || {};
const court = root.courtRisk || {};
const score = Number(root.riskScore) || 0;
let identity = 100;
if (!isHit(auth.status)) identity -= 40;
if (!isHit(m3.status)) identity -= 30;
if (m4.status === 2 || m4.status === '2') identity -= 30;
const credit = Math.min(100, Math.round((score / 1000) * 100));
let loanBehavior = 80;
if (hasCurrentOverdue(overdue, root.risks)) loanBehavior -= 40;
if (overdue.result_code === '1') loanBehavior -= 20;
const m12Org = Number(v11.als_m12_id_coon_orgnum) || 0;
const multiLoan = Math.max(20, 100 - m12Org * 12);
let device = 80;
if (m4.status === 2 || m4.status === '2') device = 40;
if ((root.risks?.mobile4Verify || []).length) device = Math.min(device, 50);
let judicial = 100;
if (court.shean) judicial -= 30;
if (court.beigao) judicial -= 25;
if (court.xingshi) judicial -= 25;
return RADAR_DIMENSIONS.map((dim) => {
const map = {
identity,
credit,
loanBehavior,
multiLoan,
device,
judicial,
};
return { ...dim, score: Math.max(0, Math.min(100, map[dim.key] ?? 50)) };
});
}
export function buildRadarPolygon(scores, cx = 110, cy = 110, maxR = 80) {
const count = scores.length || 1;
const labelPad = 15;
return scores.map((item, i) => {
const angle = (Math.PI * 2 * i) / count - Math.PI / 2;
const r = (item.score / 100) * maxR;
const cos = Math.cos(angle);
const sin = Math.sin(angle);
let labelX;
let labelY;
let anchor;
let labelCenterX;
if (cos > 0.3) {
labelX = cx + maxR + labelPad;
labelY = cy + (maxR + 5) * sin;
anchor = 'start';
} else if (cos < -0.3) {
labelX = cx - maxR - labelPad;
labelY = cy + (maxR + 5) * sin;
anchor = 'end';
} else {
labelX = cx;
labelY = cy + (maxR + 15) * sin;
anchor = 'middle';
}
const labelWidth = item.label.length * 10;
if (anchor === 'start') {
labelCenterX = labelX + labelWidth / 2;
} else if (anchor === 'end') {
labelCenterX = labelX - labelWidth / 2;
} else {
labelCenterX = labelX;
}
return {
x: cx + r * cos,
y: cy + r * sin,
label: item.label,
score: item.score,
labelX,
labelY,
labelCenterX,
anchor,
};
});
}
/** 风险趋势近6个月末月为当前评分 */
export function buildRiskTrend(root) {
const score = Number(root.riskScore) || 0;
const now = new Date();
const months = [];
const values = [];
const factors = [0.7, 0.75, 0.82, 0.88, 0.94, 1];
for (let i = 5; i >= 0; i -= 1) {
const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
months.push(`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`);
values.push(Math.round(score * factors[5 - i]));
}
return { months, values, max: 1000 };
}
export function trendPointY(value, max, chartTop = 20, chartBottom = 160) {
const h = chartBottom - chartTop;
return chartTop + h - (value / max) * h;
}
/** 风险因素分布 */
export function buildRiskFactorDistribution(root) {
const risks = root.risks || {};
const buckets = RISK_FACTOR_CATEGORIES.map((cat) => ({ ...cat, count: 0 }));
Object.entries(risks).forEach(([riskKey, items]) => {
if (!Array.isArray(items)) return;
items.forEach((item) => {
let categoryKey = RISK_KEY_CATEGORY[riskKey] || 'other';
if (riskKey === 'loanRiskTagV11') {
categoryKey = String(item).includes('申请') || String(item).includes('多头')
? 'multiLoan'
: 'credit';
}
const bucket = buckets.find((b) => b.key === categoryKey);
if (bucket) bucket.count += 1;
});
});
const filtered = buckets.filter((item) => item.count > 0);
if (!filtered.length) {
return [{ label: '暂无风险', color: '#cbd5e0', count: 1, percent: '100.0', dasharray: '440 440', dashoffset: 0 }];
}
const total = filtered.reduce((s, i) => s + i.count, 0) || 1;
let offset = 0;
const circumference = 2 * Math.PI * 70;
return filtered.map((item) => {
const percent = (item.count / total) * 100;
const dash = (percent / 100) * circumference;
const seg = {
...item,
percent: percent.toFixed(1),
dasharray: `${dash} ${circumference}`,
dashoffset: -offset,
};
offset += dash;
return seg;
});
}
export function riskFactorTotal(root) {
const risks = root.risks || {};
return Object.values(risks).reduce((sum, arr) => sum + (Array.isArray(arr) ? arr.length : 0), 0);
}
/** 核心风险发现 */
export function buildCoreRiskFindings(root) {
const findings = [];
const overdue = root.loanRiskTagV10 || {};
const v11 = root.loanRiskTagV11 || {};
const lawsuit = root.personalLawsuit || {};
const count = lawsuit.count || {};
const risks = root.risks || {};
if (hasCurrentOverdue(overdue, risks)) {
findings.push({
title: '当前逾期风险',
desc: '存在当前未结清逾期记录',
level: '高危',
tone: 'high',
details: [
`最大逾期金额: ${cellText(overdue.max_overdue_amt)}`,
`最近逾期天数: ${cellText(overdue.max_overdue_days)}`,
`最近逾期月份: ${cellText(overdue.latest_overdue_time)}`,
],
});
}
const m12 = v11.als_m12_id_coon_allnum;
const m6 = v11.als_m6_id_coon_allnum;
const m3 = v11.als_m3_id_coon_allnum;
const m1 = v11.als_m1_id_coon_allnum;
if (m12 || m6 || m3 || m1 || (risks.loanRiskTagV11 || []).length) {
findings.push({
title: '多头借贷行为',
desc: '在非银机构多次申请借款',
level: '中度',
tone: 'medium',
details: [
`近12个月申请机构数: ${cellText(m12)}`,
`近6个月申请机构数: ${cellText(m6)}`,
`近3个月申请机构数: ${cellText(m3)}`,
`近1个月申请机构数: ${cellText(m1)}`,
],
});
}
if (lawsuit.has_case === 'Y' || count.count_total || (risks.personalLawsuit || []).length) {
const roles = (risks.personalLawsuit || []).filter((r) => !r.includes('罪'));
const caseType = count.ay_stat || (risks.personalLawsuit || []).find((r) => r.includes('罪')) || '—';
findings.push({
title: '司法风险',
desc: '存在司法案件记录',
level: '高危',
tone: 'high',
details: [
`案件数量: ${cellText(count.count_total)}`,
`涉案身份: ${roles.length ? roles.join('、') : '—'}`,
`案件类型: ${cellText(caseType)}`,
],
});
}
return findings;
}
/** 风险事件时间线 */
export function buildTimelineEvents(root) {
const events = [];
const lawsuit = root.personalLawsuit || {};
const overdue = root.loanRiskTagV10 || {};
const v11 = root.loanRiskTagV11 || {};
const risks = root.risks || {};
buildAllCaseList(lawsuit).forEach((c) => {
const date = c.d_larq || c.d_jarq || '';
const month = date ? date.slice(0, 7) : '—';
const isCriminal = c.sectionKey === 'criminal'
|| (c.n_jaay || c.n_laay || '').includes('罪')
|| (c.n_ajlx || '').includes('刑');
const tone = isCriminal ? 'red' : c.sectionKey === 'implement' ? 'orange' : 'blue';
events.push({
date: month,
title: isCriminal ? '刑事案件记录' : `${c.sectionLabel || '司法案件'}记录`,
desc: `${cellText(c.n_laay || c.n_jaay)}\n案件编号:${cellText(c.c_ah)}`,
tone,
sortKey: date || c.c_ah || '',
});
});
const m6Months = Number(v11.als_m6_id_tot_mons) || 0;
if (m6Months > 0) {
const d = new Date();
d.setMonth(d.getMonth() - 6);
events.push({
date: `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`,
title: '多头借贷行为',
desc: '近6个月内多次申请\n非银机构借款',
tone: 'orange',
sortKey: d.toISOString(),
});
}
if (hasCurrentOverdue(overdue, risks) && overdue.latest_overdue_time) {
events.push({
date: overdue.latest_overdue_time,
title: '当前逾期',
desc: `存在当前未结清逾期\n最大逾期金额${cellText(overdue.max_overdue_amt)}`,
tone: 'red',
sortKey: `${overdue.latest_overdue_time}-99`,
});
}
return events
.sort((a, b) => String(a.sortKey).localeCompare(String(b.sortKey)))
.slice(-4);
}
/** 查询记录统计 */
export function buildQueryStats(root) {
const v11 = root.loanRiskTagV11 || {};
const m12All = Number(v11.als_m12_id_coon_allnum) || 0;
const m12Nbank = Number(v11.als_m12_id_nbank_allnum) || 0;
const m12Caoff = Number(v11.als_m12_id_caoff_allnum) || 0;
const m1All = Number(v11.als_m1_id_coon_allnum) || 0;
return {
orgQueries: m12All + m12Nbank + m12Caoff || m12All,
personalQueries: Math.max(0, m1All - 2) || (m1All > 0 ? 2 : 0),
orgCount: m12All || Number(v11.als_m12_id_coon_orgnum) || 0,
latestQuery: cellText(root.loanRiskTagV10?.latest_overdue_time) !== '—'
? `${root.loanRiskTagV10.latest_overdue_time}-10`
: new Date().toISOString().slice(0, 10),
};
}
/** 黑名单命中统计 */
export function buildBlacklistStatus(root) {
const data = root.blackListV121_3 || {};
const periodKeys = Object.keys(data).filter((k) => /^h\d+_30d$/.test(k) || /^ha_30d/.test(k));
const hits = periodKeys.reduce((sum, k) => sum + (Number(data[k]) || 0), 0);
const grade = Number(data.grade) || 0;
return {
hits,
status: hits === 0 && grade <= 1 ? '正常' : grade >= 4 ? '高风险' : grade >= 3 ? '中风险' : '关注',
gradeText: grade >= 4 ? '高风险' : grade >= 3 ? '中风险' : grade >= 2 ? '较低风险' : '低风险',
};
}
/** 综合建议 */
export function buildSuggestions(riskLevel) {
return SUGGESTION_TEMPLATES.filter((item) => item.levels.includes(riskLevel));
}
export const REPORT_USAGE_NOTICE = '数据来源于合法合规渠道,仅供参考,不作为决策依据。';
export const JUDICIAL_USAGE_NOTICE = [
'客户使用本报告,需经过被查询人授权,客户承担因授权不充分引起的任何法律责任。',
'本报告仅限客户内部使用,请妥善保管本报告,不得向任何第三方泄露或允许任何第三方使用本报告。',
'本报告仅供客户参考,不作为客户决策的依据。',
'未经我司书面许可,任何人不得擅自复制、摘录、编辑、转载、披露和发表。',
'请确保在安全的物理及网络环境操作并确保导出内容的保密、安全以及合规应用。',
];
const PIE_COLORS = ['#3182ce', '#48bb78', '#ed8936', '#e53e3e', '#9b59b6', '#5dade2'];
/** 解析 "判决(2),维持(1)" 类分布字符串 */
export function parseStatDistribution(stat) {
if (!stat) return [];
return String(stat)
.split(',')
.map((part) => {
const m = part.trim().match(/^(.+)\((\d+)\)$/);
if (m) return { label: m[1].trim(), count: Number(m[2]) };
return { label: part.trim(), count: 1 };
})
.filter((item) => item.label);
}
export function buildConicGradient(items) {
if (!items.length) return '#e8eef7';
const total = items.reduce((s, i) => s + i.count, 0) || 1;
let acc = 0;
const stops = items.map((item, i) => {
const pct = (item.count / total) * 100;
const start = acc;
acc += pct;
return `${PIE_COLORS[i % PIE_COLORS.length]} ${start}% ${acc}%`;
});
return `conic-gradient(${stops.join(', ')})`;
}
export function toCaseArray(v) {
if (Array.isArray(v)) return v;
if (v && typeof v === 'object' && Object.keys(v).length) return [v];
return [];
}
function isCaseRecord(v) {
return !!(v && typeof v === 'object' && (v.c_ah || v.c_id || v.n_ajbs));
}
/** 从司法分区对象提取案件列表(兼容 { cases: [] } 结构) */
export function extractCaseList(section) {
if (!section) return [];
if (Array.isArray(section)) return section;
if (Array.isArray(section.cases)) return section.cases;
if (isCaseRecord(section)) return [section];
return [];
}
/** 从司法数据中提取列表(兼容多种字段名) */
export function extractJudicialList(data, keys) {
if (!data) return [];
for (const key of keys) {
const list = extractCaseList(data[key]);
if (list.length) return list;
}
return [];
}
export function caseListText(c) {
if (!c) return '—';
return `${c.c_ah || '—'}${c.n_ssdw || '—'}${c.n_ajjzjd || '—'}`;
}
export function limitHighRow(item) {
return {
ah: item.c_ah || item.ah || item.caseCode || '—',
legalPerson: item.legalPerson || item.qyfr || item.businessentity || '—',
entName: item.entName || item.qymc || item.companyName || '—',
court: item.n_jbfy || item.court || item.zxfy || '—',
publishDate: item.publishDate || item.fbrq || item.fb_date || '—',
filingDate: item.d_larq || item.larq || item.regDate || '—',
};
}
const CASE_SECTIONS = [
{ key: 'criminal', label: '刑事案件', tag: 'criminal' },
{ key: 'civil', label: '民事案件', tag: 'civil' },
{ key: 'administrative', label: '行政案件', tag: 'civil' },
{ key: 'implement', label: '执行案件', tag: 'civil' },
{ key: 'preservation', label: '非诉保全审查', tag: 'civil' },
{ key: 'bankrupt', label: '破产案件', tag: 'civil' },
];
/** 汇总全部案件并附带类型标签 */
export function buildAllCaseList(data) {
const list = [];
for (const section of CASE_SECTIONS) {
for (const c of extractCaseList(data[section.key])) {
list.push({
...c,
sectionKey: section.key,
sectionLabel: section.label,
typeTag: section.tag,
});
}
}
return list;
}
/** 司法风险概览四项统计sf.html 左栏) */
export function buildJudicialOverviewStats(data, courtRisk = {}) {
const count = data?.count || {};
const criminalCount = extractCaseList(data?.criminal).length;
const implementCount = extractCaseList(data?.implement).length;
const involvedCount = Number(count.count_beigao) || Number(count.count_total) || 0;
return [
{
label: '涉案人员',
value: courtRisk.shean ? Math.max(involvedCount, 1) : involvedCount,
unit: '人',
},
{
label: '被告人员',
value: Number(count.count_beigao) || 0,
unit: '人',
},
{
label: '刑事案件',
value: criminalCount,
unit: '件',
},
{
label: '执行信息',
value: implementCount,
unit: '条',
},
];
}
/** 主要案件类型分布饼图 */
export function buildMainCaseTypePie(data) {
const items = parseStatDistribution(data?.count?.ay_stat);
const total = items.reduce((s, i) => s + i.count, 0);
return {
total,
items: items.map((item, i) => ({
...item,
color: PIE_COLORS[i % PIE_COLORS.length],
percent: total ? ((item.count / total) * 100).toFixed(1) : '0.0',
})),
gradient: buildConicGradient(items),
};
}
/** 案件详情表格行 */
export function buildCaseTableRows(data) {
return buildAllCaseList(data).map((c) => ({
id: c.c_ah || c.c_id,
caseNo: cellText(c.c_ah),
typeLabel: c.sectionLabel,
typeTag: c.typeTag,
cause: cellText(c.n_laay || c.n_jaay || c.n_laay_tree),
role: cellText(c.n_ssdw),
incidentDate: cellText(c.d_larq),
status: cellText(c.n_ajjzjd),
raw: c,
}));
}
function extractProsecutor(text) {
if (!text) return '—';
const m = String(text).match(/公诉机关[^。,,;]*/);
return m ? m[0].replace(/。$/, '') : '—';
}
function extractParties(dsrxx, roles) {
if (!Array.isArray(dsrxx)) return '—';
const matched = dsrxx.filter((d) => roles.some((r) => (d.n_ssdw || '').includes(r)));
if (!matched.length) return '—';
return matched.map((d) => d.c_mc).join('、');
}
function extractFromJudgment(text, pattern) {
if (!text) return '—';
const m = String(text).match(pattern);
return m ? m[0] : '—';
}
/** 案件详细信息三栏sf.html 底部卡片) */
export function buildCaseDetailColumns(caseItem) {
if (!caseItem) {
return [[], [], []];
}
const pjjg = caseItem.c_gkws_pjjg || '';
const col1 = [
{ label: '案件类型', value: cellText(caseItem.sectionLabel) },
{ label: '案  由', value: cellText(caseItem.n_laay || caseItem.n_jaay) },
{ label: '角  色', value: cellText(caseItem.n_ssdw) },
{ label: '案发时间', value: cellText(caseItem.d_larq) },
{ label: '审理法院', value: cellText(caseItem.n_jbfy) },
{ label: '案件状态', value: cellText(caseItem.n_ajjzjd) },
];
const col2 = [
{
label: '涉案人员',
value: extractParties(caseItem.c_dsrxx, ['被告人', '上诉人', '被执行人', '被申请人']),
},
{ label: '被害人', value: extractParties(caseItem.c_dsrxx, ['被害人']) },
{ label: '公诉机关', value: extractProsecutor(caseItem.c_gkws_dsr) },
];
const col3 = [
{
label: '判决结果',
value: cellText(caseItem.n_jafs || (pjjg ? pjjg.slice(0, 40) + (pjjg.length > 40 ? '…' : '') : '')),
},
{ label: '缓  刑', value: extractFromJudgment(pjjg, /缓刑[^。,,;]*/) },
{ label: '罚  金', value: extractFromJudgment(pjjg, /罚金[^。,,;]*/) },
{ label: '判决日期', value: cellText(caseItem.d_jarq) },
];
return [col1, col2, col3];
}

View File

@@ -0,0 +1,74 @@
.hy-report {
--hy-primary: #3182ce;
--hy-danger: #e53e3e;
--hy-warning: #ed8936;
--hy-success: #48bb78;
--hy-bg: #f0f4f9;
--hy-text: #1a202c;
--hy-muted: #4a5568;
background: var(--hy-bg);
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", sans-serif;
font-size: 14px;
color: var(--hy-text);
}
.hy-container {
max-width: 1200px;
margin: 0 auto;
background: #fff;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
padding: 30px;
}
.hy-card {
background: #fff;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.04);
border: 1px solid #e2e8f0;
margin-bottom: 20px;
}
.hy-card-title {
font-size: 16px;
font-weight: 600;
color: var(--hy-text);
margin-bottom: 20px;
display: flex;
align-items: center;
&::before {
content: '';
width: 4px;
height: 16px;
background-color: var(--hy-primary);
margin-right: 8px;
border-radius: 2px;
}
}
.hy-card-title--lg {
font-size: 18px;
margin-bottom: 24px;
&::before {
display: none;
}
}
.hy-text-danger { color: var(--hy-danger); }
.hy-text-warning { color: var(--hy-warning); }
.hy-text-muted { color: var(--hy-muted); }
.hy-small { font-size: 12px; color: #718096; }
@media (max-width: 768px) {
.hy-report {
padding: 12px;
}
.hy-container {
padding: 16px;
}
}

View File

@@ -0,0 +1,96 @@
<template>
<div class="gamma-grid-3">
<div class="gamma-card info-card">
<div class="gamma-title"><span>👤</span> 用户信息</div>
<div class="info-item">
<span class="info-label">姓名:</span>
<span class="info-value">
{{ maskedName }}
<span v-if="realNameAuth.coincide" class="gamma-tag">身份证姓名一致</span>
</span>
</div>
<div class="info-item"><span class="info-label">性别</span><span>{{ params.sex || '—' }}</span></div>
<div class="info-item"><span class="info-label">年龄</span><span>{{ params.age || '—' }}</span></div>
<div class="info-item"><span class="info-label">身份证号</span><span>{{ maskedIdCard }}</span></div>
<div class="info-item"><span class="info-label">户籍地</span><span>{{ params.location || '—' }}</span></div>
</div>
<div class="gamma-card info-card">
<div class="gamma-title"><span>📱</span> 手机信息</div>
<div class="info-item">
<span class="info-label">手机号:</span>
<span>
{{ maskedMobile }}
<span v-if="mobile3Verify.status === 1" class="gamma-tag">身份证姓名手机号一致</span>
</span>
</div>
<div class="info-item">
<span class="info-label">手机号在网时长:</span>
<span>{{ durationText }} </span>
</div>
<div class="info-item">
<span class="info-label">手机号在网状态:</span>
<span :class="mobile4Verify.status === 2 ? 'gamma-text-danger' : ''">
{{ mobileStatusText(mobile4Verify.status) }}
<span v-if="mobile4Verify.status === 2"> </span>
</span>
</div>
<div class="info-item"><span class="info-label">手机号运营商:</span><span>{{ params.carrier || '—' }}</span></div>
<div class="info-item"><span class="info-label">手机号归属地:</span><span>{{ params.phonePlace || '—' }}</span></div>
</div>
<div class="gamma-card info-card">
<div class="gamma-title"><span></span> 司法风险</div>
<div v-for="item in courtItems" :key="item.key" class="info-item">
<span class="info-label">
<span :class="item.hit ? 'gamma-text-danger' : 'gamma-text-success'">{{ item.hit ? '❗' : '✅' }}</span>
{{ item.label }}
</span>
<span :class="item.hit ? 'gamma-text-danger' : ''">{{ item.hit ? '是' : '否' }}</span>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { maskName, maskIdCard, maskMobile, mobileStatusText, buildCourtRiskItems } from '../reportHelper';
const props = defineProps({
params: { type: Object, default: () => ({}) },
realNameAuth: { type: Object, default: () => ({}) },
mobile3Verify: { type: Object, default: () => ({}) },
mobile4Verify: { type: Object, default: () => ({}) },
mobileDuration: { type: Object, default: () => ({}) },
courtRisk: { type: Object, default: () => ({}) },
personalLawsuit: { type: Object, default: () => ({}) },
});
const maskedName = computed(() => maskName(props.params.name));
const maskedIdCard = computed(() => maskIdCard(props.params.id_card));
const maskedMobile = computed(() => maskMobile(props.params.mobile));
const durationText = computed(() => {
const range = props.mobileDuration.range || '';
if (range.includes('[24')) return '24~36月';
return range || '—';
});
const courtItems = computed(() =>
buildCourtRiskItems(props.courtRisk, props.personalLawsuit),
);
</script>
<style lang="scss" scoped>
.info-card { margin-bottom: 0; }
.info-item {
display: flex;
justify-content: space-between;
margin: 10px 0;
font-size: 14px;
gap: 8px;
}
.info-label { color: #666; flex-shrink: 0; }
</style>

View File

@@ -0,0 +1,87 @@
<template>
<div class="gamma-card">
<div class="gamma-title"><span>📞</span> 投诉风险筛查</div>
<p class="gamma-small" style="margin-bottom: 16px;">风险分: 0-100分数越高投诉风险越高</p>
<div class="complaint-content">
<div class="complaint-score-card">
<div class="gamma-subtitle"><span></span> 筛查结果</div>
<div class="complaint-risk-circle">
<div class="circle" />
<div class="complaint-risk-text">
<div class="score">{{ data.score ?? '—' }}</div>
<div class="gamma-small">风险分</div>
</div>
</div>
</div>
<div>
<table class="gamma-table">
<thead>
<tr><th>规则名称</th><th>权重</th></tr>
</thead>
<tbody>
<tr><td>是否建议拨打电话</td><td>{{ data.is_call ? '是' : '否' }}</td></tr>
<tr><td>是否高频</td><td>{{ data.is_gp ? '是' : '否' }}</td></tr>
<tr><td>是否靓号</td><td>{{ data.is_lh ? '是' : '否' }}</td></tr>
<tr><td>近14天其他类来电次数</td><td>{{ data.other_times?.day_14 ?? '—' }}</td></tr>
<tr><td>近14天金融类来电次数</td><td>{{ data.finance_times?.day_14 ?? '—' }}</td></tr>
<tr><td>用户接受意愿登记</td><td>{{ dncText }}</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { dncRegisterText } from '../reportHelper';
const props = defineProps({
data: { type: Object, default: () => ({}) },
});
const dncText = computed(() => dncRegisterText(props.data.dnc));
</script>
<style lang="scss" scoped>
.complaint-content {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 24px;
}
.complaint-score-card {
display: flex;
flex-direction: column;
align-items: center;
}
.complaint-risk-circle {
width: 120px;
height: 120px;
position: relative;
}
.complaint-risk-circle .circle {
width: 100px;
height: 100px;
border: 6px solid #eee;
border-top-color: #66cc99;
border-radius: 50%;
margin: 0 auto;
}
.complaint-risk-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
}
.score { font-size: 28px; font-weight: 600; }
@media (max-width: 768px) {
.complaint-content { grid-template-columns: 1fr; }
}
</style>

View File

@@ -0,0 +1,483 @@
<template>
<div class="panorama-report">
<div class="panorama-main-title"><span>🛡</span> 信用全景扫描</div>
<div v-if="!available" class="panorama-empty">
{{ emptyMessage }}
</div>
<template v-else>
<!-- 第一行指数 + 机构借贷情况 -->
<div class="panorama-row">
<div class="panorama-card">
<div class="score-grid">
<div v-for="item in CREDIT_PANORAMA_SCORES" :key="item.key" class="score-item">
<div class="score-label">{{ item.label }}</div>
<div class="circle-index">
<span :class="['score-value', { muted: formatModelScore(data[item.key]) === '未命中' }]">
{{ formatModelScore(data[item.key]) }}
</span>
</div>
<div class="score-hint">350-950指数越大逾期率越低</div>
</div>
</div>
</div>
<div class="panorama-card">
<div class="block-title"><span>🏦</span> 机构借贷情况</div>
<div class="mini-grid">
<div v-for="item in CREDIT_PANORAMA_INSTITUTIONS" :key="item.key" class="mini-box">
<div class="mini-box-head">
<span>{{ item.label }}</span>
<span
class="gamma-tag"
:class="{
'gamma-tag--danger': getPanoramaRiskTag(data[item.key]).danger,
'gamma-tag--warn': getPanoramaRiskTag(data[item.key]).warn,
}"
>
{{ getPanoramaRiskTag(data[item.key]).label }}
</span>
</div>
<div class="mini-box-value">{{ formatPanoramaCount(data[item.key], item.unit) }}</div>
</div>
</div>
</div>
</div>
<!-- 第二行近期贷款申请 + 还款历史 -->
<div class="panorama-row">
<div class="panorama-card">
<div class="block-title"><span></span> 近期贷款申请</div>
<div class="list-rows">
<div v-for="item in CREDIT_PANORAMA_RECENT_LOAN" :key="item.key" class="list-row">
<div class="list-row-left">
<span
class="gamma-tag tag-inline"
:class="{
'gamma-tag--danger': getPanoramaRiskTag(data[item.key]).danger,
'gamma-tag--warn': getPanoramaRiskTag(data[item.key]).warn,
}"
>
{{ getPanoramaRiskTag(data[item.key]).label }}
</span>
<span>{{ item.label }}</span>
</div>
<span class="list-row-value">{{ formatPanoramaCount(data[item.key], item.unit) }}</span>
</div>
</div>
</div>
<div class="panorama-card">
<div class="block-title"><span>📋</span> 还款历史</div>
<div class="stat-grid-2">
<div class="stat-box">
<div class="stat-label">历史贷款机构成功还款笔数</div>
<div class="stat-value">{{ formatPanoramaCount(data.xyp_cpl0014, ' 笔') }}</div>
</div>
<div class="stat-box">
<div class="stat-label">历史贷款机构交易失败笔数</div>
<div class="stat-value">{{ formatPanoramaCount(data.xyp_cpl0015, ' 笔') }}</div>
</div>
</div>
<div class="list-rows">
<div class="list-row">
<span>90天还款成功率</span>
<span class="list-row-value">{{ formatPanoramaRatio(data.xyp_cpl0080) }}</span>
</div>
<div class="list-row">
<span>近90天内还款中成功还款总金额比例</span>
<span class="list-row-value">{{ formatPanoramaRatio(data.xyp_cpl0079) }}</span>
</div>
<div class="list-row">
<span>近5次还款中成功还款总金额比例</span>
<span class="list-row-value">{{ formatPanoramaRatio(data.xyp_cpl0073) }}</span>
</div>
<div class="list-row">
<span>近5次还款中还款成功笔数比例</span>
<span class="list-row-value">{{ formatPanoramaRatio(data.xyp_cpl0074) }}</span>
</div>
</div>
</div>
</div>
<!-- 第三行交易失败 + 交易失败后还款 -->
<div class="panorama-row">
<div class="panorama-card">
<div class="block-title"><span></span> 交易失败情况</div>
<div class="list-rows">
<div v-for="item in CREDIT_PANORAMA_FAIL_COUNTS" :key="item.key" class="list-row">
<div class="list-row-left">
<span
class="gamma-tag tag-inline"
:class="{
'gamma-tag--danger': getPanoramaRiskTag(data[item.key]).danger,
'gamma-tag--warn': getPanoramaRiskTag(data[item.key]).warn,
}"
>
{{ getPanoramaRiskTag(data[item.key]).label }}
</span>
<span>{{ item.label }}</span>
</div>
<span class="list-row-value">{{ formatPanoramaCount(data[item.key], item.unit) }}</span>
</div>
</div>
</div>
<div class="panorama-card">
<div class="block-title"><span></span> 交易失败后还款情况</div>
<div class="list-rows">
<div v-for="item in CREDIT_PANORAMA_FAIL_RECOVERY" :key="item.key" class="list-row">
<div class="list-row-left">
<span
class="gamma-tag tag-inline"
:class="{
'gamma-tag--danger': getPanoramaRiskTag(data[item.key]).danger,
'gamma-tag--warn': getPanoramaRiskTag(data[item.key]).warn,
}"
>
{{ getPanoramaRiskTag(data[item.key]).label }}
</span>
<span>{{ item.label }}</span>
</div>
<span class="list-row-value">{{ formatPanoramaCount(data[item.key], item.unit) }}</span>
</div>
</div>
</div>
</div>
<!-- 第四行逾期情况 + 贷款整体情况 -->
<div class="panorama-row">
<div class="panorama-card">
<div class="block-title"><span>🚨</span> 逾期情况</div>
<div class="stat-grid-2">
<div class="stat-box">
<div class="stat-label">当前逾期机构数</div>
<div class="stat-value">{{ formatPanoramaCount(data.xyp_cpl0071, '家') }}</div>
</div>
<div class="stat-box">
<div class="stat-label">当前逾期金额</div>
<div class="stat-value">{{ formatPanoramaAmount(data.xyp_cpl0072) }}</div>
</div>
</div>
<div class="list-rows">
<div v-for="item in CREDIT_PANORAMA_OVERDUE_FLAGS" :key="item.key" class="list-row flag-row">
<div class="list-row-left">
<span class="success-check"></span>
<span>{{ item.label }}</span>
</div>
<span
class="list-row-value"
:class="{ 'gamma-text-danger': data[item.key] === '1' }"
>
{{ formatPanoramaYesNo(data[item.key]) }}
</span>
</div>
</div>
</div>
<div class="panorama-card">
<div class="block-title"><span>📈</span> 贷款整体情况</div>
<div class="list-rows">
<div v-for="item in CREDIT_PANORAMA_LOAN_OVERVIEW" :key="item.key" class="list-row flag-row">
<div class="list-row-left">
<span class="success-check"></span>
<span>{{ item.label }}</span>
</div>
<span class="list-row-value">
{{
item.type === 'amount'
? formatPanoramaAmount(data[item.key])
: formatPanoramaCount(data[item.key], item.unit)
}}
</span>
</div>
</div>
</div>
</div>
<!-- 第五行贷款分周期汇总 -->
<div class="panorama-card panorama-card--full">
<div class="block-title"><span>📊</span> 贷款分周期汇总</div>
<div class="table-wrap">
<table class="gamma-table panorama-table">
<thead>
<tr>
<th>周期</th>
<th>贷款机构数</th>
<th>还款成功笔数</th>
<th>还款成功总金额()</th>
<th>交易失败笔数</th>
<th>交易失败总金额()</th>
</tr>
</thead>
<tbody>
<tr v-for="row in CREDIT_PANORAMA_PERIOD_ROWS" :key="row.label">
<td>{{ row.label }}</td>
<td>{{ panoramaTableCell(data, row.instKey) }}</td>
<td>{{ panoramaTableCell(data, row.successKey) }}</td>
<td>{{ panoramaTableCell(data, row.successAmtKey, 'amount') }}</td>
<td>{{ panoramaTableCell(data, row.failKey) }}</td>
<td>{{ panoramaTableCell(data, row.failAmtKey, 'amount') }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
</div>
</template>
<script setup>
import { computed } from 'vue';
import {
CREDIT_PANORAMA_SCORES,
CREDIT_PANORAMA_INSTITUTIONS,
CREDIT_PANORAMA_RECENT_LOAN,
CREDIT_PANORAMA_FAIL_COUNTS,
CREDIT_PANORAMA_FAIL_RECOVERY,
CREDIT_PANORAMA_OVERDUE_FLAGS,
CREDIT_PANORAMA_LOAN_OVERVIEW,
CREDIT_PANORAMA_PERIOD_ROWS,
formatModelScore,
formatPanoramaCount,
formatPanoramaAmount,
formatPanoramaRatio,
formatPanoramaYesNo,
getPanoramaRiskTag,
panoramaTableCell,
isPanoramaDataAvailable,
} from '../reportHelper';
const props = defineProps({
data: { type: Object, default: () => ({}) },
});
const available = computed(() => isPanoramaDataAvailable(props.data));
const emptyMessage = computed(() => props.data?.loanRiskTagV21_msg || '数据库未查得');
</script>
<style lang="scss" scoped>
.panorama-report {
background: #fff;
border-radius: 8px;
padding: 24px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.panorama-main-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 8px;
color: #333;
padding-left: 10px;
border-left: 4px solid #d4af37;
}
.panorama-empty {
text-align: center;
padding: 48px 24px;
color: #718096;
font-size: 15px;
background: #f7fafc;
border-radius: 8px;
border: 1px dashed #e2e8f0;
}
.panorama-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
.panorama-card {
background: #fff;
border: 1px solid #eee;
border-radius: 12px;
padding: 20px;
&--full {
margin-bottom: 0;
}
}
.block-title {
font-size: 15px;
font-weight: 600;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 6px;
color: #333;
}
.score-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.score-item {
text-align: center;
}
.score-label {
font-size: 14px;
font-weight: 500;
color: #444;
margin-bottom: 12px;
}
.circle-index {
width: 120px;
height: 120px;
border: 4px solid #f0f0f0;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 10px;
}
.score-value {
font-size: 20px;
font-weight: 600;
color: #2da44e;
&.muted {
font-size: 16px;
color: #999;
}
}
.score-hint {
font-size: 12px;
color: #999;
line-height: 1.4;
}
.mini-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.mini-box {
border: 1px solid #eee;
border-radius: 8px;
padding: 12px;
}
.mini-box-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
font-size: 13px;
color: #444;
}
.mini-box-value {
margin-top: 6px;
font-size: 13px;
color: #666;
}
.list-rows {
display: flex;
flex-direction: column;
gap: 12px;
}
.list-row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
font-size: 13px;
color: #444;
}
.list-row-left {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
min-width: 0;
}
.tag-inline {
flex-shrink: 0;
}
.list-row-value {
flex-shrink: 0;
color: #666;
}
.stat-grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-bottom: 16px;
}
.stat-box {
border: 1px solid #eee;
border-radius: 8px;
padding: 16px;
text-align: center;
}
.stat-label {
font-size: 12px;
color: #999;
margin-bottom: 6px;
}
.stat-value {
font-size: 16px;
font-weight: 500;
color: #444;
}
.success-check {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
background: #2da44e;
border-radius: 50%;
color: #fff;
font-size: 10px;
flex-shrink: 0;
}
.flag-row .list-row-left span:last-child {
line-height: 1.4;
}
.table-wrap {
overflow-x: auto;
}
.panorama-table {
min-width: 720px;
}
@media (max-width: 992px) {
.panorama-row {
grid-template-columns: 1fr;
}
.score-grid {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,219 @@
<template>
<div class="gamma-card">
<div class="gamma-title"><span>👤</span> 欺诈黑名单</div>
<div class="gamma-subtitle"><span></span> 风险等级</div>
<div class="fraud-risk-circle">
<div class="circle" />
<div class="fraud-risk-text">
<div class="risk-level">{{ gradeText }}</div>
<div class="gamma-small">风险等级</div>
</div>
</div>
<div class="gamma-subtitle"><span></span> 命中统计</div>
<div class="fraud-stats-grid">
<div class="chart-card">
<div class="chart-title">命中总次数</div>
<div class="chart-bars">
<div v-for="bar in hitBars" :key="bar.label" class="bar-group">
<div class="bar-value">{{ bar.value }}</div>
<div class="bar" :style="{ height: bar.height + 'px' }" />
<div class="bar-label">{{ bar.label }}</div>
</div>
</div>
</div>
<div class="chart-card">
<div class="chart-title">命中机构数</div>
<div class="chart-bars">
<div v-for="bar in orgBars" :key="bar.label" class="bar-group">
<div class="bar-value">{{ bar.value }}</div>
<div class="bar" :style="{ height: bar.height + 'px' }" />
<div class="bar-label">{{ bar.label }}</div>
</div>
</div>
</div>
</div>
<div class="gamma-subtitle"><span></span> 命中分布</div>
<div class="chart-card">
<div class="distribution-bars">
<div v-for="(dist, i) in distributions" :key="i" class="distribution-bar-group">
<div class="distribution-bar blue" :style="{ height: dist.d30 + 'px' }" />
<div class="distribution-bar orange" :style="{ height: dist.d90 + 'px' }" />
<div class="distribution-bar yellow" :style="{ height: dist.d180 + 'px' }" />
</div>
</div>
<div class="distribution-labels">
<div v-for="label in FRAUD_DIST_LABELS" :key="label">{{ label }}</div>
</div>
<div class="chart-legend">
<div class="legend-item"><div class="legend-color blue" /><span>近30天</span></div>
<div class="legend-item"><div class="legend-color orange" /><span>近90天</span></div>
<div class="legend-item"><div class="legend-color yellow" /><span>近180天</span></div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { FRAUD_DIST_LABELS, FRAUD_DIST_KEYS, fraudGradeText } from '../reportHelper';
const props = defineProps({
data: { type: Object, default: () => ({}) },
});
const gradeText = computed(() => fraudGradeText(props.data.grade));
function barHeight(val, max) {
if (!max) return 0;
return Math.max(4, Math.round((Number(val) || 0) / max * 120));
}
const hitBars = computed(() => {
const d = props.data;
const values = [
{ label: '近30天', value: d.ha_30d_C ?? 0 },
{ label: '近90天', value: d.ha_90d_C ?? 0 },
{ label: '近180天', value: d.ha_180d_C ?? 0 },
];
const max = Math.max(...values.map((v) => Number(v.value)), 1);
return values.map((v) => ({ ...v, height: barHeight(v.value, max) }));
});
const orgBars = computed(() => {
const d = props.data;
const values = [
{ label: '近30天', value: d.ha_30d_J ?? 0 },
{ label: '近90天', value: d.ha_90d_J ?? 0 },
{ label: '近180天', value: d.ha_180d_J ?? 0 },
];
const max = Math.max(...values.map((v) => Number(v.value)), 1);
return values.map((v) => ({ ...v, height: barHeight(v.value, max) }));
});
const distributions = computed(() => {
const d = props.data;
const max = Math.max(
...FRAUD_DIST_KEYS.flatMap((k) => [d[k.d30], d[k.d90], d[k.d180]].map(Number)),
1,
);
return FRAUD_DIST_KEYS.map((k) => ({
d30: barHeight(d[k.d30], max),
d90: barHeight(d[k.d90], max),
d180: barHeight(d[k.d180], max),
}));
});
</script>
<style lang="scss" scoped>
.fraud-risk-circle {
width: 200px;
height: 200px;
margin: 0 auto 24px;
position: relative;
}
.fraud-risk-circle .circle {
width: 160px;
height: 160px;
border: 8px solid #eee;
border-top-color: #fdd860;
border-right-color: #fdd860;
border-radius: 50%;
margin: 0 auto;
}
.fraud-risk-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
}
.risk-level { font-size: 20px; font-weight: 600; color: #fdd860; }
.fraud-stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
margin-bottom: 16px;
}
.chart-card {
border: 1px solid #eee;
border-radius: 6px;
padding: 16px;
}
.chart-title { text-align: center; margin-bottom: 12px; font-size: 14px; }
.chart-bars {
display: flex;
align-items: flex-end;
justify-content: space-around;
height: 150px;
}
.bar-group { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.bar {
width: 30px;
background: #4a6fd4;
border-radius: 3px 3px 0 0;
}
.bar-label, .bar-value { font-size: 12px; color: #666; }
.distribution-bars {
display: flex;
align-items: flex-end;
justify-content: space-around;
height: 160px;
margin-top: 20px;
}
.distribution-bar-group {
display: flex;
gap: 4px;
align-items: flex-end;
}
.distribution-bar {
width: 20px;
border-radius: 2px 2px 0 0;
}
.distribution-bar.blue { background: #4a6fd4; }
.distribution-bar.orange { background: #f28534; }
.distribution-bar.yellow { background: #f7bc0c; }
.distribution-labels {
display: flex;
justify-content: space-around;
font-size: 10px;
color: #666;
margin-top: 8px;
}
.chart-legend {
display: flex;
justify-content: center;
gap: 16px;
font-size: 12px;
margin-top: 12px;
}
.legend-item { display: flex; align-items: center; gap: 4px; }
.legend-color {
width: 12px;
height: 8px;
border-radius: 2px;
}
.legend-color.blue { background: #4a6fd4; }
.legend-color.orange { background: #f28534; }
.legend-color.yellow { background: #f7bc0c; }
</style>

View File

@@ -0,0 +1,988 @@
<template>
<div class="gamma-card judicial-section">
<div class="gamma-title"><span>🗺</span> 司法案件</div>
<!-- 案件概览 -->
<div class="block-title">案件概览</div>
<div class="case-overview">
<div
v-for="item in caseTypeCards"
:key="item.key"
class="overview-card"
:class="{ criminal: item.count > 0 && item.key === 'criminal' }"
>
<div v-if="item.count > 0 && item.key === 'criminal'" class="badge">!</div>
<div class="card-title">{{ item.label }}</div>
<div class="card-num">{{ item.count }}</div>
</div>
</div>
<!-- 司法风险 -->
<div class="risk-section">
<div class="risk-item">
<div class="label">司法风险<br>被告案件数</div>
<div class="bar-wrap">
<div class="bar-fill" :style="{ width: defendantBarWidth }" />
</div>
<div class="legend">
<span class="unresolved">未结案{{ count.count_wei_beigao ?? 0 }}</span>
<span class="resolved">已经结案{{ count.count_jie_beigao ?? 0 }}</span>
</div>
<div class="total-hint">{{ count.count_beigao ?? 0 }}</div>
</div>
<div class="risk-item">
<div class="label">被告案件金额</div>
<div class="num-row">
<span class="icon-yen">¥</span>
<span>{{ count.money_beigao ?? 0 }}</span>
</div>
</div>
<div class="risk-item">
<div class="label">被执行案件数</div>
<div class="num-row red">
<span>📋</span>
<span>{{ implementCount }}</span>
</div>
</div>
<div class="risk-item">
<div class="label">最近案件年份</div>
<div class="num-row red">
<span>🗓</span>
<span>{{ recentYearText }}</span>
</div>
</div>
</div>
<!-- 案件统计 -->
<div class="block-title">案件统计</div>
<div class="case-stats">
<div class="chart-title">案件数量</div>
<div class="bar-chart">
<div class="chart-legend">
<span class="unresolved">
未结案统计
<em>{{ count.count_wei_total ?? 0 }}</em>
</span>
<span class="resolved">
已结案统计
<em>{{ count.count_jie_total ?? 0 }}</em>
</span>
</div>
<div class="bar-rows">
<div v-for="row in roleBarRows" :key="row.label" class="bar-row">
<div class="row-label">{{ row.label }}</div>
<div class="row-bar-track">
<div class="row-bar" :style="{ width: row.width }" />
</div>
<div class="row-value">{{ row.count }}</div>
</div>
</div>
<div class="axis">
<span v-for="tick in axisTicks" :key="tick">{{ tick }}</span>
</div>
</div>
<div class="chart-title">涉案金额</div>
<div class="amount-section">
<div v-for="card in amountCards" :key="card.title" class="amount-card">
<div class="card-header">
<span>{{ card.icon }}</span>
<span>{{ card.title }}</span>
</div>
<div class="row"><span>总案件</span><span class="value">{{ card.total }}</span></div>
<div class="row"><span>已结案</span><span class="value">{{ card.jie }}</span></div>
<div class="row"><span>未结案</span><span class="value">{{ card.wei }}</span></div>
</div>
</div>
<div class="pie-section">
<div v-for="pie in pieCharts" :key="pie.title" class="pie-card">
<div class="pie-container">
<div class="pie-circle" :class="{ simple: pie.items.length <= 1 }" :style="{ background: pie.gradient }">
<div class="inner">
{{ pie.title }}<br>占比图
</div>
</div>
</div>
<div v-if="pie.items.length" class="pie-legend">
<div v-for="(item, i) in pie.items" :key="i" class="pie-legend-item">
<span class="dot" :style="{ background: pieColors[i % pieColors.length] }" />
{{ item.label }}({{ item.count }})
</div>
</div>
</div>
</div>
</div>
<!-- 案件列表可展开详情 -->
<div v-if="allCases.length" class="case-list-card">
<div
v-for="(c, i) in allCases"
:key="`${c.c_ah}-${i}`"
class="case-list-group"
>
<div
class="case-item"
:class="{ active: expandedIndex === i }"
@click="toggleCase(i)"
>
<span class="case-text">{{ caseListText(c) }}</span>
<span class="case-arrow">{{ expandedIndex === i ? '' : '>' }}</span>
</div>
<div v-if="expandedIndex === i" class="case-block">
<div class="case-info">
<div><span>案件类型:</span><span>{{ c.sectionLabel }}</span></div>
<div><span>诉讼地位:</span><span>{{ c.n_ssdw }}</span></div>
<div><span>所属地域:</span><span>{{ c.c_ssdy }}</span></div>
<div><span>当事人:</span><span>{{ partiesText(c.c_dsrxx) }}</span></div>
<div><span>审理程序:</span><span>{{ c.n_slcx }}</span></div>
<div><span>立案时间:</span><span>{{ c.d_larq }}</span></div>
<div><span>结束时间:</span><span>{{ c.d_jarq }}</span></div>
<div><span>立案案由详情:</span><span>{{ c.n_laay_tree }}</span></div>
<div><span>结案方式:</span><span>{{ c.n_jafs }}</span></div>
<div><span>经办法院:</span><span>{{ c.n_jbfy }}</span></div>
</div>
<div v-if="c.c_gkws_pjjg" class="case-judgment gamma-small">
判决结果<br>{{ c.c_gkws_pjjg }}
</div>
</div>
</div>
</div>
<!-- 失信被执行人 -->
<div class="module-card">
<div class="module-title title-dishonest">失信被执行人</div>
<template v-if="dishonestList.length">
<div class="case-list-card inner-list">
<div v-for="(item, i) in dishonestList" :key="i" class="case-item static">
<span class="case-text">{{ dishonestText(item) }}</span>
</div>
</div>
</template>
<div v-else class="empty-data">
<svg class="empty-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 7C3 5.89543 3.89543 5 5 5H19C20.1046 5 21 5.89543 21 7V17C21 18.1046 20.1046 19 19 19H5C3.89543 19 3 18.1046 3 17V7Z" stroke="#999" stroke-width="1.5" />
<path d="M3 7L12 12L21 7" stroke="#999" stroke-width="1.5" stroke-linecap="round" />
</svg>
<span>暂无数据</span>
</div>
</div>
<!-- 限高被执行人 -->
<div class="module-card">
<div class="module-title title-high-limit">限高被执行人</div>
<table class="data-table">
<thead>
<tr>
<th>案号</th>
<th>企业法人</th>
<th>企业名称</th>
<th>执行法院</th>
<th>发布时间日期</th>
<th>立案时间日期</th>
</tr>
</thead>
<tbody>
<template v-if="limitHighList.length">
<tr v-for="(item, i) in limitHighRows" :key="i">
<td>{{ item.ah }}</td>
<td>{{ item.legalPerson }}</td>
<td>{{ item.entName }}</td>
<td>{{ item.court }}</td>
<td>{{ item.publishDate }}</td>
<td>{{ item.filingDate }}</td>
</tr>
</template>
<tr v-else>
<td colspan="6">
<div class="empty-data">
<svg class="empty-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 7C3 5.89543 3.89543 5 5 5H19C20.1046 5 21 5.89543 21 7V17C21 18.1046 20.1046 19 19 19H5C3.89543 19 3 18.1046 3 17V7Z" stroke="#999" stroke-width="1.5" />
<path d="M3 7L12 12L21 7" stroke="#999" stroke-width="1.5" stroke-linecap="round" />
</svg>
<span>暂无数据</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 报告使用须知 -->
<div class="notice-card">
<div class="notice-title">报告使用须知</div>
<ol class="notice-list">
<li v-for="(line, i) in REPORT_USAGE_NOTICE" :key="i">{{ line }}</li>
</ol>
</div>
</div>
</template>
<script setup>
import { computed, ref } from 'vue';
import {
parseStatDistribution,
buildConicGradient,
extractCaseList,
extractJudicialList,
REPORT_USAGE_NOTICE,
caseListText,
limitHighRow,
} from '../reportHelper';
const props = defineProps({
data: { type: Object, default: () => ({}) },
});
const PIE_COLORS = ['#4E74F6', '#89d079', '#f6c358', '#ff7c7c', '#9b59b6', '#5dade2'];
const pieColors = PIE_COLORS;
const count = computed(() => props.data.count || {});
const CASE_TYPE_KEYS = [
{ key: 'administrative', label: '行政案件' },
{ key: 'civil', label: '民事案件' },
{ key: 'criminal', label: '刑事案件' },
{ key: 'implement', label: '执行案件' },
{ key: 'preservation', label: '非诉保全审查' },
{ key: 'bankrupt', label: '强制清算与破产案件' },
];
const caseTypeCards = computed(() =>
CASE_TYPE_KEYS.map((item) => ({
...item,
count: extractCaseList(props.data[item.key]).length,
})),
);
const implementCount = computed(() => extractCaseList(props.data.implement).length);
const defendantBarWidth = computed(() => {
const total = Number(count.value.count_beigao) || 0;
const jie = Number(count.value.count_jie_beigao) || 0;
if (!total) return '0%';
return `${Math.round((jie / total) * 100)}%`;
});
const recentYearText = computed(() => {
const stat = count.value.larq_stat || '';
const items = parseStatDistribution(stat);
if (!items.length) return '—';
const latest = items.reduce((a, b) => (a.label > b.label ? a : b));
return `${latest.label}(${latest.count})`;
});
const roleBarRows = computed(() => {
const c = count.value;
const rows = [
{ label: '被告', count: Number(c.count_beigao) || 0 },
{ label: '原告', count: Number(c.count_yuangao) || 0 },
{ label: '第三人', count: Number(c.count_other) || 0 },
];
const max = Math.max(...rows.map((r) => r.count), 1);
return rows.map((r) => ({
...r,
width: `${Math.round((r.count / max) * 100)}%`,
}));
});
const axisTicks = computed(() => {
const max = Math.max(...roleBarRows.value.map((r) => r.count), 2);
const step = max <= 2 ? 0.5 : 1;
const ticks = [];
for (let i = 0; i <= max; i += step) {
ticks.push(Number.isInteger(i) ? i : i.toFixed(1));
}
return ticks.length > 1 ? ticks : [0, 0.5, 1, 1.5, 2, 2.5];
});
const amountCards = computed(() => {
const c = count.value;
return [
{
title: '汇总',
icon: '📊',
total: c.money_total ?? 0,
jie: c.money_jie_total ?? 0,
wei: c.money_wei_total ?? 0,
},
{
title: '被告',
icon: '👤',
total: c.money_beigao ?? 0,
jie: c.money_jie_beigao ?? 0,
wei: c.money_wei_beigao ?? 0,
},
{
title: '原告',
icon: '👤',
total: c.money_yuangao ?? 0,
jie: c.money_jie_yuangao ?? 0,
wei: c.money_wei_yuangao ?? 0,
},
{
title: '第三人',
icon: '👤',
total: c.money_other ?? 0,
jie: c.money_jie_other ?? 0,
wei: c.money_wei_other ?? 0,
},
];
});
const pieCharts = computed(() => {
const c = count.value;
return [
{ title: '地点分布', items: parseStatDistribution(c.area_stat), gradient: '' },
{ title: '案由分布', items: parseStatDistribution(c.ay_stat), gradient: '' },
{ title: '结案分布', items: parseStatDistribution(c.jafs_stat), gradient: '' },
{ title: '时间分布', items: parseStatDistribution(c.larq_stat), gradient: '' },
].map((pie) => ({
...pie,
gradient: buildConicGradient(pie.items),
}));
});
const caseSections = computed(() =>
CASE_TYPE_KEYS.map((section) => ({
...section,
items: extractCaseList(props.data[section.key]),
})).filter((s) => s.items.length > 0),
);
const allCases = computed(() => {
const list = [];
for (const section of caseSections.value) {
for (const c of section.items) {
list.push({ ...c, sectionLabel: section.label });
}
}
return list;
});
const expandedIndex = ref(null);
function toggleCase(index) {
expandedIndex.value = expandedIndex.value === index ? null : index;
}
const dishonestList = computed(() =>
extractJudicialList(props.data, [
'dishonest',
'sxbzxr',
'breachCase',
'disinCases',
'dishonestExecutor',
]),
);
const limitHighList = computed(() =>
extractJudicialList(props.data, [
'limitHigh',
'xgbzxr',
'consumptionRestriction',
'limitCases',
'limitExecutor',
]),
);
const limitHighRows = computed(() => limitHighList.value.map(limitHighRow));
function dishonestText(item) {
const ah = item.c_ah || item.ah || item.caseCode || '—';
const court = item.n_jbfy || item.court || item.zxfy || '';
return court ? `${ah}${court}` : ah;
}
function partiesText(dsrxx) {
if (!Array.isArray(dsrxx)) return '—';
return dsrxx.map((d) => `${d.c_mc}${d.n_dsrlx}`).join('; ');
}
</script>
<style lang="scss" scoped>
.judicial-section {
--blue: #4e74f6;
--red: #ff4d4f;
--border: #e8eef7;
}
.block-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 20px 0 16px;
display: flex;
align-items: center;
gap: 6px;
&::before {
content: '';
display: inline-block;
width: 16px;
height: 16px;
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%234E74F6"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/></svg>') no-repeat center;
background-size: contain;
}
}
.case-overview {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.overview-card {
background: #fff;
border-radius: 8px;
padding: 20px 12px;
text-align: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
position: relative;
border: 1px solid var(--border);
&.criminal {
background: #fff5f5;
border-color: #ffd0d0;
.card-num { color: var(--red); }
}
}
.badge {
position: absolute;
top: 8px;
right: 8px;
width: 16px;
height: 16px;
background: var(--red);
color: #fff;
border-radius: 50%;
font-size: 10px;
display: flex;
align-items: center;
justify-content: center;
}
.card-title {
font-size: 14px;
color: #666;
margin-bottom: 12px;
}
.card-num {
font-size: 28px;
font-weight: 700;
color: var(--blue);
}
.risk-section {
background: #fff;
border-radius: 8px;
padding: 20px;
margin-bottom: 30px;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
border: 1px solid var(--border);
}
.risk-item .label {
font-size: 13px;
color: #666;
margin-bottom: 8px;
line-height: 1.5;
}
.bar-wrap {
width: 100%;
height: 8px;
background: var(--border);
border-radius: 4px;
overflow: hidden;
margin-bottom: 4px;
}
.bar-fill {
height: 100%;
background: var(--blue);
border-radius: 4px;
min-width: 0;
}
.legend {
display: flex;
gap: 12px;
font-size: 12px;
color: #888;
margin-top: 4px;
flex-wrap: wrap;
}
.legend span::before {
content: '';
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 4px;
}
.legend .unresolved::before { background: var(--red); }
.legend .resolved::before { background: var(--blue); }
.total-hint {
text-align: right;
font-size: 12px;
color: #666;
margin-top: 4px;
}
.num-row {
display: flex;
align-items: center;
gap: 6px;
font-size: 16px;
font-weight: 600;
color: #333;
&.red { color: var(--red); }
}
.icon-yen { color: var(--blue); font-size: 18px; font-style: normal; }
.chart-title {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
color: #333;
margin-bottom: 16px;
&::before {
content: '';
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--blue);
border: 2px solid var(--border);
}
}
.bar-chart {
background: #fff;
border-radius: 8px;
padding: 20px;
margin-bottom: 24px;
border: 1px solid var(--border);
position: relative;
}
.chart-legend {
display: flex;
gap: 16px;
flex-wrap: wrap;
margin-bottom: 24px;
}
.chart-legend > span {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
padding: 2px 8px;
border-radius: 4px;
}
.chart-legend .unresolved {
background: #ff4d4f20;
color: var(--red);
}
.chart-legend .resolved {
background: #4e74f620;
color: var(--blue);
}
.chart-legend em {
font-style: normal;
padding: 0 6px;
border-radius: 4px;
color: #fff;
}
.chart-legend .unresolved em { background: var(--red); }
.chart-legend .resolved em { background: var(--blue); }
.bar-rows {
display: flex;
flex-direction: column;
gap: 20px;
}
.bar-row {
display: flex;
align-items: center;
gap: 12px;
}
.row-label {
width: 60px;
font-size: 12px;
color: #666;
text-align: right;
flex-shrink: 0;
}
.row-bar-track {
flex: 1;
height: 8px;
background: var(--border);
border-radius: 4px;
overflow: hidden;
}
.row-bar {
height: 100%;
background: var(--blue);
border-radius: 4px;
min-width: 0;
position: relative;
&::after {
content: '';
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
width: 8px;
height: 8px;
background: var(--blue);
border-radius: 50%;
}
}
.row-value {
width: 24px;
font-size: 12px;
color: #666;
text-align: right;
}
.axis {
display: flex;
justify-content: space-between;
margin-top: 8px;
padding: 0 calc(60px + 36px) 0 calc(60px + 12px);
font-size: 11px;
color: #999;
}
.amount-section {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-bottom: 30px;
}
.amount-card {
background: #fff;
border-radius: 8px;
padding: 16px;
border: 1px solid var(--border);
}
.amount-card .card-header {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 12px;
}
.amount-card .row {
display: flex;
justify-content: space-between;
font-size: 13px;
color: #666;
margin-bottom: 8px;
}
.amount-card .value {
color: #333;
font-weight: 500;
}
.pie-section {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-bottom: 24px;
}
.pie-card {
background: #fff;
border-radius: 8px;
padding: 20px;
border: 1px solid var(--border);
display: flex;
flex-direction: column;
align-items: center;
}
.pie-container {
width: 120px;
height: 120px;
}
.pie-circle {
width: 100%;
height: 100%;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
&.simple { background: var(--blue) !important; }
}
.pie-circle .inner {
width: 80px;
height: 80px;
background: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 500;
color: #333;
text-align: center;
line-height: 1.4;
}
.pie-legend {
margin-top: 12px;
width: 100%;
}
.pie-legend-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: #666;
margin-bottom: 4px;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.case-list-card {
background: #fff;
border-radius: 8px;
margin: 24px 0 20px;
overflow: hidden;
border: 1px solid var(--border);
}
.case-list-card.inner-list {
margin: 0;
border: none;
}
.case-list-group {
border-bottom: 1px solid #e8edf5;
&:last-child { border-bottom: none; }
}
.case-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 20px;
background: #f0f7ff;
border-bottom: 1px solid #e8edf5;
cursor: pointer;
user-select: none;
&::before {
content: '';
display: inline-block;
width: 3px;
height: 16px;
background: #4080ff;
margin-right: 10px;
flex-shrink: 0;
}
&.static { cursor: default; }
&.active { background: #e6f0ff; }
}
.case-text {
flex: 1;
font-size: 14px;
color: #333;
}
.case-arrow {
color: #999;
font-size: 16px;
margin-left: 8px;
}
.case-block {
padding: 16px 20px 16px 33px;
background: #fafbfc;
border-top: 1px dashed #e8edf5;
}
.case-info {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.case-info div {
display: flex;
justify-content: space-between;
padding: 6px 0;
border-bottom: 1px dashed #eee;
gap: 8px;
font-size: 13px;
}
.case-judgment {
margin-top: 10px;
line-height: 1.6;
color: #666;
}
.module-card {
background: #fff;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
border: 1px solid var(--border);
}
.module-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 20px;
display: flex;
align-items: center;
&::before {
content: '';
display: inline-block;
width: 18px;
height: 18px;
margin-right: 8px;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
opacity: 0.7;
}
}
.title-dishonest::before {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23666"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>');
}
.title-high-limit::before {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23666"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-7 14h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>');
}
.empty-data {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 0;
color: #999;
font-size: 14px;
}
.empty-icon {
width: 48px;
height: 48px;
margin-bottom: 10px;
opacity: 0.5;
}
.data-table {
width: 100%;
border-collapse: collapse;
}
.data-table th {
background: #fff9e8;
padding: 12px 8px;
text-align: center;
font-size: 13px;
color: #666;
font-weight: 500;
}
.data-table td {
padding: 12px 8px;
text-align: center;
font-size: 13px;
color: #333;
border-top: 1px solid #f0f0f0;
}
.notice-card {
background: #fff;
border-radius: 8px;
padding: 20px;
border: 1px solid #e0e0e0;
margin-top: 4px;
}
.notice-title {
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 12px;
}
.notice-list {
list-style-type: decimal;
padding-left: 20px;
font-size: 13px;
color: #666;
line-height: 1.8;
}
@media (max-width: 1024px) {
.case-overview { grid-template-columns: repeat(3, 1fr); }
.risk-section { grid-template-columns: repeat(2, 1fr); }
.amount-section,
.pie-section { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 640px) {
.case-overview { grid-template-columns: repeat(2, 1fr); }
.risk-section,
.amount-section,
.pie-section { grid-template-columns: 1fr; }
.data-table {
display: block;
overflow-x: auto;
}
}
</style>

View File

@@ -0,0 +1,278 @@
<template>
<div class="intent-report">
<div class="section-title">借贷意向</div>
<p class="desc-text">
借贷意向数据覆盖大部分的金融机构机构类型包括银行改制机构小贷消费类分期现金类分期代偿类分期和非银其它
</p>
<div class="section-title">最终决策结果</div>
<div class="tab-box">
<div
class="tab-item"
:class="{ active: activeTab === 'decision' }"
@click="activeTab = 'decision'"
>
{{ data.Rule_final_decision || '最终决策结果' }}
</div>
<div
class="tab-item"
:class="{ active: activeTab === 'review' }"
@click="activeTab = 'review'"
>
复议
</div>
</div>
<p v-if="data.Rule_name_QJF045" class="rule-hint">
命中规则{{ data.Rule_name_QJF045 }}
<span v-if="data.Rule_final_weight">权重 {{ data.Rule_final_weight }}</span>
</p>
<!-- 本人在本机构借贷意向表现 -->
<div class="section-title">本人在本机构借贷意向表现</div>
<table class="intent-table">
<thead>
<tr>
<th class="left-align">申请次数</th>
<th v-for="p in LOAN_INTENT_PERIOD_PREFIXES" :key="p.prefix">{{ p.label }}</th>
</tr>
</thead>
<tbody>
<tr v-for="row in LOAN_INTENT_INST_ROWS" :key="row.label">
<td class="left-align">{{ row.label }}</td>
<td v-for="p in LOAN_INTENT_PERIOD_PREFIXES" :key="`${row.label}-${p.prefix}`">
{{ formatAlsCount(data, p.prefix, row.suffix) }}
</td>
</tr>
</tbody>
</table>
<div class="note-text" v-html="LOAN_INTENT_NOTE" />
<!-- 本人在各个客户类型借贷意向表现 -->
<div class="section-title">本人在各个客户类型借贷意向表现</div>
<table class="intent-table">
<thead>
<tr>
<th class="left-align" rowspan="2">客户类型</th>
<th v-for="p in LOAN_INTENT_PERIOD_PREFIXES" :key="'h1-' + p.prefix" colspan="2">{{ p.label }}</th>
</tr>
<tr>
<template v-for="p in LOAN_INTENT_PERIOD_PREFIXES" :key="'h2-' + p.prefix">
<th>机构数</th>
<th>次数</th>
</template>
</tr>
</thead>
<tbody>
<tr
v-for="row in LOAN_INTENT_CUSTOMER_ROWS"
:key="row.label"
:class="{ 'light-bg': row.highlight }"
>
<td class="left-align">{{ row.label }}</td>
<template v-for="p in LOAN_INTENT_PERIOD_PREFIXES" :key="`${row.label}-${p.prefix}`">
<td>{{ formatAlsOrg(data, p.prefix, row.suffix) }}</td>
<td>{{ formatAlsCount(data, p.prefix, row.suffix) }}</td>
</template>
</tr>
</tbody>
</table>
<div class="note-text" v-html="LOAN_INTENT_NOTE" />
<!-- 本人在各个业务类型借贷意向表现 -->
<div class="section-title">本人在各个业务类型借贷意向表现</div>
<table class="intent-table">
<thead>
<tr>
<th class="left-align" rowspan="2">业务类型</th>
<th v-for="p in LOAN_INTENT_PERIOD_PREFIXES" :key="'b1-' + p.prefix" colspan="2">{{ p.label }}</th>
</tr>
<tr>
<template v-for="p in LOAN_INTENT_PERIOD_PREFIXES" :key="'b2-' + p.prefix">
<th>机构数</th>
<th>次数</th>
</template>
</tr>
</thead>
<tbody>
<tr v-for="row in LOAN_INTENT_BUSINESS_ROWS" :key="row.label">
<td class="left-align">{{ row.label }}</td>
<template v-for="p in LOAN_INTENT_PERIOD_PREFIXES" :key="`${row.label}-${p.prefix}`">
<td>{{ formatAlsOrg(data, p.prefix, row.suffix) }}</td>
<td>{{ formatAlsCount(data, p.prefix, row.suffix) }}</td>
</template>
</tr>
</tbody>
</table>
<div class="note-text" v-html="LOAN_INTENT_NOTE" />
<!-- 本人在异常时间段借贷意向表现 -->
<div class="section-title">本人在异常时间段借贷意向表现</div>
<table class="intent-table">
<thead>
<tr>
<th class="left-align" rowspan="2">时间-机构</th>
<th v-for="p in LOAN_INTENT_PERIOD_PREFIXES" :key="'a1-' + p.prefix" colspan="2">{{ p.label }}</th>
</tr>
<tr>
<template v-for="p in LOAN_INTENT_PERIOD_PREFIXES" :key="'a2-' + p.prefix">
<th>机构数</th>
<th>次数</th>
</template>
</tr>
</thead>
<tbody>
<tr v-for="row in LOAN_INTENT_ABNORMAL_ROWS" :key="row.label">
<td class="left-align">{{ row.label }}</td>
<template v-for="p in LOAN_INTENT_PERIOD_PREFIXES" :key="`${row.label}-${p.prefix}`">
<td>{{ formatAlsOrg(data, p.prefix, row.suffix) }}</td>
<td>{{ formatAlsCount(data, p.prefix, row.suffix) }}</td>
</template>
</tr>
</tbody>
</table>
<div class="note-text" v-html="LOAN_INTENT_NOTE" />
</div>
</template>
<script setup>
import { ref } from 'vue';
import {
LOAN_INTENT_PERIOD_PREFIXES,
LOAN_INTENT_INST_ROWS,
LOAN_INTENT_CUSTOMER_ROWS,
LOAN_INTENT_BUSINESS_ROWS,
LOAN_INTENT_ABNORMAL_ROWS,
LOAN_INTENT_NOTE,
formatAlsOrg,
formatAlsCount,
} from '../reportHelper';
defineProps({
data: { type: Object, default: () => ({}) },
});
const activeTab = ref('review');
</script>
<style lang="scss" scoped>
.intent-report {
background: #fff;
border-radius: 8px;
padding: 24px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.section-title {
font-size: 18px;
font-weight: bold;
margin: 30px 0 16px;
padding-left: 10px;
border-left: 4px solid #d4af37;
display: flex;
align-items: center;
gap: 8px;
&:first-child {
margin-top: 0;
}
&::before {
content: '';
display: inline-block;
width: 20px;
height: 16px;
background: linear-gradient(135deg, #d4af37 30%, transparent 30%, transparent 60%, #d4af37 60%);
}
}
.desc-text {
color: #666;
font-size: 15px;
margin: 8px 0 16px;
line-height: 1.6;
padding-left: 14px;
border-left: 2px solid #d4af37;
}
.tab-box {
display: inline-flex;
margin: 10px 0;
}
.tab-item {
padding: 8px 24px;
border: 1px solid #eee;
border-right: none;
background: #f5f7fa;
font-size: 15px;
cursor: pointer;
user-select: none;
&.active {
background: #fff;
font-weight: bold;
}
&:last-child {
border-right: 1px solid #eee;
border-radius: 0 4px 4px 0;
}
&:first-child {
border-radius: 4px 0 0 4px;
}
}
.rule-hint {
font-size: 13px;
color: #d89614;
margin: 4px 0 12px;
padding-left: 14px;
}
.intent-table {
width: 100%;
border-collapse: collapse;
margin: 16px 0;
font-size: 14px;
th,
td {
border: 1px solid #eee;
padding: 12px 8px;
text-align: center;
}
th {
background-color: #fdf8e9;
font-weight: 500;
}
}
.left-align {
text-align: left !important;
}
.light-bg {
background-color: #f0f5ff;
}
.note-text {
font-size: 13px;
color: #666;
margin: 8px 0 16px;
line-height: 1.6;
}
@media (max-width: 768px) {
.intent-report {
padding: 16px;
overflow-x: auto;
}
.intent-table {
min-width: 900px;
}
}
</style>

View File

@@ -0,0 +1,80 @@
<template>
<div class="gamma-card">
<div class="gamma-title"><span>🏦</span> 借贷画像</div>
<div class="loan-profile-grid">
<div v-for="item in items" :key="item.label" class="loan-item">
<div class="loan-icon" :class="item.color">{{ item.icon }}</div>
<div>
<div class="loan-text">{{ item.label }}</div>
<div class="loan-value">{{ item.value }}</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { cellText } from '../reportHelper';
const props = defineProps({
loanTotal: { type: Object, default: () => ({}) },
});
const items = computed(() => {
const d = props.loanTotal || {};
return [
{ label: '机构查询总次数', value: cellText(d.queryCount), icon: '📑', color: 'purple' },
{ label: '借贷机构数2年内', value: cellText(d.orgCount), icon: '🏢', color: 'green' },
{ label: '正常还款订单占比', value: cellText(d.repayRate), icon: '🤲', color: 'orange' },
{ label: '预估网贷授信额度', value: cellText(d.creditLimit), icon: '📄', color: 'pink' },
{ label: '网贷产品数', value: cellText(d.productCount), icon: '📦', color: 'red' },
{ label: '已结清订单数', value: cellText(d.settledCount), icon: '👤', color: 'blue' },
{ label: '借款总金额2年内', value: d.loanAmount != null ? `${d.loanAmount}` : '— 元', icon: '💴', color: 'orange' },
{ label: '逾期总金额2年内', value: cellText(d.overdueAmount), icon: '📄', color: 'pink' },
];
});
</script>
<style lang="scss" scoped>
.loan-profile-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
.loan-item {
background: #f9fafc;
border-radius: 6px;
padding: 16px;
display: flex;
align-items: center;
gap: 10px;
}
.loan-icon {
width: 36px;
height: 36px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 18px;
flex-shrink: 0;
}
.loan-icon.purple { background: #6c5ce7; }
.loan-icon.green { background: #00b894; }
.loan-icon.orange { background: #fdcb6e; }
.loan-icon.pink { background: #e84393; }
.loan-icon.red { background: #e74c3c; }
.loan-icon.blue { background: #3498db; }
.loan-text { font-size: 14px; line-height: 1.4; }
.loan-value { font-size: 16px; font-weight: 600; margin-top: 4px; }
@media (max-width: 768px) {
.loan-profile-grid { grid-template-columns: repeat(2, 1fr); }
}
</style>

View File

@@ -0,0 +1,83 @@
<template>
<div class="gamma-card">
<div class="gamma-title"><span>🚫</span> 逾期黑名单</div>
<div class="gamma-subtitle"><span></span> 命中结果</div>
<div class="blacklist-row">
<div class="blacklist-label">是否命中黑名单</div>
<div class="blacklist-value">{{ isHit(data.black_list) ? '是' : '否' }}</div>
</div>
<div class="gamma-subtitle"><span></span> 疑似标签</div>
<div class="tags-grid">
<div v-for="tag in BLACKLIST_TAGS" :key="tag.key" class="tag-item">
<div class="tag-label">{{ tag.label }}</div>
<div class="tag-value">{{ isHit(data[tag.key]) ? '命中' : '未命中' }}</div>
</div>
</div>
</div>
</template>
<script setup>
import { BLACKLIST_TAGS, isHit } from '../reportHelper';
defineProps({
data: { type: Object, default: () => ({}) },
});
</script>
<style lang="scss" scoped>
.blacklist-row {
display: flex;
align-items: center;
max-width: 400px;
margin-bottom: 16px;
}
.blacklist-label {
width: 160px;
font-size: 14px;
color: #666;
background: #f5f7fa;
padding: 8px 12px;
border-radius: 4px 0 0 4px;
}
.blacklist-value {
flex: 1;
padding: 8px 12px;
border: 1px solid #eee;
border-left: none;
border-radius: 0 4px 4px 0;
text-align: center;
}
.tags-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.tag-item { display: flex; align-items: center; }
.tag-label {
width: 200px;
font-size: 14px;
color: #666;
background: #f9fafc;
padding: 8px 12px;
border-radius: 4px 0 0 4px;
}
.tag-value {
flex: 1;
padding: 8px 12px;
border: 1px solid #eee;
border-left: none;
border-radius: 0 4px 4px 0;
text-align: center;
}
@media (max-width: 768px) {
.tags-grid { grid-template-columns: 1fr; }
.tag-label { width: 140px; }
}
</style>

View File

@@ -0,0 +1,28 @@
<template>
<div class="gamma-card">
<div class="gamma-title"><span>🏠</span> 逾期勘测V3</div>
<div class="gamma-grid">
<div v-for="field in fields" :key="field.key" class="gamma-item">
<label>{{ field.label }}</label>
<span>{{ field.display }}</span>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { OVERDUE_SURVEY_FIELDS, cellText, overdueResultText } from '../reportHelper';
const props = defineProps({
data: { type: Object, default: () => ({}) },
});
const fields = computed(() =>
OVERDUE_SURVEY_FIELDS.map((f) => {
let display = cellText(props.data[f.key]);
if (f.key === 'result_code') display = overdueResultText(props.data[f.key]);
return { ...f, display };
}),
);
</script>

View File

@@ -0,0 +1,108 @@
<template>
<div class="header-section" :style="bannerStyle">
<div class="header-left">
<h1>海宇贷前风险档案</h1>
<p>多维度大数据风险分析</p>
</div>
<div class="header-right">
<div>报告输出时间: {{ displayTime }}</div>
<div>报告版本: V1.0</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import personBanner from '@/assets/images/reporthead/daiqian-banner.png';
const props = defineProps({
reportTime: { type: String, default: '' },
});
const bannerStyle = computed(() => ({
backgroundImage: `url(${personBanner})`,
}));
const displayTime = computed(() => {
if (!props.reportTime) {
return formatDateTime(new Date());
}
const parsed = new Date(props.reportTime);
if (!Number.isNaN(parsed.getTime())) {
return formatDateTime(parsed);
}
return props.reportTime;
});
function formatDateTime(date) {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
const h = String(date.getHours()).padStart(2, '0');
const min = String(date.getMinutes()).padStart(2, '0');
const s = String(date.getSeconds()).padStart(2, '0');
return `${y}-${m}-${d} ${h}:${min}:${s}`;
}
</script>
<style lang="scss" scoped>
.header-section {
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding: 28px 32px;
border-radius: 12px;
overflow: hidden;
aspect-ratio: 8 / 1;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.header-left,
.header-right {
position: relative;
z-index: 1;
}
.header-left h1 {
font-size: 32px;
font-weight: 700;
color: #1a202c;
margin: 0;
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.6);
}
.header-left p {
font-size: 16px;
color: #2d3748;
margin: 8px 0 0;
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.6);
}
.header-right {
text-align: right;
font-size: 12px;
color: #2d3748;
line-height: 1.8;
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.6);
}
@media (max-width: 768px) {
.header-section {
flex-direction: column;
align-items: flex-start;
justify-content: center;
gap: 16px;
padding: 24px 20px;
aspect-ratio: 6 / 1;
}
.header-right {
text-align: left;
width: 100%;
}
}
</style>

Some files were not shown because too many files have changed in this diff Show More