Compare commits

...

32 Commits

Author SHA1 Message Date
Mrx
792fdc2c50 f 2026-06-09 21:27:03 +08:00
Mrx
fbb81449c6 f 2026-06-04 18:13:08 +08:00
Mrx
483fdec6a2 f 2026-06-04 18:04:21 +08:00
c9102f2d51 f 2026-05-13 14:43:38 +08:00
92efdae9eb Merge branch 'main' of http://1.117.67.95:3000/team/qnc-webview-v3 2026-04-30 21:07:28 +08:00
cb079f6da7 add 2026-04-30 21:07:16 +08:00
Mrx
399d2ac365 close date 2026-03-21 11:29:58 +08:00
69a81a927a f 2026-03-02 12:58:07 +08:00
4bbdde51f6 Merge branch 'main' of http://1.117.67.95:3000/team/qnc-webview-v3 2026-02-28 17:30:07 +08:00
97fd7d3a04 add ivyz0s0d 2026-02-28 17:29:22 +08:00
Mrx
e021548adf f 2026-02-28 15:24:44 +08:00
Mrx
7fc01544de f 2026-02-28 14:33:47 +08:00
Mrx
653e1357ac f 2026-02-28 11:36:26 +08:00
Mrx
ca575800ba f 2026-02-28 11:21:11 +08:00
Mrx
2fad9bd3dd f 2026-02-28 11:07:13 +08:00
Mrx
072a258e53 add smsabuse 2026-02-26 10:48:55 +08:00
Mrx
f6ac4c9a50 open 2026-02-14 16:25:07 +08:00
Mrx
7cacd0c567 f 2026-02-14 16:15:58 +08:00
Mrx
eb17fa1b3f f 2026-02-14 13:34:13 +08:00
Mrx
08469ce4dd up ui 2026-02-14 13:31:25 +08:00
Mrx
2ba4e75f45 f 2026-02-13 18:25:21 +08:00
Mrx
e5e86b5da8 f 2026-02-13 18:19:25 +08:00
Mrx
7b225aa54a Merge branch 'main' of http://1.117.67.95:3000/team/qnc-webview-v3 2026-02-09 12:46:09 +08:00
Mrx
a490254abd f login logic 2026-02-09 12:46:04 +08:00
Mrx
8540328bf1 Merge branch 'main' of http://1.117.67.95:3000/team/qnc-webview-v3 2026-02-06 16:12:50 +08:00
Mrx
ae37b0713f f 2026-02-06 16:12:48 +08:00
Mrx
8302626569 f 2026-02-06 15:23:41 +08:00
Mrx
b7a72786de Merge branch 'main' of http://1.117.67.95:3000/team/qnc-webview-v3 2026-02-06 15:20:48 +08:00
Mrx
6bbe4d1fbb Add Bank Card for Withdrawal 2026-02-06 15:20:30 +08:00
743df3665c f 2026-02-02 15:02:01 +08:00
da078d07c1 fix add 2026-01-04 19:18:03 +08:00
2caf2d3c0c f 2026-01-04 17:59:28 +08:00
103 changed files with 27388 additions and 2275 deletions

12
.env
View File

@@ -1,4 +1,6 @@
# VITE_API_URL=https://www.quannengcha.com/
VITE_API_URL=
VITE_API_PREFIX=/api/v1
VITE_COMPANY_NAME=海南海宇大数据有限公司
@@ -9,7 +11,7 @@ VITE_INQUIRE_AES_KEY=ff83609b2b24fc73196aac3d3dfb874f
VITE_WECHAT_APP_ID=wx442ee1ac1ee75917
VITE_ICP_RECORD=琼ICP备2024048057号-2
VITE_PUBLIC_SECURITY_RECORD=琼公网安备46010002000584
VITE_PUBLIC_SECURITY_RECORD=琼公网安备46010002000443
VITE_SHOW_PUBLIC_SECURITY_RECORD=true
VITE_CHAT_AES_KEY=qw5w6SFE2D1jmxyd
@@ -18,4 +20,10 @@ VITE_CHAT_AES_IV=345GDFED433223DF
VITE_SHARE_TITLE=全能查|大数据风险报告查询与代理平台,支持个人和企业多场景风控应用
VITE_SHARE_DESC=提供个人信用评估、入职背调、信贷风控、企业风险监测等服务
VITE_SHARE_IMG=https://www.quannengcha.com/logo.png
VITE_TOKEN_VERSION=1.0
VITE_TOKEN_VERSION=1.0
# 阿里云滑块验证码配置
VITE_ALIYUN_CAPTCHA_SCENE_ID=wynt39to
# 是否启用加密模式true/false需要在阿里云控制台开启加密模式
# 注意:根据代码逻辑,设置为 true 表示禁用加密,设置为 false 表示启用加密
VITE_ALIYUN_CAPTCHA_ENCRYPTED=true

View File

@@ -5,7 +5,7 @@ VITE_COMPANY_NAME=海南海宇大数据有限公司
VITE_APP_NAME=全能查
VITE_ICP_RECORD=琼ICP备2024038584号-10
VITE_PUBLIC_SECURITY_RECORD=琼公网安备46010002000584
VITE_PUBLIC_SECURITY_RECORD=琼公网安备46010002000443
VITE_SHOW_PUBLIC_SECURITY_RECORD=false
VITE_SERVICE_URL=https://work.weixin.qq.com/kfid/kfc82d4424e4b19e5f3

View File

@@ -28,15 +28,15 @@
<!-- 基础SEO信息 -->
<title>
全能查|大数据风险报告查询与代理平台,支持个人和企业多场景风控应用
全能查官网_个人婚姻状态报告_综合风险排查工具箱
</title>
<meta
name="description"
content="全能查,专业大数据风险报告查询与代理平台,支持个人信用查询、小微企业风控、贷前风险背调等多场景报告应用,免费开通代理权限,助力高效识别信用与风险。"
content="全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。"
/>
<meta
name="keywords"
content="大数据风险报告查询、大数据风险评估、大数据分析报告、个人大数据风险查询、小微企业风险、贷前风险背调、代理管理平台、免费开通代理、风险管控平台、信用风险分析、企业风险报告、贷前信用审核、失信人名单查询、被执行人信息、信用黑名单查询"
content="全能查,婚姻状态核实,风险排查工具,个人风险预警,第三方背调,商业信用评估"
/>
<meta name="author" content="全能查" />
<meta name="robots" content="index, follow" />
@@ -45,28 +45,28 @@
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://www.zhinengcha.cn/" />
<meta property="og:url" content="https://www.quannengcha.com/" />
<meta
property="og:title"
content="全能查|大数据风险报告查询与代理平台,支持个人和企业多场景风控应用"
content="全能查官网_个人婚姻状态报告_综合风险排查工具箱"
/>
<meta
property="og:description"
content="全能查,专业大数据风险报告查询与代理平台,支持个人信用查询、小微企业风控、贷前风险背调等多场景报告应用,免费开通代理权限,助力高效识别信用与风险。"
content="全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。"
/>
<meta property="og:site_name" content="全能查" />
<meta property="og:locale" content="zh_CN" />
<!-- Twitter -->
<meta property="twitter:card" content="summary" />
<meta property="twitter:url" content="https://www.zhinengcha.cn/" />
<meta property="twitter:url" content="https://www.quannengcha.com/" />
<meta
property="twitter:title"
content="全能查|大数据风险报告查询与代理平台,支持个人和企业多场景风控应用"
content="全能查官网_个人婚姻状态报告_综合风险排查工具箱"
/>
<meta
property="twitter:description"
content="全能查,专业大数据风险报告查询与代理平台,支持个人信用查询、小微企业风控、贷前风险背调等多场景报告应用,免费开通代理权限,助力高效识别信用与风险。"
content="全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。"
/>
<!-- 其他重要meta标签 -->
@@ -83,11 +83,11 @@
"@context": "https://schema.org",
"@type": "WebSite",
"name": "全能查",
"url": "https://www.zhinengcha.cn/",
"url": "https://www.quannengcha.com/",
"description": "专业大数据风险报告查询与代理平台,支持个人信用查询、小微企业风控、贷前风险背调等多场景报告应用",
"potentialAction": {
"@type": "SearchAction",
"target": "https://www.zhinengcha.cn/search?q={search_term_string}",
"target": "https://www.quannengcha.com/search?q={search_term_string}",
"query-input": "required name=search_term_string"
}
}
@@ -98,7 +98,7 @@
"@context": "https://schema.org",
"@type": "Organization",
"name": "全能查",
"url": "https://www.zhinengcha.cn/",
"url": "https://www.quannengcha.com/",
"description": "专业大数据风险报告查询与代理平台,支持个人和企业多场景风控应用"
}
</script>
@@ -109,10 +109,19 @@
delete window.wx;
</script>
<!-- 阿里云滑块验证码 -->
<script>
window.AliyunCaptchaConfig = { region: "cn", prefix: "12zxnj" };
</script>
<script
type="text/javascript"
src="https://o.alicdn.com/captcha-frontend/aliyunCaptcha/AliyunCaptcha.js"
></script>
<!-- 预加载关键资源 -->
<link rel="preconnect" href="https://www.zhinengcha.cn" />
<link rel="preconnect" href="https://www.quannengcha.com" />
<link rel="preconnect" href="https://res.wx.qq.com" />
<link rel="dns-prefetch" href="https://www.zhinengcha.cn" />
<link rel="dns-prefetch" href="https://www.quannengcha.com" />
<link rel="dns-prefetch" href="https://res.wx.qq.com" />
<style>
@@ -207,6 +216,8 @@
<div class="loading-text">加载中</div>
</div>
<div id="app"></div>
<!-- 阿里云滑块验证码挂载容器 -->
<div id="captcha-element"></div>
<script type="module" src="/src/main.js"></script>
</body>

View File

@@ -44,7 +44,7 @@ Disallow: /js/
Disallow: /css/
# 网站地图
Sitemap: https://www.zhinengcha.cn/sitemap.xml
Sitemap: https://www.quannengcha.com/sitemap.xml
# 爬取延迟(毫秒)
Crawl-delay: 1

View File

@@ -0,0 +1,80 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>全能查合作政策指南_合作伙伴权益与结算说明_官方文档</title>
<meta name="description" content="全能查官方合作体系说明文档。详细解读合作伙伴的等级权益、服务费结算标准及晋升机制。致力于构建公平、透明的商业合作生态,助力合作伙伴快速上手业务。">
<meta name="keywords" content="合作伙伴政策,服务费结算,渠道等级说明,业务操作指南,代理系统后台">
<meta property="og:title" content="全能查合作政策指南_合作伙伴权益与结算说明_官方文档">
<meta property="og:description" content="全能查官方合作体系说明文档。详细解读合作伙伴的等级权益、服务费结算标准及晋升机制。致力于构建公平、透明的商业合作生态,助力合作伙伴快速上手业务。">
<meta property="og:url" content="https://www.quannengcha.com/agent/system-guide">
<meta property="og:type" content="website">
<meta property="og:site_name" content="全能查">
<meta property="og:locale" content="zh_CN">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="全能查合作政策指南_合作伙伴权益与结算说明_官方文档">
<meta name="twitter:description" content="全能查官方合作体系说明文档。详细解读合作伙伴的等级权益、服务费结算标准及晋升机制。致力于构建公平、透明的商业合作生态,助力合作伙伴快速上手业务。">
<meta name="twitter:url" content="https://www.quannengcha.com/agent/system-guide">
<link rel="canonical" href="https://www.quannengcha.com/agent/system-guide">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "全能查合作政策指南_合作伙伴权益与结算说明_官方文档",
"description": "全能查官方合作体系说明文档。详细解读合作伙伴的等级权益、服务费结算标准及晋升机制。致力于构建公平、透明的商业合作生态,助力合作伙伴快速上手业务。",
"url": "https://www.quannengcha.com/agent/system-guide",
"mainEntity": {
"@type": "Organization",
"name": "全能查",
"url": "https://www.quannengcha.com/",
"description": "专业大数据风险报告查询与代理平台,支持个人和企业多场景风控应用"
}
}
</script>
<meta name="robots" content="index, follow">
<meta name="googlebot" content="index, follow">
<meta name="baiduspider" content="index, follow">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; margin: 0; padding: 0; line-height: 1.6; }
.seo-content { max-width: 1200px; margin: 0 auto; padding: 20px; }
h1 { color: #333; }
p { color: #666; }
.redirect-notice { background: #fff3cd; border: 1px solid #ffc107; color: #856404; padding: 10px; margin: 20px 0; border-radius: 4px; }
</style>
</head>
<body>
<div class="seo-content">
<h1>全能查合作政策指南_合作伙伴权益与结算说明_官方文档</h1>
<div class="redirect-notice">
<p>正在跳转到完整版网站...</p>
<p>如果浏览器没有自动跳转,请 <a href="https://www.quannengcha.com/agent/system-guide">点击这里</a></p>
</div>
<p>全能查官方合作体系说明文档。详细解读合作伙伴的等级权益、服务费结算标准及晋升机制。致力于构建公平、透明的商业合作生态,助力合作伙伴快速上手业务。</p>
<section>
<h2>关于全能查</h2>
<p>全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。</p>
</section>
<section>
<h2>核心服务</h2>
<ul>
<li>个人综合风险分析与履约能力画像</li>
<li>企业工商信用画像与商业风险透视</li>
<li>婚前综合背景了解与情感安全风险评估</li>
<li>职场背景核验与入职背调</li>
<li>家政人员背景核实与安全评估</li>
<li>个人履约能力评估与经济风险分析</li>
</ul>
</section>
</div>
</body>
</html>

View File

@@ -0,0 +1,80 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>全能查代理 - 免费开通代理权限 | 大数据风险报告代理</title>
<meta name="description" content="全能查代理平台,免费开通代理权限,享受大数据风险报告查询服务代理收益。专业的大数据风险报告、婚姻查询、个人信用评估等服务的代理合作。">
<meta name="keywords" content="全能查代理, 免费代理, 大数据风险报告代理, 代理权限, 代理收益">
<meta property="og:title" content="全能查代理 - 免费开通代理权限 | 大数据风险报告代理">
<meta property="og:description" content="全能查代理平台,免费开通代理权限,享受大数据风险报告查询服务代理收益。专业的大数据风险报告、婚姻查询、个人信用评估等服务的代理合作。">
<meta property="og:url" content="https://www.quannengcha.com/agent">
<meta property="og:type" content="website">
<meta property="og:site_name" content="全能查">
<meta property="og:locale" content="zh_CN">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="全能查代理 - 免费开通代理权限 | 大数据风险报告代理">
<meta name="twitter:description" content="全能查代理平台,免费开通代理权限,享受大数据风险报告查询服务代理收益。专业的大数据风险报告、婚姻查询、个人信用评估等服务的代理合作。">
<meta name="twitter:url" content="https://www.quannengcha.com/agent">
<link rel="canonical" href="https://www.quannengcha.com/agent">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "全能查代理 - 免费开通代理权限 | 大数据风险报告代理",
"description": "全能查代理平台,免费开通代理权限,享受大数据风险报告查询服务代理收益。专业的大数据风险报告、婚姻查询、个人信用评估等服务的代理合作。",
"url": "https://www.quannengcha.com/agent",
"mainEntity": {
"@type": "Organization",
"name": "全能查",
"url": "https://www.quannengcha.com/",
"description": "专业大数据风险报告查询与代理平台,支持个人和企业多场景风控应用"
}
}
</script>
<meta name="robots" content="index, follow">
<meta name="googlebot" content="index, follow">
<meta name="baiduspider" content="index, follow">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; margin: 0; padding: 0; line-height: 1.6; }
.seo-content { max-width: 1200px; margin: 0 auto; padding: 20px; }
h1 { color: #333; }
p { color: #666; }
.redirect-notice { background: #fff3cd; border: 1px solid #ffc107; color: #856404; padding: 10px; margin: 20px 0; border-radius: 4px; }
</style>
</head>
<body>
<div class="seo-content">
<h1>全能查代理 - 免费开通代理权限 | 大数据风险报告代理</h1>
<div class="redirect-notice">
<p>正在跳转到完整版网站...</p>
<p>如果浏览器没有自动跳转,请 <a href="https://www.quannengcha.com/agent">点击这里</a></p>
</div>
<p>全能查代理平台,免费开通代理权限,享受大数据风险报告查询服务代理收益。专业的大数据风险报告、婚姻查询、个人信用评估等服务的代理合作。</p>
<section>
<h2>关于全能查</h2>
<p>全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。</p>
</section>
<section>
<h2>核心服务</h2>
<ul>
<li>个人综合风险分析与履约能力画像</li>
<li>企业工商信用画像与商业风险透视</li>
<li>婚前综合背景了解与情感安全风险评估</li>
<li>职场背景核验与入职背调</li>
<li>家政人员背景核实与安全评估</li>
<li>个人履约能力评估与经济风险分析</li>
</ul>
</section>
</div>
</body>
</html>

View File

@@ -0,0 +1,80 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>示例报告 - 全能查报告展示 | 大数据风险报告样例</title>
<meta name="description" content="全能查示例报告展示,包含大数据风险报告、婚姻状况查询、个人信用评估等服务的报告样例,让用户了解报告内容和格式。">
<meta name="keywords" content="示例报告, 报告展示, 报告样例, 大数据风险报告, 婚姻查询报告">
<meta property="og:title" content="示例报告 - 全能查报告展示 | 大数据风险报告样例">
<meta property="og:description" content="全能查示例报告展示,包含大数据风险报告、婚姻状况查询、个人信用评估等服务的报告样例,让用户了解报告内容和格式。">
<meta property="og:url" content="https://www.quannengcha.com/example">
<meta property="og:type" content="website">
<meta property="og:site_name" content="全能查">
<meta property="og:locale" content="zh_CN">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="示例报告 - 全能查报告展示 | 大数据风险报告样例">
<meta name="twitter:description" content="全能查示例报告展示,包含大数据风险报告、婚姻状况查询、个人信用评估等服务的报告样例,让用户了解报告内容和格式。">
<meta name="twitter:url" content="https://www.quannengcha.com/example">
<link rel="canonical" href="https://www.quannengcha.com/example">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "示例报告 - 全能查报告展示 | 大数据风险报告样例",
"description": "全能查示例报告展示,包含大数据风险报告、婚姻状况查询、个人信用评估等服务的报告样例,让用户了解报告内容和格式。",
"url": "https://www.quannengcha.com/example",
"mainEntity": {
"@type": "Organization",
"name": "全能查",
"url": "https://www.quannengcha.com/",
"description": "专业大数据风险报告查询与代理平台,支持个人和企业多场景风控应用"
}
}
</script>
<meta name="robots" content="index, follow">
<meta name="googlebot" content="index, follow">
<meta name="baiduspider" content="index, follow">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; margin: 0; padding: 0; line-height: 1.6; }
.seo-content { max-width: 1200px; margin: 0 auto; padding: 20px; }
h1 { color: #333; }
p { color: #666; }
.redirect-notice { background: #fff3cd; border: 1px solid #ffc107; color: #856404; padding: 10px; margin: 20px 0; border-radius: 4px; }
</style>
</head>
<body>
<div class="seo-content">
<h1>示例报告 - 全能查报告展示 | 大数据风险报告样例</h1>
<div class="redirect-notice">
<p>正在跳转到完整版网站...</p>
<p>如果浏览器没有自动跳转,请 <a href="https://www.quannengcha.com/example">点击这里</a></p>
</div>
<p>全能查示例报告展示,包含大数据风险报告、婚姻状况查询、个人信用评估等服务的报告样例,让用户了解报告内容和格式。</p>
<section>
<h2>关于全能查</h2>
<p>全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。</p>
</section>
<section>
<h2>核心服务</h2>
<ul>
<li>个人综合风险分析与履约能力画像</li>
<li>企业工商信用画像与商业风险透视</li>
<li>婚前综合背景了解与情感安全风险评估</li>
<li>职场背景核验与入职背调</li>
<li>家政人员背景核实与安全评估</li>
<li>个人履约能力评估与经济风险分析</li>
</ul>
</section>
</div>
</body>
</html>

View File

@@ -0,0 +1,80 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>使用指南 - 全能查操作教程 | 功能说明</title>
<meta name="description" content="全能查详细使用指南,包含各功能模块的操作教程、功能说明、注意事项等,让用户快速上手使用。">
<meta name="keywords" content="使用指南, 操作教程, 功能说明, 快速上手, 全能查教程">
<meta property="og:title" content="使用指南 - 全能查操作教程 | 功能说明">
<meta property="og:description" content="全能查详细使用指南,包含各功能模块的操作教程、功能说明、注意事项等,让用户快速上手使用。">
<meta property="og:url" content="https://www.quannengcha.com/help/guide">
<meta property="og:type" content="website">
<meta property="og:site_name" content="全能查">
<meta property="og:locale" content="zh_CN">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="使用指南 - 全能查操作教程 | 功能说明">
<meta name="twitter:description" content="全能查详细使用指南,包含各功能模块的操作教程、功能说明、注意事项等,让用户快速上手使用。">
<meta name="twitter:url" content="https://www.quannengcha.com/help/guide">
<link rel="canonical" href="https://www.quannengcha.com/help/guide">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "使用指南 - 全能查操作教程 | 功能说明",
"description": "全能查详细使用指南,包含各功能模块的操作教程、功能说明、注意事项等,让用户快速上手使用。",
"url": "https://www.quannengcha.com/help/guide",
"mainEntity": {
"@type": "Organization",
"name": "全能查",
"url": "https://www.quannengcha.com/",
"description": "专业大数据风险报告查询与代理平台,支持个人和企业多场景风控应用"
}
}
</script>
<meta name="robots" content="index, follow">
<meta name="googlebot" content="index, follow">
<meta name="baiduspider" content="index, follow">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; margin: 0; padding: 0; line-height: 1.6; }
.seo-content { max-width: 1200px; margin: 0 auto; padding: 20px; }
h1 { color: #333; }
p { color: #666; }
.redirect-notice { background: #fff3cd; border: 1px solid #ffc107; color: #856404; padding: 10px; margin: 20px 0; border-radius: 4px; }
</style>
</head>
<body>
<div class="seo-content">
<h1>使用指南 - 全能查操作教程 | 功能说明</h1>
<div class="redirect-notice">
<p>正在跳转到完整版网站...</p>
<p>如果浏览器没有自动跳转,请 <a href="https://www.quannengcha.com/help/guide">点击这里</a></p>
</div>
<p>全能查详细使用指南,包含各功能模块的操作教程、功能说明、注意事项等,让用户快速上手使用。</p>
<section>
<h2>关于全能查</h2>
<p>全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。</p>
</section>
<section>
<h2>核心服务</h2>
<ul>
<li>个人综合风险分析与履约能力画像</li>
<li>企业工商信用画像与商业风险透视</li>
<li>婚前综合背景了解与情感安全风险评估</li>
<li>职场背景核验与入职背调</li>
<li>家政人员背景核实与安全评估</li>
<li>个人履约能力评估与经济风险分析</li>
</ul>
</section>
</div>
</body>
</html>

View File

@@ -0,0 +1,80 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>帮助中心 - 全能查使用指南 | 常见问题解答</title>
<meta name="description" content="全能查帮助中心,提供详细的使用指南、常见问题解答、操作教程等,帮助用户更好地使用大数据风险报告查询服务。">
<meta name="keywords" content="全能查帮助, 使用指南, 常见问题, 操作教程, 客服支持">
<meta property="og:title" content="帮助中心 - 全能查使用指南 | 常见问题解答">
<meta property="og:description" content="全能查帮助中心,提供详细的使用指南、常见问题解答、操作教程等,帮助用户更好地使用大数据风险报告查询服务。">
<meta property="og:url" content="https://www.quannengcha.com/help">
<meta property="og:type" content="website">
<meta property="og:site_name" content="全能查">
<meta property="og:locale" content="zh_CN">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="帮助中心 - 全能查使用指南 | 常见问题解答">
<meta name="twitter:description" content="全能查帮助中心,提供详细的使用指南、常见问题解答、操作教程等,帮助用户更好地使用大数据风险报告查询服务。">
<meta name="twitter:url" content="https://www.quannengcha.com/help">
<link rel="canonical" href="https://www.quannengcha.com/help">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "帮助中心 - 全能查使用指南 | 常见问题解答",
"description": "全能查帮助中心,提供详细的使用指南、常见问题解答、操作教程等,帮助用户更好地使用大数据风险报告查询服务。",
"url": "https://www.quannengcha.com/help",
"mainEntity": {
"@type": "Organization",
"name": "全能查",
"url": "https://www.quannengcha.com/",
"description": "专业大数据风险报告查询与代理平台,支持个人和企业多场景风控应用"
}
}
</script>
<meta name="robots" content="index, follow">
<meta name="googlebot" content="index, follow">
<meta name="baiduspider" content="index, follow">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; margin: 0; padding: 0; line-height: 1.6; }
.seo-content { max-width: 1200px; margin: 0 auto; padding: 20px; }
h1 { color: #333; }
p { color: #666; }
.redirect-notice { background: #fff3cd; border: 1px solid #ffc107; color: #856404; padding: 10px; margin: 20px 0; border-radius: 4px; }
</style>
</head>
<body>
<div class="seo-content">
<h1>帮助中心 - 全能查使用指南 | 常见问题解答</h1>
<div class="redirect-notice">
<p>正在跳转到完整版网站...</p>
<p>如果浏览器没有自动跳转,请 <a href="https://www.quannengcha.com/help">点击这里</a></p>
</div>
<p>全能查帮助中心,提供详细的使用指南、常见问题解答、操作教程等,帮助用户更好地使用大数据风险报告查询服务。</p>
<section>
<h2>关于全能查</h2>
<p>全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。</p>
</section>
<section>
<h2>核心服务</h2>
<ul>
<li>个人综合风险分析与履约能力画像</li>
<li>企业工商信用画像与商业风险透视</li>
<li>婚前综合背景了解与情感安全风险评估</li>
<li>职场背景核验与入职背调</li>
<li>家政人员背景核实与安全评估</li>
<li>个人履约能力评估与经济风险分析</li>
</ul>
</section>
</div>
</body>
</html>

View File

@@ -0,0 +1,80 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>全能查官网_个人婚姻状态报告_综合风险排查工具箱</title>
<meta name="description" content="全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。">
<meta name="keywords" content="全能查,婚姻状态核实,风险排查工具,个人风险预警,第三方背调,商业信用评估">
<meta property="og:title" content="全能查官网_个人婚姻状态报告_综合风险排查工具箱">
<meta property="og:description" content="全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。">
<meta property="og:url" content="https://www.quannengcha.com">
<meta property="og:type" content="website">
<meta property="og:site_name" content="全能查">
<meta property="og:locale" content="zh_CN">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="全能查官网_个人婚姻状态报告_综合风险排查工具箱">
<meta name="twitter:description" content="全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。">
<meta name="twitter:url" content="https://www.quannengcha.com">
<link rel="canonical" href="https://www.quannengcha.com">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "全能查官网_个人婚姻状态报告_综合风险排查工具箱",
"description": "全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。",
"url": "https://www.quannengcha.com",
"mainEntity": {
"@type": "Organization",
"name": "全能查",
"url": "https://www.quannengcha.com/",
"description": "专业大数据风险报告查询与代理平台,支持个人和企业多场景风控应用"
}
}
</script>
<meta name="robots" content="index, follow">
<meta name="googlebot" content="index, follow">
<meta name="baiduspider" content="index, follow">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; margin: 0; padding: 0; line-height: 1.6; }
.seo-content { max-width: 1200px; margin: 0 auto; padding: 20px; }
h1 { color: #333; }
p { color: #666; }
.redirect-notice { background: #fff3cd; border: 1px solid #ffc107; color: #856404; padding: 10px; margin: 20px 0; border-radius: 4px; }
</style>
</head>
<body>
<div class="seo-content">
<h1>全能查官网_个人婚姻状态报告_综合风险排查工具箱</h1>
<div class="redirect-notice">
<p>正在跳转到完整版网站...</p>
<p>如果浏览器没有自动跳转,请 <a href="https://www.quannengcha.com">点击这里</a></p>
</div>
<p>全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。</p>
<section>
<h2>关于全能查</h2>
<p>全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。</p>
</section>
<section>
<h2>核心服务</h2>
<ul>
<li>个人综合风险分析与履约能力画像</li>
<li>企业工商信用画像与商业风险透视</li>
<li>婚前综合背景了解与情感安全风险评估</li>
<li>职场背景核验与入职背调</li>
<li>家政人员背景核实与安全评估</li>
<li>个人履约能力评估与经济风险分析</li>
</ul>
</section>
</div>
</body>
</html>

View File

@@ -0,0 +1,80 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>职场背景核验报告_候选人职业风险与竞业核验_全能查</title>
<meta name="description" content="全能查为企业提供专业的入职背调服务。一键筛查候选人的学历背景、涉及的商业利益冲突、劳动仲裁记录及社会不良风险。数据实时合规,降低企业用工试错成本,提升招聘决策效率。">
<meta name="keywords" content="员工入职背调,职业背景核实,竞业限制评估,职场信用报告,候选人风险筛查">
<meta property="og:title" content="职场背景核验报告_候选人职业风险与竞业核验_全能查">
<meta property="og:description" content="全能查为企业提供专业的入职背调服务。一键筛查候选人的学历背景、涉及的商业利益冲突、劳动仲裁记录及社会不良风险。数据实时合规,降低企业用工试错成本,提升招聘决策效率。">
<meta property="og:url" content="https://www.quannengcha.com/inquire/backgroundcheck">
<meta property="og:type" content="website">
<meta property="og:site_name" content="全能查">
<meta property="og:locale" content="zh_CN">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="职场背景核验报告_候选人职业风险与竞业核验_全能查">
<meta name="twitter:description" content="全能查为企业提供专业的入职背调服务。一键筛查候选人的学历背景、涉及的商业利益冲突、劳动仲裁记录及社会不良风险。数据实时合规,降低企业用工试错成本,提升招聘决策效率。">
<meta name="twitter:url" content="https://www.quannengcha.com/inquire/backgroundcheck">
<link rel="canonical" href="https://www.quannengcha.com/inquire/backgroundcheck">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "职场背景核验报告_候选人职业风险与竞业核验_全能查",
"description": "全能查为企业提供专业的入职背调服务。一键筛查候选人的学历背景、涉及的商业利益冲突、劳动仲裁记录及社会不良风险。数据实时合规,降低企业用工试错成本,提升招聘决策效率。",
"url": "https://www.quannengcha.com/inquire/backgroundcheck",
"mainEntity": {
"@type": "Organization",
"name": "全能查",
"url": "https://www.quannengcha.com/",
"description": "专业大数据风险报告查询与代理平台,支持个人和企业多场景风控应用"
}
}
</script>
<meta name="robots" content="index, follow">
<meta name="googlebot" content="index, follow">
<meta name="baiduspider" content="index, follow">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; margin: 0; padding: 0; line-height: 1.6; }
.seo-content { max-width: 1200px; margin: 0 auto; padding: 20px; }
h1 { color: #333; }
p { color: #666; }
.redirect-notice { background: #fff3cd; border: 1px solid #ffc107; color: #856404; padding: 10px; margin: 20px 0; border-radius: 4px; }
</style>
</head>
<body>
<div class="seo-content">
<h1>职场背景核验报告_候选人职业风险与竞业核验_全能查</h1>
<div class="redirect-notice">
<p>正在跳转到完整版网站...</p>
<p>如果浏览器没有自动跳转,请 <a href="https://www.quannengcha.com/inquire/backgroundcheck">点击这里</a></p>
</div>
<p>全能查为企业提供专业的入职背调服务。一键筛查候选人的学历背景、涉及的商业利益冲突、劳动仲裁记录及社会不良风险。数据实时合规,降低企业用工试错成本,提升招聘决策效率。</p>
<section>
<h2>关于全能查</h2>
<p>全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。</p>
</section>
<section>
<h2>核心服务</h2>
<ul>
<li>个人综合风险分析与履约能力画像</li>
<li>企业工商信用画像与商业风险透视</li>
<li>婚前综合背景了解与情感安全风险评估</li>
<li>职场背景核验与入职背调</li>
<li>家政人员背景核实与安全评估</li>
<li>个人履约能力评估与经济风险分析</li>
</ul>
</section>
</div>
</body>
</html>

View File

@@ -0,0 +1,80 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>企业工商信用画像_经营异常与商业风险透视_全能查</title>
<meta name="description" content="全能查企业版深度透视商业真相。聚合工商、司法及税务公开数据,核验企业经营异常名录、行政处罚、法律诉讼及股权穿透信息。全方位评估合作伙伴的商业健康度,规避合同违约风险。">
<meta name="keywords" content="企业信用评估,工商背景核验,商业风险评估,公司经营异常,合作方背景核实">
<meta property="og:title" content="企业工商信用画像_经营异常与商业风险透视_全能查">
<meta property="og:description" content="全能查企业版深度透视商业真相。聚合工商、司法及税务公开数据,核验企业经营异常名录、行政处罚、法律诉讼及股权穿透信息。全方位评估合作伙伴的商业健康度,规避合同违约风险。">
<meta property="og:url" content="https://www.quannengcha.com/inquire/companyinfo">
<meta property="og:type" content="website">
<meta property="og:site_name" content="全能查">
<meta property="og:locale" content="zh_CN">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="企业工商信用画像_经营异常与商业风险透视_全能查">
<meta name="twitter:description" content="全能查企业版深度透视商业真相。聚合工商、司法及税务公开数据,核验企业经营异常名录、行政处罚、法律诉讼及股权穿透信息。全方位评估合作伙伴的商业健康度,规避合同违约风险。">
<meta name="twitter:url" content="https://www.quannengcha.com/inquire/companyinfo">
<link rel="canonical" href="https://www.quannengcha.com/inquire/companyinfo">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "企业工商信用画像_经营异常与商业风险透视_全能查",
"description": "全能查企业版深度透视商业真相。聚合工商、司法及税务公开数据,核验企业经营异常名录、行政处罚、法律诉讼及股权穿透信息。全方位评估合作伙伴的商业健康度,规避合同违约风险。",
"url": "https://www.quannengcha.com/inquire/companyinfo",
"mainEntity": {
"@type": "Organization",
"name": "全能查",
"url": "https://www.quannengcha.com/",
"description": "专业大数据风险报告查询与代理平台,支持个人和企业多场景风控应用"
}
}
</script>
<meta name="robots" content="index, follow">
<meta name="googlebot" content="index, follow">
<meta name="baiduspider" content="index, follow">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; margin: 0; padding: 0; line-height: 1.6; }
.seo-content { max-width: 1200px; margin: 0 auto; padding: 20px; }
h1 { color: #333; }
p { color: #666; }
.redirect-notice { background: #fff3cd; border: 1px solid #ffc107; color: #856404; padding: 10px; margin: 20px 0; border-radius: 4px; }
</style>
</head>
<body>
<div class="seo-content">
<h1>企业工商信用画像_经营异常与商业风险透视_全能查</h1>
<div class="redirect-notice">
<p>正在跳转到完整版网站...</p>
<p>如果浏览器没有自动跳转,请 <a href="https://www.quannengcha.com/inquire/companyinfo">点击这里</a></p>
</div>
<p>全能查企业版深度透视商业真相。聚合工商、司法及税务公开数据,核验企业经营异常名录、行政处罚、法律诉讼及股权穿透信息。全方位评估合作伙伴的商业健康度,规避合同违约风险。</p>
<section>
<h2>关于全能查</h2>
<p>全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。</p>
</section>
<section>
<h2>核心服务</h2>
<ul>
<li>个人综合风险分析与履约能力画像</li>
<li>企业工商信用画像与商业风险透视</li>
<li>婚前综合背景了解与情感安全风险评估</li>
<li>职场背景核验与入职背调</li>
<li>家政人员背景核实与安全评估</li>
<li>个人履约能力评估与经济风险分析</li>
</ul>
</section>
</div>
</body>
</html>

View File

@@ -0,0 +1,80 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>个人履约能力评估_经济风险与收支压力参考_全能查</title>
<meta name="description" content="全能查履约报告基于大数据算法,提供个人经济稳定性的客观分析。多维度检测综合履约分、经济关联风险及潜在的资金压力指数。本服务仅提供大数据层面的风险参考,助您优化财务管理。">
<meta name="keywords" content="履约能力评估,经济风险指数,综合评分波动,资金压力分析,财务健康度">
<meta property="og:title" content="个人履约能力评估_经济风险与收支压力参考_全能查">
<meta property="og:description" content="全能查履约报告基于大数据算法,提供个人经济稳定性的客观分析。多维度检测综合履约分、经济关联风险及潜在的资金压力指数。本服务仅提供大数据层面的风险参考,助您优化财务管理。">
<meta property="og:url" content="https://www.quannengcha.com/inquire/consumerFinanceReport">
<meta property="og:type" content="website">
<meta property="og:site_name" content="全能查">
<meta property="og:locale" content="zh_CN">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="个人履约能力评估_经济风险与收支压力参考_全能查">
<meta name="twitter:description" content="全能查履约报告基于大数据算法,提供个人经济稳定性的客观分析。多维度检测综合履约分、经济关联风险及潜在的资金压力指数。本服务仅提供大数据层面的风险参考,助您优化财务管理。">
<meta name="twitter:url" content="https://www.quannengcha.com/inquire/consumerFinanceReport">
<link rel="canonical" href="https://www.quannengcha.com/inquire/consumerFinanceReport">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "个人履约能力评估_经济风险与收支压力参考_全能查",
"description": "全能查履约报告基于大数据算法,提供个人经济稳定性的客观分析。多维度检测综合履约分、经济关联风险及潜在的资金压力指数。本服务仅提供大数据层面的风险参考,助您优化财务管理。",
"url": "https://www.quannengcha.com/inquire/consumerFinanceReport",
"mainEntity": {
"@type": "Organization",
"name": "全能查",
"url": "https://www.quannengcha.com/",
"description": "专业大数据风险报告查询与代理平台,支持个人和企业多场景风控应用"
}
}
</script>
<meta name="robots" content="index, follow">
<meta name="googlebot" content="index, follow">
<meta name="baiduspider" content="index, follow">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; margin: 0; padding: 0; line-height: 1.6; }
.seo-content { max-width: 1200px; margin: 0 auto; padding: 20px; }
h1 { color: #333; }
p { color: #666; }
.redirect-notice { background: #fff3cd; border: 1px solid #ffc107; color: #856404; padding: 10px; margin: 20px 0; border-radius: 4px; }
</style>
</head>
<body>
<div class="seo-content">
<h1>个人履约能力评估_经济风险与收支压力参考_全能查</h1>
<div class="redirect-notice">
<p>正在跳转到完整版网站...</p>
<p>如果浏览器没有自动跳转,请 <a href="https://www.quannengcha.com/inquire/consumerFinanceReport">点击这里</a></p>
</div>
<p>全能查履约报告基于大数据算法,提供个人经济稳定性的客观分析。多维度检测综合履约分、经济关联风险及潜在的资金压力指数。本服务仅提供大数据层面的风险参考,助您优化财务管理。</p>
<section>
<h2>关于全能查</h2>
<p>全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。</p>
</section>
<section>
<h2>核心服务</h2>
<ul>
<li>个人综合风险分析与履约能力画像</li>
<li>企业工商信用画像与商业风险透视</li>
<li>婚前综合背景了解与情感安全风险评估</li>
<li>职场背景核验与入职背调</li>
<li>家政人员背景核实与安全评估</li>
<li>个人履约能力评估与经济风险分析</li>
</ul>
</section>
</div>
</body>
</html>

View File

@@ -0,0 +1,80 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>家政人员背景核实_保姆月嫂司法安全评估_全能查</title>
<meta name="description" content="全能查针对家庭用工场景,提供客观的家政人员背景核验服务。重点核验身份信息、司法涉诉记录及失信历史。辅助雇主识别高危人员,让居家养老育儿更安心。">
<meta name="keywords" content="保姆背景核验,家政风险筛查,月嫂司法记录,雇佣安全评估,家政人员核验">
<meta property="og:title" content="家政人员背景核实_保姆月嫂司法安全评估_全能查">
<meta property="og:description" content="全能查针对家庭用工场景,提供客观的家政人员背景核验服务。重点核验身份信息、司法涉诉记录及失信历史。辅助雇主识别高危人员,让居家养老育儿更安心。">
<meta property="og:url" content="https://www.quannengcha.com/inquire/homeservice">
<meta property="og:type" content="website">
<meta property="og:site_name" content="全能查">
<meta property="og:locale" content="zh_CN">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="家政人员背景核实_保姆月嫂司法安全评估_全能查">
<meta name="twitter:description" content="全能查针对家庭用工场景,提供客观的家政人员背景核验服务。重点核验身份信息、司法涉诉记录及失信历史。辅助雇主识别高危人员,让居家养老育儿更安心。">
<meta name="twitter:url" content="https://www.quannengcha.com/inquire/homeservice">
<link rel="canonical" href="https://www.quannengcha.com/inquire/homeservice">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "家政人员背景核实_保姆月嫂司法安全评估_全能查",
"description": "全能查针对家庭用工场景,提供客观的家政人员背景核验服务。重点核验身份信息、司法涉诉记录及失信历史。辅助雇主识别高危人员,让居家养老育儿更安心。",
"url": "https://www.quannengcha.com/inquire/homeservice",
"mainEntity": {
"@type": "Organization",
"name": "全能查",
"url": "https://www.quannengcha.com/",
"description": "专业大数据风险报告查询与代理平台,支持个人和企业多场景风控应用"
}
}
</script>
<meta name="robots" content="index, follow">
<meta name="googlebot" content="index, follow">
<meta name="baiduspider" content="index, follow">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; margin: 0; padding: 0; line-height: 1.6; }
.seo-content { max-width: 1200px; margin: 0 auto; padding: 20px; }
h1 { color: #333; }
p { color: #666; }
.redirect-notice { background: #fff3cd; border: 1px solid #ffc107; color: #856404; padding: 10px; margin: 20px 0; border-radius: 4px; }
</style>
</head>
<body>
<div class="seo-content">
<h1>家政人员背景核实_保姆月嫂司法安全评估_全能查</h1>
<div class="redirect-notice">
<p>正在跳转到完整版网站...</p>
<p>如果浏览器没有自动跳转,请 <a href="https://www.quannengcha.com/inquire/homeservice">点击这里</a></p>
</div>
<p>全能查针对家庭用工场景,提供客观的家政人员背景核验服务。重点核验身份信息、司法涉诉记录及失信历史。辅助雇主识别高危人员,让居家养老育儿更安心。</p>
<section>
<h2>关于全能查</h2>
<p>全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。</p>
</section>
<section>
<h2>核心服务</h2>
<ul>
<li>个人综合风险分析与履约能力画像</li>
<li>企业工商信用画像与商业风险透视</li>
<li>婚前综合背景了解与情感安全风险评估</li>
<li>职场背景核验与入职背调</li>
<li>家政人员背景核实与安全评估</li>
<li>个人履约能力评估与经济风险分析</li>
</ul>
</section>
</div>
</body>
</html>

View File

@@ -0,0 +1,80 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>婚前综合背景了解_情感安全风险评估_家庭履约分析_全能查</title>
<meta name="description" content="全能查婚恋风险报告基于合法公开数据,辅助评估对象的婚前背景。核心核验司法涉诉记录、失信被执行历史、多重履约能力及不良社会标签。拒绝情感盲区,用数据守护您的家庭与财产安全。">
<meta name="keywords" content="婚前背景报告,恋爱对象风险,情感安全评估,司法记录核验,家庭风险防范">
<meta property="og:title" content="婚前综合背景了解_情感安全风险评估_家庭履约分析_全能查">
<meta property="og:description" content="全能查婚恋风险报告基于合法公开数据,辅助评估对象的婚前背景。核心核验司法涉诉记录、失信被执行历史、多重履约能力及不良社会标签。拒绝情感盲区,用数据守护您的家庭与财产安全。">
<meta property="og:url" content="https://www.quannengcha.com/inquire/marriage">
<meta property="og:type" content="website">
<meta property="og:site_name" content="全能查">
<meta property="og:locale" content="zh_CN">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="婚前综合背景了解_情感安全风险评估_家庭履约分析_全能查">
<meta name="twitter:description" content="全能查婚恋风险报告基于合法公开数据,辅助评估对象的婚前背景。核心核验司法涉诉记录、失信被执行历史、多重履约能力及不良社会标签。拒绝情感盲区,用数据守护您的家庭与财产安全。">
<meta name="twitter:url" content="https://www.quannengcha.com/inquire/marriage">
<link rel="canonical" href="https://www.quannengcha.com/inquire/marriage">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "婚前综合背景了解_情感安全风险评估_家庭履约分析_全能查",
"description": "全能查婚恋风险报告基于合法公开数据,辅助评估对象的婚前背景。核心核验司法涉诉记录、失信被执行历史、多重履约能力及不良社会标签。拒绝情感盲区,用数据守护您的家庭与财产安全。",
"url": "https://www.quannengcha.com/inquire/marriage",
"mainEntity": {
"@type": "Organization",
"name": "全能查",
"url": "https://www.quannengcha.com/",
"description": "专业大数据风险报告查询与代理平台,支持个人和企业多场景风控应用"
}
}
</script>
<meta name="robots" content="index, follow">
<meta name="googlebot" content="index, follow">
<meta name="baiduspider" content="index, follow">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; margin: 0; padding: 0; line-height: 1.6; }
.seo-content { max-width: 1200px; margin: 0 auto; padding: 20px; }
h1 { color: #333; }
p { color: #666; }
.redirect-notice { background: #fff3cd; border: 1px solid #ffc107; color: #856404; padding: 10px; margin: 20px 0; border-radius: 4px; }
</style>
</head>
<body>
<div class="seo-content">
<h1>婚前综合背景了解_情感安全风险评估_家庭履约分析_全能查</h1>
<div class="redirect-notice">
<p>正在跳转到完整版网站...</p>
<p>如果浏览器没有自动跳转,请 <a href="https://www.quannengcha.com/inquire/marriage">点击这里</a></p>
</div>
<p>全能查婚恋风险报告基于合法公开数据,辅助评估对象的婚前背景。核心核验司法涉诉记录、失信被执行历史、多重履约能力及不良社会标签。拒绝情感盲区,用数据守护您的家庭与财产安全。</p>
<section>
<h2>关于全能查</h2>
<p>全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。</p>
</section>
<section>
<h2>核心服务</h2>
<ul>
<li>个人综合风险分析与履约能力画像</li>
<li>企业工商信用画像与商业风险透视</li>
<li>婚前综合背景了解与情感安全风险评估</li>
<li>职场背景核验与入职背调</li>
<li>家政人员背景核实与安全评估</li>
<li>个人履约能力评估与经济风险分析</li>
</ul>
</section>
</div>
</body>
</html>

View File

@@ -0,0 +1,80 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>综合履约评分检测_多平台履约记录分析_个人财务履约报告_全能查</title>
<meta name="description" content="全能查提供专业的个人履约健康度体检服务。基于多维大数据分析,检测您的综合评分波动、历史履约记录及潜在的风险标签。本服务旨在帮助用户优化个人数据画像,提升信用管理意识,不提供任何信贷金融服务。">
<meta name="keywords" content="综合评分检测,多重履约压力分析,履约能力评估,综合评分优化,个人数据画像">
<meta property="og:title" content="综合履约评分检测_多平台履约记录分析_个人财务履约报告_全能查">
<meta property="og:description" content="全能查提供专业的个人履约健康度体检服务。基于多维大数据分析,检测您的综合评分波动、历史履约记录及潜在的风险标签。本服务旨在帮助用户优化个人数据画像,提升信用管理意识,不提供任何信贷金融服务。">
<meta property="og:url" content="https://www.quannengcha.com/inquire/preloanbackgroundcheck">
<meta property="og:type" content="website">
<meta property="og:site_name" content="全能查">
<meta property="og:locale" content="zh_CN">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="综合履约评分检测_多平台履约记录分析_个人财务履约报告_全能查">
<meta name="twitter:description" content="全能查提供专业的个人履约健康度体检服务。基于多维大数据分析,检测您的综合评分波动、历史履约记录及潜在的风险标签。本服务旨在帮助用户优化个人数据画像,提升信用管理意识,不提供任何信贷金融服务。">
<meta name="twitter:url" content="https://www.quannengcha.com/inquire/preloanbackgroundcheck">
<link rel="canonical" href="https://www.quannengcha.com/inquire/preloanbackgroundcheck">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "综合履约评分检测_多平台履约记录分析_个人财务履约报告_全能查",
"description": "全能查提供专业的个人履约健康度体检服务。基于多维大数据分析,检测您的综合评分波动、历史履约记录及潜在的风险标签。本服务旨在帮助用户优化个人数据画像,提升信用管理意识,不提供任何信贷金融服务。",
"url": "https://www.quannengcha.com/inquire/preloanbackgroundcheck",
"mainEntity": {
"@type": "Organization",
"name": "全能查",
"url": "https://www.quannengcha.com/",
"description": "专业大数据风险报告查询与代理平台,支持个人和企业多场景风控应用"
}
}
</script>
<meta name="robots" content="index, follow">
<meta name="googlebot" content="index, follow">
<meta name="baiduspider" content="index, follow">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; margin: 0; padding: 0; line-height: 1.6; }
.seo-content { max-width: 1200px; margin: 0 auto; padding: 20px; }
h1 { color: #333; }
p { color: #666; }
.redirect-notice { background: #fff3cd; border: 1px solid #ffc107; color: #856404; padding: 10px; margin: 20px 0; border-radius: 4px; }
</style>
</head>
<body>
<div class="seo-content">
<h1>综合履约评分检测_多平台履约记录分析_个人财务履约报告_全能查</h1>
<div class="redirect-notice">
<p>正在跳转到完整版网站...</p>
<p>如果浏览器没有自动跳转,请 <a href="https://www.quannengcha.com/inquire/preloanbackgroundcheck">点击这里</a></p>
</div>
<p>全能查提供专业的个人履约健康度体检服务。基于多维大数据分析,检测您的综合评分波动、历史履约记录及潜在的风险标签。本服务旨在帮助用户优化个人数据画像,提升信用管理意识,不提供任何信贷金融服务。</p>
<section>
<h2>关于全能查</h2>
<p>全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。</p>
</section>
<section>
<h2>核心服务</h2>
<ul>
<li>个人综合风险分析与履约能力画像</li>
<li>企业工商信用画像与商业风险透视</li>
<li>婚前综合背景了解与情感安全风险评估</li>
<li>职场背景核验与入职背调</li>
<li>家政人员背景核实与安全评估</li>
<li>个人履约能力评估与经济风险分析</li>
</ul>
</section>
</div>
</body>
</html>

View File

@@ -0,0 +1,80 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>个人综合风险分析_履约能力画像_多维数据检测_全能查</title>
<meta name="description" content="全能查个人风险报告为您提供全方位的信用健康度参考。基于公开数据深度解析综合风险指数、司法关联风险、历史履约趋势及潜在的负面标签。数据客观中立,帮助用户建立良好的个人履约记录管理意识。">
<meta name="keywords" content="个人风险检测,履约能力分析,综合风险指数,信用健康度,个人数据画像">
<meta property="og:title" content="个人综合风险分析_履约能力画像_多维数据检测_全能查">
<meta property="og:description" content="全能查个人风险报告为您提供全方位的信用健康度参考。基于公开数据深度解析综合风险指数、司法关联风险、历史履约趋势及潜在的负面标签。数据客观中立,帮助用户建立良好的个人履约记录管理意识。">
<meta property="og:url" content="https://www.quannengcha.com/inquire/riskassessment">
<meta property="og:type" content="website">
<meta property="og:site_name" content="全能查">
<meta property="og:locale" content="zh_CN">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="个人综合风险分析_履约能力画像_多维数据检测_全能查">
<meta name="twitter:description" content="全能查个人风险报告为您提供全方位的信用健康度参考。基于公开数据深度解析综合风险指数、司法关联风险、历史履约趋势及潜在的负面标签。数据客观中立,帮助用户建立良好的个人履约记录管理意识。">
<meta name="twitter:url" content="https://www.quannengcha.com/inquire/riskassessment">
<link rel="canonical" href="https://www.quannengcha.com/inquire/riskassessment">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "个人综合风险分析_履约能力画像_多维数据检测_全能查",
"description": "全能查个人风险报告为您提供全方位的信用健康度参考。基于公开数据深度解析综合风险指数、司法关联风险、历史履约趋势及潜在的负面标签。数据客观中立,帮助用户建立良好的个人履约记录管理意识。",
"url": "https://www.quannengcha.com/inquire/riskassessment",
"mainEntity": {
"@type": "Organization",
"name": "全能查",
"url": "https://www.quannengcha.com/",
"description": "专业大数据风险报告查询与代理平台,支持个人和企业多场景风控应用"
}
}
</script>
<meta name="robots" content="index, follow">
<meta name="googlebot" content="index, follow">
<meta name="baiduspider" content="index, follow">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; margin: 0; padding: 0; line-height: 1.6; }
.seo-content { max-width: 1200px; margin: 0 auto; padding: 20px; }
h1 { color: #333; }
p { color: #666; }
.redirect-notice { background: #fff3cd; border: 1px solid #ffc107; color: #856404; padding: 10px; margin: 20px 0; border-radius: 4px; }
</style>
</head>
<body>
<div class="seo-content">
<h1>个人综合风险分析_履约能力画像_多维数据检测_全能查</h1>
<div class="redirect-notice">
<p>正在跳转到完整版网站...</p>
<p>如果浏览器没有自动跳转,请 <a href="https://www.quannengcha.com/inquire/riskassessment">点击这里</a></p>
</div>
<p>全能查个人风险报告为您提供全方位的信用健康度参考。基于公开数据深度解析综合风险指数、司法关联风险、历史履约趋势及潜在的负面标签。数据客观中立,帮助用户建立良好的个人履约记录管理意识。</p>
<section>
<h2>关于全能查</h2>
<p>全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。</p>
</section>
<section>
<h2>核心服务</h2>
<ul>
<li>个人综合风险分析与履约能力画像</li>
<li>企业工商信用画像与商业风险透视</li>
<li>婚前综合背景了解与情感安全风险评估</li>
<li>职场背景核验与入职背调</li>
<li>家政人员背景核实与安全评估</li>
<li>个人履约能力评估与经济风险分析</li>
</ul>
</section>
</div>
</body>
</html>

View File

@@ -0,0 +1,80 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>全能查合伙人计划_风控平台系统招商_渠道合作平台_全能查</title>
<meta name="description" content="全能查开放全国渠道合作,提供零门槛的风险评估系统接入服务。一键开通独立后台,支持婚恋、职场、家政及商业风控等多场景报告推广。正规项目,结算透明,赋能流量方实现合规商业价值。">
<meta name="keywords" content="风控系统代理,风险评估平台招商,平台渠道合作,企业服务创业,全能查合伙人">
<meta property="og:title" content="全能查合伙人计划_风控平台系统招商_渠道合作平台_全能查">
<meta property="og:description" content="全能查开放全国渠道合作,提供零门槛的风险评估系统接入服务。一键开通独立后台,支持婚恋、职场、家政及商业风控等多场景报告推广。正规项目,结算透明,赋能流量方实现合规商业价值。">
<meta property="og:url" content="https://www.quannengcha.com/promote">
<meta property="og:type" content="website">
<meta property="og:site_name" content="全能查">
<meta property="og:locale" content="zh_CN">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="全能查合伙人计划_风控平台系统招商_渠道合作平台_全能查">
<meta name="twitter:description" content="全能查开放全国渠道合作,提供零门槛的风险评估系统接入服务。一键开通独立后台,支持婚恋、职场、家政及商业风控等多场景报告推广。正规项目,结算透明,赋能流量方实现合规商业价值。">
<meta name="twitter:url" content="https://www.quannengcha.com/promote">
<link rel="canonical" href="https://www.quannengcha.com/promote">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "全能查合伙人计划_风控平台系统招商_渠道合作平台_全能查",
"description": "全能查开放全国渠道合作,提供零门槛的风险评估系统接入服务。一键开通独立后台,支持婚恋、职场、家政及商业风控等多场景报告推广。正规项目,结算透明,赋能流量方实现合规商业价值。",
"url": "https://www.quannengcha.com/promote",
"mainEntity": {
"@type": "Organization",
"name": "全能查",
"url": "https://www.quannengcha.com/",
"description": "专业大数据风险报告查询与代理平台,支持个人和企业多场景风控应用"
}
}
</script>
<meta name="robots" content="index, follow">
<meta name="googlebot" content="index, follow">
<meta name="baiduspider" content="index, follow">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; margin: 0; padding: 0; line-height: 1.6; }
.seo-content { max-width: 1200px; margin: 0 auto; padding: 20px; }
h1 { color: #333; }
p { color: #666; }
.redirect-notice { background: #fff3cd; border: 1px solid #ffc107; color: #856404; padding: 10px; margin: 20px 0; border-radius: 4px; }
</style>
</head>
<body>
<div class="seo-content">
<h1>全能查合伙人计划_风控平台系统招商_渠道合作平台_全能查</h1>
<div class="redirect-notice">
<p>正在跳转到完整版网站...</p>
<p>如果浏览器没有自动跳转,请 <a href="https://www.quannengcha.com/promote">点击这里</a></p>
</div>
<p>全能查开放全国渠道合作,提供零门槛的风险评估系统接入服务。一键开通独立后台,支持婚恋、职场、家政及商业风控等多场景报告推广。正规项目,结算透明,赋能流量方实现合规商业价值。</p>
<section>
<h2>关于全能查</h2>
<p>全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。</p>
</section>
<section>
<h2>核心服务</h2>
<ul>
<li>个人综合风险分析与履约能力画像</li>
<li>企业工商信用画像与商业风险透视</li>
<li>婚前综合背景了解与情感安全风险评估</li>
<li>职场背景核验与入职背调</li>
<li>家政人员背景核实与安全评估</li>
<li>个人履约能力评估与经济风险分析</li>
</ul>
</section>
</div>
</body>
</html>

View File

@@ -0,0 +1,80 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>客服中心 - 全能查在线客服 | 技术支持</title>
<meta name="description" content="全能查客服中心,提供在线客服支持、技术咨询、问题反馈等服务,确保用户获得及时有效的帮助。">
<meta name="keywords" content="客服中心, 在线客服, 技术支持, 问题反馈, 全能查客服">
<meta property="og:title" content="客服中心 - 全能查在线客服 | 技术支持">
<meta property="og:description" content="全能查客服中心,提供在线客服支持、技术咨询、问题反馈等服务,确保用户获得及时有效的帮助。">
<meta property="og:url" content="https://www.quannengcha.com/service">
<meta property="og:type" content="website">
<meta property="og:site_name" content="全能查">
<meta property="og:locale" content="zh_CN">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="客服中心 - 全能查在线客服 | 技术支持">
<meta name="twitter:description" content="全能查客服中心,提供在线客服支持、技术咨询、问题反馈等服务,确保用户获得及时有效的帮助。">
<meta name="twitter:url" content="https://www.quannengcha.com/service">
<link rel="canonical" href="https://www.quannengcha.com/service">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "客服中心 - 全能查在线客服 | 技术支持",
"description": "全能查客服中心,提供在线客服支持、技术咨询、问题反馈等服务,确保用户获得及时有效的帮助。",
"url": "https://www.quannengcha.com/service",
"mainEntity": {
"@type": "Organization",
"name": "全能查",
"url": "https://www.quannengcha.com/",
"description": "专业大数据风险报告查询与代理平台,支持个人和企业多场景风控应用"
}
}
</script>
<meta name="robots" content="index, follow">
<meta name="googlebot" content="index, follow">
<meta name="baiduspider" content="index, follow">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; margin: 0; padding: 0; line-height: 1.6; }
.seo-content { max-width: 1200px; margin: 0 auto; padding: 20px; }
h1 { color: #333; }
p { color: #666; }
.redirect-notice { background: #fff3cd; border: 1px solid #ffc107; color: #856404; padding: 10px; margin: 20px 0; border-radius: 4px; }
</style>
</head>
<body>
<div class="seo-content">
<h1>客服中心 - 全能查在线客服 | 技术支持</h1>
<div class="redirect-notice">
<p>正在跳转到完整版网站...</p>
<p>如果浏览器没有自动跳转,请 <a href="https://www.quannengcha.com/service">点击这里</a></p>
</div>
<p>全能查客服中心,提供在线客服支持、技术咨询、问题反馈等服务,确保用户获得及时有效的帮助。</p>
<section>
<h2>关于全能查</h2>
<p>全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。</p>
</section>
<section>
<h2>核心服务</h2>
<ul>
<li>个人综合风险分析与履约能力画像</li>
<li>企业工商信用画像与商业风险透视</li>
<li>婚前综合背景了解与情感安全风险评估</li>
<li>职场背景核验与入职背调</li>
<li>家政人员背景核实与安全评估</li>
<li>个人履约能力评估与经济风险分析</li>
</ul>
</section>
</div>
</body>
</html>

View File

@@ -1,5 +1,5 @@
{
"name": "全能查|大数据风险报告查询与代理平台,支持个人和企业多场景风控应用",
"name": "全能查官网_个人婚姻状态报告_综合风险排查工具箱",
"short_name": "全能查",
"description": "专业大数据风险报告查询与代理平台,支持个人信用查询、小微企业风控、贷前风险背调等多场景报告应用",
"start_url": "/",

View File

@@ -1,67 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://www.zhinengcha.cn/</loc>
<loc>https://www.quannengcha.com/</loc>
<lastmod>2025-08-01</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://www.zhinengcha.cn/agent</loc>
<loc>https://www.quannengcha.com/agent</loc>
<lastmod>2025-08-01</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://www.zhinengcha.cn/help</loc>
<loc>https://www.quannengcha.com/help</loc>
<lastmod>2025-08-01</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://www.zhinengcha.cn/help/guide</loc>
<loc>https://www.quannengcha.com/help/guide</loc>
<lastmod>2025-08-01</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://www.zhinengcha.cn/example</loc>
<loc>https://www.quannengcha.com/example</loc>
<lastmod>2025-08-01</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://www.zhinengcha.cn/service</loc>
<loc>https://www.quannengcha.com/service</loc>
<lastmod>2025-08-01</lastmod>
<changefreq>monthly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>https://www.zhinengcha.cn/privacyPolicy</loc>
<loc>https://www.quannengcha.com/privacyPolicy</loc>
<lastmod>2025-08-01</lastmod>
<changefreq>yearly</changefreq>
<priority>0.3</priority>
</url>
<url>
<loc>https://www.zhinengcha.cn/userAgreement</loc>
<loc>https://www.quannengcha.com/userAgreement</loc>
<lastmod>2025-08-01</lastmod>
<changefreq>yearly</changefreq>
<priority>0.3</priority>
</url>
<url>
<loc>https://www.zhinengcha.cn/agentManageAgreement</loc>
<loc>https://www.quannengcha.com/agentManageAgreement</loc>
<lastmod>2025-08-01</lastmod>
<changefreq>yearly</changefreq>
<priority>0.3</priority>
</url>
<url>
<loc>https://www.zhinengcha.cn/agentSerivceAgreement</loc>
<loc>https://www.quannengcha.com/agentSerivceAgreement</loc>
<lastmod>2025-08-01</lastmod>
<changefreq>yearly</changefreq>
<priority>0.3</priority>
</url>
<url>
<loc>https://www.zhinengcha.cn/authorization</loc>
<loc>https://www.quannengcha.com/authorization</loc>
<lastmod>2025-08-01</lastmod>
<changefreq>yearly</changefreq>
<priority>0.3</priority>

170
server/crawler-detector.js Normal file
View File

@@ -0,0 +1,170 @@
/**
* 爬虫检测模块
* 用于识别搜索引擎爬虫和社交媒体爬虫
*/
class CrawlerDetector {
constructor() {
// 常见搜索引擎爬虫User-Agent列表
this.crawlerPatterns = [
// 百度爬虫
'baiduspider',
'baiduspider-mobile',
'baiduspider-image',
'baiduspider-video',
'baiduspider-news',
'baiduboxapp',
// Google爬虫
'googlebot',
'googlebot-image',
'googlebot-news',
'googlebot-mobile',
'googlebot-video',
'google-web-snippet',
// 360搜索
'360spider',
'soha-agent',
'haosouspider',
// 搜狗搜索
'sogou spider',
'sogou news spider',
'sogou orion spider',
'sogou-blog',
// 必应
'bingbot',
'msnbot',
// 雅虎
'slurp',
// 搜搜
'sosospider',
'sosoimagespider',
// 有道
'youdaobot',
'yodaobot',
// 头条搜索
'bytedance-spider',
'toutiaospider',
// 社交媒体爬虫
'facebookexternalhit',
'facebookcatalog',
'twitterbot',
'linkedinbot',
'whatsapp',
'telegrambot',
'viber',
'line',
// 其他常见爬虫
'applebot',
'semrushbot',
'ahrefsbot',
'mj12bot',
'dotbot',
'crawler',
'spider',
'bot'
]
// 需要检测的头部字段
this.crawlerHeaders = ['x-bot', 'x-crawler', 'x-forwarded-for']
}
/**
* 检测请求是否来自爬虫
* @param {Object} req - HTTP请求对象
* @returns {Boolean} 是否为爬虫
*/
isCrawler(req) {
const userAgent = req.headers['user-agent']?.toLowerCase() || ''
const headers = req.headers
// 1. 通过User-Agent检测
if (this.checkUserAgent(userAgent)) {
console.log(`[CrawlerDetector] 检测到爬虫 UA: ${userAgent}`)
return true
}
// 2. 通过特定头部检测
if (this.checkHeaders(headers)) {
console.log(`[CrawlerDetector] 检测到爬虫 Headers`)
return true
}
// 3. 通过IP地址检测可选
// if (this.checkIP(req.connection.remoteAddress)) {
// return true
// }
return false
}
/**
* 检查User-Agent
* @param {String} userAgent
* @returns {Boolean}
*/
checkUserAgent(userAgent) {
if (!userAgent) return false
return this.crawlerPatterns.some(pattern => {
return userAgent.includes(pattern.toLowerCase())
})
}
/**
* 检查请求头
* @param {Object} headers
* @returns {Boolean}
*/
checkHeaders(headers) {
for (const header of this.crawlerHeaders) {
const headerValue = headers[header]?.toLowerCase()
if (headerValue && (headerValue.includes('bot') || headerValue.includes('crawler'))) {
return true
}
}
return false
}
/**
* 检查IP地址是否为已知爬虫IP
* @param {String} ip
* @returns {Boolean}
*/
checkIP(ip) {
// 这里可以添加已知爬虫IP段的检测
// 需要定期更新爬虫IP列表
return false
}
/**
* 获取爬虫类型
* @param {String} userAgent
* @returns {String} 爬虫类型
*/
getCrawlerType(userAgent) {
const ua = userAgent.toLowerCase()
if (ua.includes('baiduspider')) return 'baidu'
if (ua.includes('googlebot')) return 'google'
if (ua.includes('bingbot') || ua.includes('msnbot')) return 'bing'
if (ua.includes('360spider')) return '360'
if (ua.includes('sogou spider')) return 'sogou'
if (ua.includes('facebookexternalhit')) return 'facebook'
if (ua.includes('twitterbot')) return 'twitter'
if (ua.includes('linkedinbot')) return 'linkedin'
return 'unknown'
}
}
module.exports = CrawlerDetector

View File

@@ -0,0 +1,245 @@
/**
* SEO模板生成器
* 根据 useSEO.js 的页面配置自动生成静态 HTML 模板,供爬虫访问时返回
* 配置与 src/composables/useSEO.js 保持一致
*
* 多站点:通过环境变量 SEO_BASE_URL 指定 canonical/og:url 域名后生成
* 例SEO_BASE_URL=https://www.quannengcha.com node generate-seo-templates.cjs
*/
const fs = require('fs')
const path = require('path')
const BASE_URL = process.env.SEO_BASE_URL || 'https://www.quannengcha.com'
// 页面 SEO 配置(与 src/composables/useSEO.js 的 routeConfigs 保持一致)
const pageSEOConfigs = {
'index.html': {
title: '全能查官网_个人婚姻状态报告_综合风险排查工具箱',
description: '全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。',
keywords: '全能查,婚姻状态核实,风险排查工具,个人风险预警,第三方背调,商业信用评估',
url: BASE_URL
},
'agent-system-guide.html': {
title: '全能查合作政策指南_合作伙伴权益与结算说明_官方文档',
description: '全能查官方合作体系说明文档。详细解读合作伙伴的等级权益、服务费结算标准及晋升机制。致力于构建公平、透明的商业合作生态,助力合作伙伴快速上手业务。',
keywords: '合作伙伴政策,服务费结算,渠道等级说明,业务操作指南,代理系统后台',
url: `${BASE_URL}/agent/system-guide`
},
'inquire-riskassessment.html': {
title: '个人综合风险分析_履约能力画像_多维数据检测_全能查',
description: '全能查个人风险报告为您提供全方位的信用健康度参考。基于公开数据深度解析综合风险指数、司法关联风险、历史履约趋势及潜在的负面标签。数据客观中立,帮助用户建立良好的个人履约记录管理意识。',
keywords: '个人风险检测,履约能力分析,综合风险指数,信用健康度,个人数据画像',
url: `${BASE_URL}/inquire/riskassessment`
},
'inquire-companyinfo.html': {
title: '企业工商信用画像_经营异常与商业风险透视_全能查',
description: '全能查企业版深度透视商业真相。聚合工商、司法及税务公开数据,核验企业经营异常名录、行政处罚、法律诉讼及股权穿透信息。全方位评估合作伙伴的商业健康度,规避合同违约风险。',
keywords: '企业信用评估,工商背景核验,商业风险评估,公司经营异常,合作方背景核实',
url: `${BASE_URL}/inquire/companyinfo`
},
'inquire-preloanbackgroundcheck.html': {
title: '综合履约评分检测_多平台履约记录分析_个人财务履约报告_全能查',
description: '全能查提供专业的个人履约健康度体检服务。基于多维大数据分析,检测您的综合评分波动、历史履约记录及潜在的风险标签。本服务旨在帮助用户优化个人数据画像,提升信用管理意识,不提供任何信贷金融服务。',
keywords: '综合评分检测,多重履约压力分析,履约能力评估,综合评分优化,个人数据画像',
url: `${BASE_URL}/inquire/preloanbackgroundcheck`
},
'inquire-marriage.html': {
title: '婚前综合背景了解_情感安全风险评估_家庭履约分析_全能查',
description: '全能查婚恋风险报告基于合法公开数据,辅助评估对象的婚前背景。核心核验司法涉诉记录、失信被执行历史、多重履约能力及不良社会标签。拒绝情感盲区,用数据守护您的家庭与财产安全。',
keywords: '婚前背景报告,恋爱对象风险,情感安全评估,司法记录核验,家庭风险防范',
url: `${BASE_URL}/inquire/marriage`
},
'inquire-backgroundcheck.html': {
title: '职场背景核验报告_候选人职业风险与竞业核验_全能查',
description: '全能查为企业提供专业的入职背调服务。一键筛查候选人的学历背景、涉及的商业利益冲突、劳动仲裁记录及社会不良风险。数据实时合规,降低企业用工试错成本,提升招聘决策效率。',
keywords: '员工入职背调,职业背景核实,竞业限制评估,职场信用报告,候选人风险筛查',
url: `${BASE_URL}/inquire/backgroundcheck`
},
'inquire-homeservice.html': {
title: '家政人员背景核实_保姆月嫂司法安全评估_全能查',
description: '全能查针对家庭用工场景,提供客观的家政人员背景核验服务。重点核验身份信息、司法涉诉记录及失信历史。辅助雇主识别高危人员,让居家养老育儿更安心。',
keywords: '保姆背景核验,家政风险筛查,月嫂司法记录,雇佣安全评估,家政人员核验',
url: `${BASE_URL}/inquire/homeservice`
},
'inquire-consumerFinanceReport.html': {
title: '个人履约能力评估_经济风险与收支压力参考_全能查',
description: '全能查履约报告基于大数据算法,提供个人经济稳定性的客观分析。多维度检测综合履约分、经济关联风险及潜在的资金压力指数。本服务仅提供大数据层面的风险参考,助您优化财务管理。',
keywords: '履约能力评估,经济风险指数,综合评分波动,资金压力分析,财务健康度',
url: `${BASE_URL}/inquire/consumerFinanceReport`
},
'agent.html': {
title: '全能查代理 - 免费开通代理权限 | 大数据风险报告代理',
description: '全能查代理平台,免费开通代理权限,享受大数据风险报告查询服务代理收益。专业的大数据风险报告、婚姻查询、个人信用评估等服务的代理合作。',
keywords: '全能查代理, 免费代理, 大数据风险报告代理, 代理权限, 代理收益',
url: `${BASE_URL}/agent`
},
'help.html': {
title: '帮助中心 - 全能查使用指南 | 常见问题解答',
description: '全能查帮助中心,提供详细的使用指南、常见问题解答、操作教程等,帮助用户更好地使用大数据风险报告查询服务。',
keywords: '全能查帮助, 使用指南, 常见问题, 操作教程, 客服支持',
url: `${BASE_URL}/help`
},
'help-guide.html': {
title: '使用指南 - 全能查操作教程 | 功能说明',
description: '全能查详细使用指南,包含各功能模块的操作教程、功能说明、注意事项等,让用户快速上手使用。',
keywords: '使用指南, 操作教程, 功能说明, 快速上手, 全能查教程',
url: `${BASE_URL}/help/guide`
},
'example.html': {
title: '示例报告 - 全能查报告展示 | 大数据风险报告样例',
description: '全能查示例报告展示,包含大数据风险报告、婚姻状况查询、个人信用评估等服务的报告样例,让用户了解报告内容和格式。',
keywords: '示例报告, 报告展示, 报告样例, 大数据风险报告, 婚姻查询报告',
url: `${BASE_URL}/example`
},
'service.html': {
title: '客服中心 - 全能查在线客服 | 技术支持',
description: '全能查客服中心,提供在线客服支持、技术咨询、问题反馈等服务,确保用户获得及时有效的帮助。',
keywords: '客服中心, 在线客服, 技术支持, 问题反馈, 全能查客服',
url: `${BASE_URL}/service`
},
'promote.html': {
title: '全能查合伙人计划_风控平台系统招商_渠道合作平台_全能查',
description: '全能查开放全国渠道合作,提供零门槛的风险评估系统接入服务。一键开通独立后台,支持婚恋、职场、家政及商业风控等多场景报告推广。正规项目,结算透明,赋能流量方实现合规商业价值。',
keywords: '风控系统代理,风险评估平台招商,平台渠道合作,企业服务创业,全能查合伙人',
url: `${BASE_URL}/promote`
}
}
/**
* 规范化文案:统一为中文标点,避免乱码
*/
function normalizeText(str) {
if (typeof str !== 'string') return str
return str
.replace(/\uFFFD/g, '')
.replace(/。/g, '。')
.replace(/、/g, '、')
}
/**
* 转义 HTML 属性值
*/
function escapeAttr(str) {
if (typeof str !== 'string') return ''
return str
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
}
/**
* 生成单页 HTML 模板
*/
function generateHTMLTemplate(config) {
const title = normalizeText(config.title)
const description = normalizeText(config.description)
const keywords = normalizeText(config.keywords)
const structuredData = {
'@context': 'https://schema.org',
'@type': 'WebPage',
name: title,
description: description,
url: config.url,
mainEntity: {
'@type': 'Organization',
name: '全能查',
url: 'https://www.quannengcha.com/',
description: '专业大数据风险报告查询与代理平台,支持个人和企业多场景风控应用'
}
}
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>${escapeAttr(title)}</title>
<meta name="description" content="${escapeAttr(description)}">
<meta name="keywords" content="${escapeAttr(keywords)}">
<meta property="og:title" content="${escapeAttr(title)}">
<meta property="og:description" content="${escapeAttr(description)}">
<meta property="og:url" content="${escapeAttr(config.url)}">
<meta property="og:type" content="website">
<meta property="og:site_name" content="全能查">
<meta property="og:locale" content="zh_CN">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="${escapeAttr(title)}">
<meta name="twitter:description" content="${escapeAttr(description)}">
<meta name="twitter:url" content="${escapeAttr(config.url)}">
<link rel="canonical" href="${escapeAttr(config.url)}">
<script type="application/ld+json">
${JSON.stringify(structuredData, null, 8)}
</script>
<meta name="robots" content="index, follow">
<meta name="googlebot" content="index, follow">
<meta name="baiduspider" content="index, follow">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; margin: 0; padding: 0; line-height: 1.6; }
.seo-content { max-width: 1200px; margin: 0 auto; padding: 20px; }
h1 { color: #333; }
p { color: #666; }
.redirect-notice { background: #fff3cd; border: 1px solid #ffc107; color: #856404; padding: 10px; margin: 20px 0; border-radius: 4px; }
</style>
</head>
<body>
<div class="seo-content">
<h1>${escapeAttr(title)}</h1>
<div class="redirect-notice">
<p>正在跳转到完整版网站...</p>
<p>如果浏览器没有自动跳转,请 <a href="${escapeAttr(config.url)}">点击这里</a></p>
</div>
<p>${escapeAttr(description)}</p>
<section>
<h2>关于全能查</h2>
<p>全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。</p>
</section>
<section>
<h2>核心服务</h2>
<ul>
<li>个人综合风险分析与履约能力画像</li>
<li>企业工商信用画像与商业风险透视</li>
<li>婚前综合背景了解与情感安全风险评估</li>
<li>职场背景核验与入职背调</li>
<li>家政人员背景核实与安全评估</li>
<li>个人履约能力评估与经济风险分析</li>
</ul>
</section>
</div>
</body>
</html>`
}
function main() {
const outputDir = path.join(__dirname, '../public/seo-templates')
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true })
console.log(`✓ 创建模板目录: ${outputDir}`)
}
let successCount = 0
Object.entries(pageSEOConfigs).forEach(([filename, config]) => {
const htmlContent = generateHTMLTemplate(config)
const filePath = path.join(outputDir, filename)
fs.writeFileSync(filePath, htmlContent, 'utf-8')
console.log(`✓ 生成模板: ${filename}`)
successCount++
})
console.log(`\n✓ 成功生成 ${successCount} 个 SEO 模板文件`)
console.log(`📁 模板目录: ${outputDir}`)
console.log(`💡 配置与 useSEO.js 一致,当前域名: ${BASE_URL}`)
}
main()

179
server/middleware.js Normal file
View File

@@ -0,0 +1,179 @@
/**
* SEO中间件
* 用于在Node.js服务器中检测爬虫并返回静态HTML
*/
const fs = require('fs')
const path = require('path')
const CrawlerDetector = require('./crawler-detector')
class SEOMiddleware {
constructor(options = {}) {
this.detector = new CrawlerDetector()
this.templateDir = options.templateDir || path.join(__dirname, '../public/seo-templates')
this.defaultTemplate = options.defaultTemplate || 'index.html'
this.fallbackToSPA = options.fallbackToSPA !== false
this.debug = options.debug || false
// 路由到模板的映射(与 useSEO.js 及 generate-seo-templates.cjs 保持一致;子路径放前面以优先精确匹配)
this.routeTemplateMap = {
'/': 'index.html',
'/agent/system-guide': 'agent-system-guide.html',
'/inquire/riskassessment': 'inquire-riskassessment.html',
'/inquire/companyinfo': 'inquire-companyinfo.html',
'/inquire/preloanbackgroundcheck': 'inquire-preloanbackgroundcheck.html',
'/inquire/marriage': 'inquire-marriage.html',
'/inquire/backgroundcheck': 'inquire-backgroundcheck.html',
'/inquire/homeservice': 'inquire-homeservice.html',
'/inquire/consumerFinanceReport': 'inquire-consumerFinanceReport.html',
'/agent': 'agent.html',
'/help/guide': 'help-guide.html',
'/help': 'help.html',
'/example': 'example.html',
'/service': 'service.html',
'/promote': 'promote.html'
}
// 初始化模板缓存
this.templateCache = new Map()
this.cacheTemplates()
}
/**
* 缓存所有模板文件
*/
cacheTemplates() {
try {
if (!fs.existsSync(this.templateDir)) {
console.warn(`[SEOMiddleware] 模板目录不存在: ${this.templateDir}`)
return
}
const files = fs.readdirSync(this.templateDir)
files.forEach(file => {
const filePath = path.join(this.templateDir, file)
if (fs.statSync(filePath).isFile()) {
this.templateCache.set(file, fs.readFileSync(filePath, 'utf-8'))
if (this.debug) {
console.log(`[SEOMiddleware] 已缓存模板: ${file}`)
}
}
})
console.log(`[SEOMiddleware] 已缓存 ${this.templateCache.size} 个模板文件`)
} catch (error) {
console.error('[SEOMiddleware] 缓存模板失败:', error)
}
}
/**
* 获取对应的模板文件名
* @param {String} path - 请求路径
* @returns {String} 模板文件名
*/
getTemplatePath(requestPath) {
// 完全匹配
if (this.routeTemplateMap[requestPath]) {
return this.routeTemplateMap[requestPath]
}
// 模糊匹配(处理动态路由)
const matchedKey = Object.keys(this.routeTemplateMap).find(route => {
return requestPath.startsWith(route)
})
return matchedKey ? this.routeTemplateMap[matchedKey] : this.defaultTemplate
}
/**
* 获取模板内容
* @param {String} templateName - 模板文件名
* @returns {String|null} 模板内容
*/
getTemplate(templateName) {
// 首先尝试缓存
let content = this.templateCache.get(templateName)
// 如果缓存中没有,尝试从磁盘读取
if (!content) {
try {
const filePath = path.join(this.templateDir, templateName)
if (fs.existsSync(filePath)) {
content = fs.readFileSync(filePath, 'utf-8')
this.templateCache.set(templateName, content)
}
} catch (error) {
console.error(`[SEOMiddleware] 读取模板失败: ${templateName}`, error)
}
}
return content || null
}
/**
* Express中间件
*/
express() {
return (req, res, next) => {
// 检测是否为爬虫
if (this.detector.isCrawler(req)) {
const templateName = this.getTemplatePath(req.path)
const template = this.getTemplate(templateName)
if (template) {
// 设置响应头
res.setHeader('Content-Type', 'text/html; charset=utf-8')
res.setHeader('X-SEOMiddleware', 'prerendered')
// 返回静态HTML
if (this.debug) {
console.log(`[SEOMiddleware] 返回SEO模板: ${templateName} for ${req.path}`)
}
return res.send(template)
}
}
// 不是爬虫或模板不存在继续处理SPA
next()
}
}
/**
* Koa中间件
*/
koa() {
return async (ctx, next) => {
// 检测是否为爬虫
if (this.detector.isCrawler(ctx.req)) {
const templateName = this.getTemplatePath(ctx.path)
const template = this.getTemplate(templateName)
if (template) {
ctx.type = 'text/html; charset=utf-8'
ctx.set('X-SEOMiddleware', 'prerendered')
if (this.debug) {
console.log(`[SEOMiddleware] 返回SEO模板: ${templateName} for ${ctx.path}`)
}
ctx.body = template
return
}
}
await next()
}
}
/**
* 重新加载模板缓存
*/
reloadCache() {
this.templateCache.clear()
this.cacheTemplates()
console.log('[SEOMiddleware] 模板缓存已重新加载')
}
}
module.exports = SEOMiddleware

27
server/package.json Normal file
View File

@@ -0,0 +1,27 @@
{
"name": "tydata-seo-server",
"version": "1.0.0",
"description": "SPA SEO 优化 - 爬虫检测与静态 HTML 回退,与 useSEO.js 同步",
"main": "server-example-express.js",
"scripts": {
"start": "node server-example-express.js",
"dev": "node server-example-express.js",
"generate": "node generate-seo-templates.cjs",
"test": "node test-seo.js",
"test:crawler": "node test-crawler-detection.js"
},
"keywords": [
"seo",
"crawler",
"spa",
"prerender"
],
"license": "MIT",
"dependencies": {
"express": "^4.18.2",
"compression": "^1.7.4"
},
"devDependencies": {
"nodemon": "^3.0.1"
}
}

View File

@@ -0,0 +1,36 @@
/**
* Express服务器示例
* 展示如何集成SEO中间件
*/
const express = require('express')
const path = require('path')
const SEOMiddleware = require('./middleware')
const app = express()
const port = process.env.PORT || 3000
// 初始化SEO中间件
const seoMiddleware = new SEOMiddleware({
templateDir: path.join(__dirname, '../public/seo-templates'),
debug: process.env.NODE_ENV === 'development'
})
// 应用SEO中间件必须在静态文件服务之前
app.use(seoMiddleware.express())
// 静态文件服务
app.use(express.static(path.join(__dirname, '../dist')))
// SPA路由处理
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, '../dist/index.html'))
})
// 启动服务器
app.listen(port, () => {
console.log(`🚀 服务器运行在 http://localhost:${port}`)
console.log(`🔍 SEO中间件已启用`)
})
module.exports = app

View File

@@ -0,0 +1,112 @@
/**
* 爬虫检测测试脚本
* 用于验证爬虫检测功能是否正常工作
*/
const CrawlerDetector = require('./crawler-detector')
const detector = new CrawlerDetector()
// 测试用例
const testCases = [
// 爬虫User-Agent
{ userAgent: 'Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)', expected: true, description: '百度爬虫' },
{ userAgent: 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)', expected: true, description: 'Google爬虫' },
{ userAgent: 'Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)', expected: true, description: '必应爬虫' },
{ userAgent: 'Sogou web spider/4.0(+http://www.sogou.com/docs/help/webmasters.htm#07)', expected: true, description: '搜狗爬虫' },
{ userAgent: '360Spider', expected: true, description: '360爬虫' },
{ userAgent: 'facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)', expected: true, description: 'Facebook爬虫' },
{ userAgent: 'Twitterbot/1.0', expected: true, description: 'Twitter爬虫' },
{ userAgent: 'LinkedInBot/1.0 (compatible; Mozilla/5.0; +https://www.linkedin.com/help/linkedin/answer/8665)', expected: true, description: 'LinkedIn爬虫' },
// 正常浏览器User-Agent
{ userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', expected: false, description: 'Chrome浏览器' },
{ userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0', expected: false, description: 'Firefox浏览器' },
{ userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15', expected: false, description: 'Safari浏览器' },
{ userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1', expected: false, description: 'iPhone Safari' },
{ userAgent: 'Mozilla/5.0 (Linux; Android 13; SM-S908B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36', expected: false, description: 'Android Chrome' },
{ userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0', expected: false, description: 'Edge浏览器' },
// 边界情况
{ userAgent: '', expected: false, description: '空User-Agent' },
{ userAgent: 'Mozilla/5.0 (compatible; MyBot/1.0)', expected: true, description: '包含bot关键词' },
{ userAgent: 'Mozilla/5.0 (compatible; Spider/1.0)', expected: true, description: '包含spider关键词' },
{ userAgent: 'Mozilla/5.0 (compatible; Crawler/1.0)', expected: true, description: '包含crawler关键词' }
]
console.log('='.repeat(70))
console.log('爬虫检测测试')
console.log('='.repeat(70))
console.log()
let passed = 0
let failed = 0
testCases.forEach((testCase, index) => {
const req = {
headers: {
'user-agent': testCase.userAgent
}
}
const result = detector.isCrawler(req)
const success = result === testCase.expected
const status = success ? '✓ 通过' : '✗ 失败'
const crawlerType = result ? detector.getCrawlerType(testCase.userAgent) : 'N/A'
if (success) {
passed++
console.log(`${status} 测试 ${index + 1}: ${testCase.description}`)
} else {
failed++
console.error(`${status} 测试 ${index + 1}: ${testCase.description}`)
console.error(` User-Agent: ${testCase.userAgent.substring(0, 80)}...`)
console.error(` 预期: ${testCase.expected}, 实际: ${result}`)
}
if (result) {
console.log(` 识别为: ${crawlerType} 爬虫`)
}
})
console.log()
console.log('='.repeat(70))
console.log(`测试结果: ${passed} 通过, ${failed} 失败, 共 ${testCases.length} 个测试`)
console.log('='.repeat(70))
console.log()
// 测试爬虫类型识别
console.log('爬虫类型识别测试:')
console.log('-'.repeat(70))
const crawlerTypes = [
{ userAgent: 'Baiduspider', expected: 'baidu', description: '百度爬虫' },
{ userAgent: 'Googlebot', expected: 'google', description: 'Google爬虫' },
{ userAgent: 'bingbot', expected: 'bing', description: '必应爬虫' },
{ userAgent: '360spider', expected: '360', description: '360爬虫' },
{ userAgent: 'sogou spider', expected: 'sogou', description: '搜狗爬虫' },
{ userAgent: 'facebookexternalhit', expected: 'facebook', description: 'Facebook爬虫' },
{ userAgent: 'Twitterbot', expected: 'twitter', description: 'Twitter爬虫' },
{ userAgent: 'linkedinbot', expected: 'linkedin', description: 'LinkedIn爬虫' }
]
let typePassed = 0
crawlerTypes.forEach(test => {
const result = detector.getCrawlerType(test.userAgent)
const success = result === test.expected
if (success) {
typePassed++
console.log(`${test.description}: ${result}`)
} else {
console.error(`${test.description}: 预期 ${test.expected}, 实际 ${result}`)
}
})
console.log()
console.log('='.repeat(70))
console.log(`爬虫类型识别: ${typePassed}/${crawlerTypes.length} 正确`)
console.log('='.repeat(70))
// 退出码
process.exit(failed === 0 ? 0 : 1)

178
server/test-seo.js Normal file
View File

@@ -0,0 +1,178 @@
/**
* SEO 端到端检测脚本
* 模拟爬虫与普通用户请求,验证是否返回正确的页面
*
* 使用前请先启动服务器: npm run start
* 然后运行: npm run test 或 node test-seo.js
*/
const http = require('http')
const https = require('https')
const BASE_URL = process.env.SEO_TEST_URL || 'http://localhost:3000'
// 要检测的路由及期望的 SEO 标题关键词(与 useSEO.js 一致,天远数据)
const ROUTES = [
{ path: '/', titleKeyword: '天远数据' },
{ path: '/agent', titleKeyword: '天远数据代理' },
{ path: '/help', titleKeyword: '天远数据帮助中心' },
{ path: '/inquire/personalData', titleKeyword: '个人综合风险报告' },
{ path: '/agent/promote', titleKeyword: '推广码' },
{ path: '/historyQuery', titleKeyword: '我的报告' }
]
function request(url, userAgent) {
return new Promise((resolve, reject) => {
const lib = url.startsWith('https') ? https : http
const req = lib.get(url, {
headers: { 'User-Agent': userAgent },
timeout: 10000
}, res => {
const chunks = []
res.on('data', chunk => chunks.push(chunk))
res.on('end', () => {
resolve({
statusCode: res.statusCode,
headers: res.headers,
body: Buffer.concat(chunks).toString('utf-8')
})
})
})
req.on('error', reject)
req.on('timeout', () => {
req.destroy()
reject(new Error('请求超时'))
})
})
}
function extractTitle(html) {
const match = html.match(/<title[^>]*>([^<]+)<\/title>/i)
return match ? match[1].trim() : null
}
function hasMetaDescription(html) {
return /<meta\s+name=["']description["']\s+content=["']/i.test(html)
}
function isSEOTemplate(html) {
return (
/<meta\s+name=["']description["']/i.test(html) &&
/<meta\s+name=["']keywords["']/i.test(html) &&
/<link\s+rel=["']canonical["']/i.test(html)
)
}
async function runTest(route, titleKeyword) {
const url = BASE_URL + route
const results = { route, crawler: null, normal: null }
// 1. 爬虫请求
try {
const crawlerRes = await request(url, 'Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)')
const title = extractTitle(crawlerRes.body)
const seoHeader = crawlerRes.headers['x-seomiddleware']
results.crawler = {
status: crawlerRes.statusCode,
title,
hasSEOMeta: hasMetaDescription(crawlerRes.body),
isSEOTemplate: isSEOTemplate(crawlerRes.body),
seoHeader: seoHeader || '(无)'
}
if (crawlerRes.statusCode !== 200) {
results.crawler.error = `HTTP ${crawlerRes.statusCode}`
} else if (!title || !title.includes(titleKeyword)) {
results.crawler.error = `标题不匹配,期望含「${titleKeyword}」,实际: ${title || '未找到'}`
} else if (!results.crawler.isSEOTemplate) {
results.crawler.error = '响应中缺少完整 SEO 标签description/keywords/canonical'
}
} catch (e) {
results.crawler = { error: e.message || String(e) }
}
// 2. 普通用户请求(仅验证能正常返回)
try {
const normalRes = await request(url, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36')
results.normal = {
status: normalRes.statusCode,
bodyLength: normalRes.body.length
}
if (normalRes.statusCode !== 200) {
results.normal.error = `HTTP ${normalRes.statusCode}`
}
} catch (e) {
results.normal = { error: e.message || String(e) }
}
return results
}
function printResult(results) {
const { route, crawler, normal } = results
console.log(`\n📍 路由: ${route}`)
console.log('─'.repeat(60))
const crawlerOk = crawler && !crawler.error && crawler.status === 200 && crawler.isSEOTemplate
if (crawlerOk) {
console.log(' 爬虫请求: ✓ 通过')
console.log(` 标题: ${crawler.title}`)
console.log(` 响应头: X-SEOMiddleware = ${crawler.seoHeader}`)
} else {
console.log(' 爬虫请求: ✗ 未通过')
if (crawler && crawler.error) console.log(` 原因: ${crawler.error}`)
else if (crawler) console.log(` 状态: ${crawler.status}, 标题: ${crawler.title || '无'}`)
else console.log(' 请求失败')
}
const normalOk = normal && !normal.error && normal.status === 200
if (normalOk) {
console.log(' 普通用户: ✓ 正常 (SPA)')
} else {
console.log(' 普通用户: ✗ 异常')
if (normal && normal.error) console.log(` 原因: ${normal.error}`)
}
}
async function main() {
console.log('='.repeat(60))
console.log('SEO 端到端检测')
console.log('='.repeat(60))
console.log(`目标地址: ${BASE_URL}`)
console.log('若服务器未启动,请先执行: npm run start')
console.log('')
let allPass = true
for (const r of ROUTES) {
try {
const results = await runTest(r.path, r.titleKeyword)
printResult(results)
const crawlerOk = results.crawler && !results.crawler.error && results.crawler.isSEOTemplate
const normalOk = results.normal && !results.normal.error && results.normal.status === 200
if (!crawlerOk || !normalOk) allPass = false
} catch (e) {
console.log(`\n📍 路由: ${r.path}`)
console.log(' 错误:', e.message)
allPass = false
}
}
console.log('\n' + '='.repeat(60))
if (allPass) {
console.log('✓ 全部检测通过:爬虫获得 SEO 模板,普通用户获得 SPA')
} else {
console.log('✗ 部分检测未通过,请检查服务器与模板配置')
}
console.log('='.repeat(60))
process.exit(allPass ? 0 : 1)
}
main().catch(err => {
console.error('检测失败:', err)
process.exit(1)
})

View File

@@ -278,3 +278,66 @@ export function getInviteLink(params) {
const queryString = buildQueryString(params || {});
return useApiFetch(`/agent/invite_link${queryString}`).get().json();
}
// ==================== 白名单相关接口 ====================
/**
* 获取可屏蔽的 feature 列表(带价格)
*/
export function getWhitelistFeatures() {
return useApiFetch("/agent/whitelist/features").get().json();
}
/**
* 创建白名单订单
* @param {object} params - 创建参数
* @param {string} params.id_card - 身份证号
* @param {string[]} params.feature_ids - 要屏蔽的 feature ID 列表
*/
export function createWhitelistOrder(params) {
return useApiFetch("/agent/whitelist/order/create").post(params).json();
}
/**
* 查询白名单列表
* @param {object} params - 查询参数
* @param {number} params.page - 页码
* @param {number} params.page_size - 每页数量
* @param {string} params.id_card - 身份证号(可选)
*/
export function getWhitelistList(params) {
const queryString = buildQueryString(params || {});
return useApiFetch(`/agent/whitelist/list${queryString}`).get().json();
}
/**
* 检查模块是否已下架
* @param {object} params - 查询参数
* @param {string} params.id_card - 身份证号
* @param {string} params.feature_api_id - Feature 的 API 标识
* @param {string} params.query_id - 查询记录 ID可选
*/
export function checkFeatureWhitelistStatus(params) {
const queryString = buildQueryString(params || {});
return useApiFetch(`/agent/whitelist/check${queryString}`).get().json();
}
/**
* 下架单个模块(免费或需支付)
* @param {object} params - 下架参数
* @param {string} params.feature_api_id - Feature 的 API 标识
* @param {string} params.query_id - 查询记录 ID必填
*/
export function offlineFeature(params) {
return useApiFetch("/agent/whitelist/offline").post(params).json();
}
/**
* 检查订单是否属于当前代理推广
* @param {object} params - 查询参数
* @param {string} params.order_id - 订单 ID
*/
export function checkOrderAgent(params) {
const queryString = buildQueryString(params || {});
return useApiFetch(`/agent/order/agent${queryString}`).get().json();
}

View File

@@ -117,6 +117,7 @@ declare global {
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
const useAgent: typeof import('./composables/useAgent.js')['useAgent']
const useAgentStore: typeof import('./stores/agentStore.js')['useAgentStore']
const useAliyunCaptcha: typeof import('./composables/useAliyunCaptcha.js')['default']
const useAnimate: typeof import('@vueuse/core')['useAnimate']
const useApiFetch: typeof import('./composables/useApiFetch.js')['default']
const useAppStore: typeof import('./stores/appStore.js')['useAppStore']
@@ -283,6 +284,7 @@ declare global {
const useUserStore: typeof import('./stores/userStore.js')['useUserStore']
const useVModel: typeof import('@vueuse/core')['useVModel']
const useVModels: typeof import('@vueuse/core')['useVModels']
const useVehiclePayload: typeof import('./composables/useVehiclePayload.js')['useVehiclePayload']
const useVibrate: typeof import('@vueuse/core')['useVibrate']
const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
const useWakeLock: typeof import('@vueuse/core')['useWakeLock']

View File

@@ -127,6 +127,7 @@ const router = useRouter();
const show = defineModel("show");
import { useCascaderAreaData } from "@vant/area-data";
import { showToast } from "vant"; // 引入 showToast 方法
import { useAliyunCaptcha } from "@/composables/useAliyunCaptcha";
const emit = defineEmits(); // 确保 emit 可以正确使用
const props = defineProps({
isSelf: {
@@ -139,6 +140,7 @@ const props = defineProps({
},
});
const { isSelf, userName } = toRefs(props);
const { runWithCaptcha } = useAliyunCaptcha();
const form = ref({
referrer: "",
region: "",
@@ -173,20 +175,22 @@ const getSmsCode = async () => {
loadingSms.value = true;
const { data, error } = await useApiFetch("auth/sendSms")
.post({ mobile: form.value.mobile, actionType: "agentApply" })
.json();
loadingSms.value = false;
if (data.value && !error.value) {
if (data.value.code === 200) {
showToast({ message: "获取成功" });
startCountdown(); // 启动倒计时
} else {
showToast(data.value.msg);
// 使用滑块验证码保护发送短信接口
runWithCaptcha(
(captchaVerifyParam) =>
useApiFetch("auth/sendSms")
.post({ mobile: form.value.mobile, actionType: "agentApply", captchaVerifyParam })
.json(),
(res) => {
loadingSms.value = false;
if (res.code === 200) {
showToast({ message: "获取成功" });
startCountdown(); // 启动倒计时
} else {
showToast(res.msg || "获取验证码失败");
}
}
}
);
};
let timer = null;

View File

@@ -3,12 +3,19 @@ import ShareReportButton from "./ShareReportButton.vue";
import TitleBanner from "./TitleBanner.vue";
import VerificationCard from "./VerificationCard.vue";
import StyledTabs from "./StyledTabs.vue";
import Payment from "./Payment.vue";
import { splitDWBG8B4DForTabs } from '@/ui/CDWBG8B4D/utils/simpleSplitter.js';
import { splitDWBG6A2CForTabs } from '@/ui/DWBG6A2C/utils/simpleSplitter.js';
import { splitJRZQ7F1AForTabs } from '@/ui/JRZQ7F1A/utils/simpleSplitter.js';
import { splitCJRZQ5E9FForTabs } from '@/ui/CJRZQ5E9F/utils/simpleSplitter.js';
import { splitCQYGL3F8EForTabs } from '@/ui/CQYGL3F8E/utils/simpleSplitter.js';
import { useAppStore } from "@/stores/appStore";
import { useAgentStore } from "@/stores/agentStore";
import { storeToRefs } from "pinia";
import { showFailToast } from "vant";
import { checkFeatureWhitelistStatus, offlineFeature, checkOrderAgent } from "@/api/agent";
import { vehicleFeatureMap, vehicleFeatureRiskLevels } from "@/config/vehicleFeatureMap";
import { normalizeVehicleReportData } from "@/utils/vehicleReportNormalize";
// 动态导入产品背景图片的函数
const loadProductBackground = async (productType) => {
@@ -53,6 +60,11 @@ const props = defineProps({
type: String,
default: "",
},
queryId: {
type: String,
required: false,
default: "",
},
feature: {
type: String,
required: true,
@@ -98,8 +110,20 @@ const {
isEmpty,
isDone,
isExample,
orderId,
orderNo,
queryId,
} = toRefs(props);
const agentStore = useAgentStore();
const { isDiamond } = storeToRefs(agentStore);
// 订单是否属于当前代理推广
const isAgentOrder = ref(false);
// 获取身份证号(从 reportParams 中)
const idCard = computed(() => reportParams.value?.id_card || "");
const active = ref(null);
const backgroundContainerRef = ref(null); // 背景容器的引用
@@ -153,6 +177,21 @@ onMounted(async () => {
// 监听窗口大小变化,重新计算高度
window.addEventListener('resize', handleResize);
// 检查订单是否属于当前代理推广
if (!isExample.value && orderId.value) {
try {
const { data, error } = await checkOrderAgent({ order_id: orderId.value });
if (data.value && !error.value && data.value.code === 200) {
isAgentOrder.value = data.value.data.is_agent_order;
}
} catch (err) {
console.error("检查订单代理状态失败:", err);
}
}
if (isAgentOrder.value && idCard.value && !isExample.value) {
checkAllFeaturesStatus();
}
});
// 处理窗口大小变化(带防抖)
@@ -189,8 +228,10 @@ const processedReportData = computed(() => {
// 拆分CQYGL3F8E数据
data = splitCQYGL3F8EForTabs(data);
// 车辆报告:解包嵌套 data与微信小程序 normalizeVehicleQueryData 对齐
data = normalizeVehicleReportData(data);
// 过滤掉在featureMap中没有对应的项
return data.filter(item => featureMap[item.data.apiID]);
return data.filter(item => featureMap[item.data?.apiID]);
});
// 获取产品背景图片
@@ -242,6 +283,200 @@ const trapezoidBgStyle = computed(() => {
return {};
});
// 模块下架状态映射主模块ID -> { isOfflined, whitelistPrice, isSubmitting }
const featureOfflineStatus = ref(new Map());
const getMainApiId = (apiId) => {
if (!apiId) return "";
const index = apiId.indexOf("_");
return index > 0 ? apiId.substring(0, index) : apiId;
};
const checkFeatureStatus = async (featureApiId, forceRefresh = false) => {
if (!idCard.value || !featureApiId) return;
const mainApiId = getMainApiId(featureApiId);
if (!mainApiId) return;
if (!forceRefresh && featureOfflineStatus.value.has(mainApiId)) return;
try {
const { data, error } = await checkFeatureWhitelistStatus({
id_card: idCard.value,
feature_api_id: mainApiId,
query_id: queryId.value || "",
});
if (data.value && !error.value && data.value.code === 200) {
const isWhitelisted = data.value.data.is_whitelisted || false;
const dataDeleted = data.value.data.data_deleted !== undefined ? data.value.data.data_deleted : true;
featureOfflineStatus.value.set(mainApiId, {
isOfflined: isWhitelisted && dataDeleted,
whitelistPrice: data.value.data.whitelist_price || 0,
isSubmitting: false,
});
}
} catch (err) {
console.error("检查模块状态失败:", err);
}
};
const checkAllFeaturesStatus = async () => {
if (!idCard.value || !isAgentOrder.value || isExample.value) return;
const featureApiIds = processedReportData.value.map((item) => item.data.apiID);
const mainApiIds = [...new Set(featureApiIds.map((id) => getMainApiId(id)))];
for (const mainApiId of mainApiIds) {
if (mainApiId) await checkFeatureStatus(mainApiId);
}
};
const getFeatureStatus = (featureApiId) => {
const mainApiId = getMainApiId(featureApiId);
return featureOfflineStatus.value.get(mainApiId) || {
isOfflined: false,
whitelistPrice: 0,
isSubmitting: false,
};
};
const currentOfflineFeature = ref(null);
const showOfflineConfirmDialog = ref(false);
const handleOfflineClick = async (featureApiId, featureName) => {
const mainApiId = getMainApiId(featureApiId);
const status = getFeatureStatus(mainApiId);
if (status.isOfflined) {
showFailToast("该模块已下架");
return;
}
if (status.whitelistPrice <= 0) {
await confirmOfflineDirectly(mainApiId, featureName);
return;
}
currentOfflineFeature.value = {
featureApiId: mainApiId,
featureName,
whitelistPrice: status.whitelistPrice,
};
showOfflineConfirmDialog.value = true;
};
const showWhitelistPayment = ref(false);
const whitelistPaymentData = ref({ product_name: "", sell_price: 0 });
const whitelistPaymentId = ref("");
const whitelistPaymentType = ref("whitelist");
const getCurrentReportUrl = () => {
if (orderNo.value) return `/report?orderNo=${orderNo.value}`;
if (orderId.value) return `/report?orderId=${orderId.value}`;
return "";
};
const confirmOfflineDirectly = async (mainApiId, featureName) => {
if (!idCard.value || !mainApiId) return;
const status = getFeatureStatus(mainApiId);
status.isSubmitting = true;
featureOfflineStatus.value.set(mainApiId, { ...status });
try {
if (!queryId.value) {
showFailToast("缺少查询记录ID无法下架");
const cur = getFeatureStatus(mainApiId);
cur.isSubmitting = false;
featureOfflineStatus.value.set(mainApiId, { ...cur });
return;
}
const { data, error } = await offlineFeature({
query_id: queryId.value,
feature_api_id: mainApiId,
});
if (!data.value || error.value || data.value.code !== 200) {
showFailToast(data.value?.msg || "下架失败");
const cur = getFeatureStatus(mainApiId);
cur.isSubmitting = false;
featureOfflineStatus.value.set(mainApiId, { ...cur });
return;
}
const resp = data.value.data || {};
if (resp.need_pay) {
const cur = getFeatureStatus(mainApiId);
cur.isSubmitting = false;
cur.whitelistPrice = resp.amount || 0;
featureOfflineStatus.value.set(mainApiId, { ...cur });
whitelistPaymentData.value = {
product_name: `${featureName || "模块"} 下架`,
sell_price: resp.amount || 0,
};
whitelistPaymentId.value = `${idCard.value}|${mainApiId}`;
whitelistPaymentType.value = "whitelist";
showWhitelistPayment.value = true;
return;
}
showFailToast("下架成功");
const updated = getFeatureStatus(mainApiId);
updated.isSubmitting = false;
updated.isOfflined = true;
featureOfflineStatus.value.set(mainApiId, { ...updated });
if (queryId.value || orderId.value) window.location.reload();
} catch (err) {
console.error("下架模块失败:", err);
showFailToast("下架模块失败");
const cur = getFeatureStatus(mainApiId);
cur.isSubmitting = false;
featureOfflineStatus.value.set(mainApiId, { ...cur });
}
};
const confirmOffline = async () => {
if (!currentOfflineFeature.value) return;
const { featureApiId } = currentOfflineFeature.value;
const mainApiId = featureApiId;
if (!queryId.value) {
showFailToast("缺少查询记录ID无法下架");
return;
}
const status = getFeatureStatus(mainApiId);
status.isSubmitting = true;
featureOfflineStatus.value.set(mainApiId, { ...status });
try {
const { data, error } = await offlineFeature({
query_id: queryId.value,
feature_api_id: mainApiId,
});
if (!data.value || error.value || data.value.code !== 200) {
showFailToast(data.value?.msg || "下架失败");
const cur = getFeatureStatus(mainApiId);
cur.isSubmitting = false;
featureOfflineStatus.value.set(mainApiId, { ...cur });
return;
}
const resp = data.value.data || {};
if (resp.need_pay) {
showOfflineConfirmDialog.value = false;
whitelistPaymentData.value = {
product_name: `${currentOfflineFeature.value?.featureName || "模块"} 下架`,
sell_price: resp.amount || 0,
};
whitelistPaymentId.value = `${idCard.value}|${mainApiId}`;
whitelistPaymentType.value = "whitelist";
showWhitelistPayment.value = true;
const cur = getFeatureStatus(mainApiId);
cur.isSubmitting = false;
featureOfflineStatus.value.set(mainApiId, { ...cur });
return;
}
showFailToast("下架成功");
showOfflineConfirmDialog.value = false;
currentOfflineFeature.value = null;
const updated = getFeatureStatus(mainApiId);
updated.isSubmitting = false;
updated.isOfflined = true;
featureOfflineStatus.value.set(mainApiId, { ...updated });
if (queryId.value || orderId.value) window.location.reload();
} catch (err) {
console.error("下架模块失败:", err);
showFailToast("下架模块失败");
const cur = getFeatureStatus(mainApiId);
cur.isSubmitting = false;
featureOfflineStatus.value.set(mainApiId, { ...cur });
}
};
const featureMap = {
IVYZ5733: {
name: "婚姻状态",
@@ -269,6 +504,12 @@ const featureMap = {
name: "违约失信",
component: defineAsyncComponent(() => import("@/ui/CFLXG3D56.vue")),
},
// IVYZ0S0D:{
// name: "劳动仲裁信息查询(个人版)",
// component: defineAsyncComponent(() => import("@/ui/CIVYZ0S0D.vue")),
// remark: '劳动仲裁信息查询(个人版)用于查询个人在劳动仲裁方面的信息,包括劳动仲裁案件数量、劳动仲裁案件类型、劳动仲裁案件结果等。',
// },
FLXG0V4B: {
name: "司法涉诉",
@@ -316,14 +557,7 @@ const featureMap = {
name: "税务风险",
component: defineAsyncComponent(() => import("@/ui/CQYGL3F8E/components/TaxRisk/index.vue")),
},
QCXG7A2B: {
name: "名下车辆",
component: defineAsyncComponent(() => import("@/ui/CQCXG7A2B.vue")),
},
QCXG9P1C: {
name: "名下车辆",
component: defineAsyncComponent(() => import("@/ui/CQCXG9P1C.vue")),
},
...vehicleFeatureMap,
BehaviorRiskScan: {
name: "风险行为扫描",
component: defineAsyncComponent(() =>
@@ -347,6 +581,11 @@ const featureMap = {
component: defineAsyncComponent(() => import("@/ui/IVYZ3P9M.vue")),
remark: '学历信息展示学生姓名、身份证号、学校、专业、入学与毕业时间、学历层次以及学习形式等字段,可结合字典编码了解具体含义。',
},
IVYZ0S0D: {
name: "劳动仲裁信息",
component: defineAsyncComponent(() => import("@/ui/IVYZ0S0D.vue")),
remark: '劳动仲裁信息展示被查询人在失信限高、劳动争议、社会保险、福利待遇、人事争议、仲裁流程及通知函触达等方面的风险信息。',
},
IVYZ8I9J: {
name: "网络社交异常",
component: defineAsyncComponent(() => import("@/ui/IVYZ8I9J.vue")),
@@ -376,10 +615,10 @@ const featureMap = {
name: "逾期风险综述",
component: defineAsyncComponent(() => import("@/ui/CDWBG8B4D/components/OverdueRiskSection.vue")),
},
// DWBG8B4D_CourtInfo: {
// name: "法院曝光台信息",
// component: defineAsyncComponent(() => import("@/ui/CDWBG8B4D/components/MultCourtInfoSection.vue")),
// },
DWBG8B4D_CourtInfo: {
name: "法院曝光台信息",
component: defineAsyncComponent(() => import("@/ui/CDWBG8B4D/components/MultCourtInfoSection.vue")),
},
DWBG8B4D_LoanEvaluation: {
name: "借贷评估",
component: defineAsyncComponent(() => import("@/ui/CDWBG8B4D/components/LoanEvaluationSection.vue")),
@@ -465,10 +704,10 @@ const featureMap = {
name: "关联风险监督",
component: defineAsyncComponent(() => import("@/ui/DWBG6A2C/components/RiskSupervisionSection.vue")),
},
// DWBG6A2C_CourtRiskInfo: {
// name: "法院风险信息",
// component: defineAsyncComponent(() => import("@/ui/DWBG6A2C/components/CourtRiskInfoSection.vue")),
// },
DWBG6A2C_CourtRiskInfo: {
name: "法院风险信息",
component: defineAsyncComponent(() => import("@/ui/DWBG6A2C/components/CourtRiskInfoSection.vue")),
},
// 贷款风险报告
JRZQ5E9F: {
name: "贷款风险评估",
@@ -572,7 +811,12 @@ const featureMap = {
component: defineAsyncComponent(() => import("@/ui/JRZQ3C9R/index.vue")),
remark: '支付表现是指数基于近两年的查验记录、还款成功与失败表现以及余额不足情况,对用户支付与还款习惯进行量化评分,用于评估其支付稳定性与违约风险。'
},
// 学历信息
IVYZ4Y27: {
name: "学历信息",
component: defineAsyncComponent(() => import("@/ui/IVYZ4Y27/index.vue")),
remark: '学历信息展示专业能力相关的学习经历,包括院校名称、学习类型、专业方向、学习时间、学历等级及核心竞争力等级等字段。',
},
};
const maskValue = computed(() => {
@@ -644,87 +888,95 @@ const maskValue = computed(() => {
// ==================== 新评分系统 ====================
// Feature 风险等级配置(权重越高表示风险越大,最终分数越高越安全)
const featureRiskLevels = {
// 🔴 高风险类 - 权重 10
'FLXG0V4B': 20, // 司法涉诉
'FLXG7E8F': 20, // 个人涉诉
'FLXG3D56': 10, // 违约失信
'FLXGDEA9': 18, // 本人不良
'JRZQ4AA8': 10, // 还款压力
// 🔴 高风险类 - 权重 60有高风险直接扣约70-80分
'FLXG0V4B': 60, // 司法涉诉
'FLXG3D56': 60, // 违约失信
'JRZQ8A2D': 60, // 特殊名单验证
'FLXG7E8F': 55, // 个人涉诉
'FLXGDEA9': 55, // 本人不良
// 🟠 中高风险类 - 权重 7
'JRZQ0A03': 7, // 借贷申请记录
'JRZQ6F2A': 7, // 借贷申请
'JRZQ8203': 7, // 借贷行为记录
'JRZQ4B6C': 7, // 信贷表现
'BehaviorRiskScan': 7, // 风险行为扫描
'IVYZ8I9J': 7, // 网络社交异常
'JRZQ8A2D': 9, // 特殊名单验证
'JRZQ7F1A': 8, // 全景雷达
'JRZQ7F1A_ApplyReport': 3,
'JRZQ7F1A_BehaviorReport': 3,
'JRZQ7F1A_BigDataReport': 2,
'YYSY7D3E': 5, // 手机携号转网
'YYSY8B1C': 5, // 手机在网时长
'DWBG7F3A': 8, // 多头借贷
// 🟡 中高风险类 - 权重 20-30
'JRZQ4AA8': 25, // 还款压力
'JRZQ0A03': 25, // 借贷申请记录
'JRZQ8203': 25, // 借贷行为记录
'JRZQ4B6C': 25, // 信贷表现
'JRZQ6F2A': 25, // 借贷申请
'BehaviorRiskScan': 60, // 风险行为扫描
'IVYZ8I9J': 45, // 网络社交异常
'JRZQ7F1A': 20, // 全景雷达
'JRZQ7F1A_ApplyReport': 8,
'JRZQ7F1A_BehaviorReport': 8,
'JRZQ7F1A_BigDataReport': 4,
'DWBG7F3A': 25, // 多头借贷
'YYSY7D3E': 15, // 手机携号转网
'YYSY8B1C': 15, // 手机在网时长
// 🟡 中风险类 - 权重 5
'QYGL3F8E': 5, // 人企关系加强版
'QCXG7A2B': 5, // 名下车辆
'JRZQ09J8': 5, // 收入评估
'JRZQ3C9R': 5, // 支付行为指数
// 🔵 低风险类 - 权重 3
'IVYZ5733': 3, // 婚姻状态
'IVYZ9A2B': 3, // 学历信息
'IVYZ3P9M': 3, // 学历信息查询(实时版)
// 🟢 中风险类 - 权重 8-12
'QYGL3F8E': 10, // 人企关系加强版
...vehicleFeatureRiskLevels,
'JRZQ09J8': 10, // 收入评估
'JRZQ3C9R': 10, // 支付行为指数
// 'IVYZ0S0D': 10, // 个人仲裁信息
// 🔵 低风险类 - 权重 3-5
'IVYZ5733': 4, // 婚姻状态
'IVYZ9A2B': 4, // 学历信息
'IVYZ3P9M': 4, // 学历信息查询(实时版)
'IVYZ0S0D': 10, // 劳动仲裁信息
'IVYZ4Y27': 3, // 学历信息
// 📊 复合报告类 - 按子模块动态计算
'DWBG8B4D': 0, // 谛听多维报告(由子模块计算)
'DWBG6A2C': 0, // 司南报告(由子模块计算)
'JRZQ5E9F': 0, // 贷款风险评估(由子模块计算)
// 谛听多维报告子模块
'DWBG8B4D_Overview': 10,
'DWBG8B4D_ElementVerification': 4,
'DWBG8B4D_Identity': 4,
'DWBG8B4D_RiskWarning': 10,
'DWBG8B4D_OverdueRisk': 9,
'DWBG8B4D_LoanEvaluation': 7,
'DWBG8B4D_LeasingRisk': 6,
'DWBG8B4D_RiskSupervision': 8,
'DWBG8B4D_RiskWarningTab': 9,
'DWBG8B4D_Overview': 30,
'DWBG8B4D_ElementVerification': 10,
'DWBG8B4D_Identity': 10,
'DWBG8B4D_RiskWarning': 35,
'DWBG8B4D_OverdueRisk': 30,
'DWBG8B4D_LoanEvaluation': 20,
'DWBG8B4D_LeasingRisk': 18,
'DWBG8B4D_RiskSupervision': 25,
'DWBG8B4D_RiskWarningTab': 30,
'DWBG8B4D_CourtInfo': 31,
// 司南报告子模块
'DWBG6A2C_StandLiveInfo': 4,
'DWBG6A2C_RiskPoint': 9,
'DWBG6A2C_SecurityInfo': 15,
'DWBG6A2C_AntiFraudInfo': 15,
'DWBG6A2C_RiskList': 12,
'DWBG6A2C_ApplicationStatistics': 7,
'DWBG6A2C_LendingStatistics': 6,
'DWBG6A2C_PerformanceStatistics': 7,
'DWBG6A2C_OverdueRecord': 9,
'DWBG6A2C_CreditDetail': 5,
'DWBG6A2C_RentalBehavior': 5,
'DWBG6A2C_RiskSupervision': 8,
'DWBG6A2C_StandLiveInfo': 10,
'DWBG6A2C_RiskPoint': 28,
'DWBG6A2C_SecurityInfo': 45,
'DWBG6A2C_AntiFraudInfo': 45,
'DWBG6A2C_RiskList': 38,
'DWBG6A2C_ApplicationStatistics': 22,
'DWBG6A2C_LendingStatistics': 18,
'DWBG6A2C_PerformanceStatistics': 22,
'DWBG6A2C_OverdueRecord': 28,
'DWBG6A2C_CreditDetail': 15,
'DWBG6A2C_RentalBehavior': 15,
'DWBG6A2C_RiskSupervision': 25,
'DWBG6A2C_CourtRiskInfo': 29,
// 贷款风险评估子模块
'CJRZQ5E9F_RiskOverview': 8,
'CJRZQ5E9F_CreditScores': 7,
'CJRZQ5E9F_LoanBehaviorAnalysis': 7,
'CJRZQ5E9F_InstitutionAnalysis': 5,
'CJRZQ5E9F_TimeTrendAnalysis': 6,
'CJRZQ5E9F_RiskIndicators': 8,
'CJRZQ5E9F_RiskAdvice': 2,
'CJRZQ5E9F_RiskOverview': 25,
'CJRZQ5E9F_CreditScores': 22,
'CJRZQ5E9F_LoanBehaviorAnalysis': 22,
'CJRZQ5E9F_InstitutionAnalysis': 15,
'CJRZQ5E9F_TimeTrendAnalysis': 18,
'CJRZQ5E9F_RiskIndicators': 25,
'CJRZQ5E9F_RiskAdvice': 6,
// 人企关系加强版子模块
'CQYGL3F8E_Investment': 4,
'CQYGL3F8E_SeniorExecutive': 4,
'CQYGL3F8E_Lawsuit': 8,
'CQYGL3F8E_InvestHistory': 3,
'CQYGL3F8E_FinancingHistory': 3,
'CQYGL3F8E_Punishment': 7,
'CQYGL3F8E_Abnormal': 6,
'CQYGL3F8E_TaxRisk': 7,
'CQYGL3F8E_Investment': 12,
'CQYGL3F8E_SeniorExecutive': 12,
'CQYGL3F8E_Lawsuit': 25,
'CQYGL3F8E_InvestHistory': 8,
'CQYGL3F8E_FinancingHistory': 8,
'CQYGL3F8E_Punishment': 22,
'CQYGL3F8E_Abnormal': 18,
'CQYGL3F8E_TaxRisk': 22,
};
// 存储每个组件的 ref 引用
@@ -876,9 +1128,17 @@ const showPublicSecurityRecord = import.meta.env.VITE_SHOW_PUBLIC_SECURITY_RECOR
</van-tab>
<van-tab v-for="(item, index) in processedReportData" :key="`${item.data.apiID}_${index}`"
:title="featureMap[item.data.apiID]?.name">
<TitleBanner :id="item.data.apiID" class="mb-4">
{{ featureMap[item.data.apiID]?.name }}
</TitleBanner>
<div class="flex items-center justify-between gap-2 mb-4">
<TitleBanner :id="item.data.apiID" class="mb-0 flex-1">
{{ featureMap[item.data.apiID]?.name }}
</TitleBanner>
<van-button
v-if="!isShare && !isExample && isAgentOrder && !getFeatureStatus(item.data.apiID).isOfflined"
size="small" type="default" :loading="getFeatureStatus(item.data.apiID).isSubmitting"
@click="handleOfflineClick(item.data.apiID, featureMap[item.data.apiID]?.name)">
下架
</van-button>
</div>
<component :is="featureMap[item.data.apiID]?.component" :ref="el => {
if (el) {
const refKey = `${item.data.apiID}_${index}`;
@@ -931,6 +1191,18 @@ const showPublicSecurityRecord = import.meta.env.VITE_SHOW_PUBLIC_SECURITY_RECOR
<div>{{ companyName }}版权所有</div>
</div>
<!-- 下架确认弹窗付费场景 -->
<van-dialog v-model:show="showOfflineConfirmDialog" title="确认下架" show-cancel-button @confirm="confirmOffline">
<div class="p-4 text-gray-600">
确定要下架{{ currentOfflineFeature?.featureName || '该模块' }}需支付 ¥{{
currentOfflineFeature?.whitelistPrice?.toFixed(2) || '0.00' }}
</div>
</van-dialog>
<!-- 白名单下架支付弹窗 -->
<Payment v-model="showWhitelistPayment" :data="whitelistPaymentData" :id="whitelistPaymentId"
:type="whitelistPaymentType" :return-url="getCurrentReportUrl()" />
</template>
<style lang="scss" scoped>

View File

@@ -7,6 +7,7 @@ import { useUserStore } from "@/stores/userStore";
import { showToast } from "vant";
import useApiFetch from "@/composables/useApiFetch";
import { registerByInviteCode } from "@/api/agent";
import { useAliyunCaptcha } from "@/composables/useAliyunCaptcha";
const emit = defineEmits(['register-success'])
const router = useRouter();
@@ -14,6 +15,7 @@ const route = useRoute();
const dialogStore = useDialogStore();
const agentStore = useAgentStore();
const userStore = useUserStore();
const { runWithCaptcha } = useAliyunCaptcha();
const appName = import.meta.env.VITE_APP_NAME || '全能查';
const phoneNumber = ref("");
const verificationCode = ref("");
@@ -80,26 +82,31 @@ async function sendVerificationCode() {
showToast({ message: "请先输入邀请码" });
return;
}
const actionType = hasAccount.value ? "bindMobile" : "agentApply";
const { data, error } = await useApiFetch("auth/sendSms")
.post({ mobile: phoneNumber.value, actionType })
.json();
if (data.value && !error.value) {
if (data.value.code === 200) {
showToast({ message: "获取成功" });
startCountdown();
// 聚焦到验证码输入框
nextTick(() => {
const verificationCodeInput = document.getElementById('registerVerificationCode');
if (verificationCodeInput) {
verificationCodeInput.focus();
}
});
} else {
showToast(data.value.msg);
const actionType = hasAccount.value ? "bindMobile" : "agentApply";
// 使用滑块验证码保护发送短信接口
runWithCaptcha(
(captchaVerifyParam) =>
useApiFetch("auth/sendSms")
.post({ mobile: phoneNumber.value, actionType, captchaVerifyParam })
.json(),
(res) => {
if (res.code === 200) {
showToast({ message: "获取成功" });
startCountdown();
// 聚焦到验证码输入框
nextTick(() => {
const verificationCodeInput = document.getElementById('registerVerificationCode');
if (verificationCodeInput) {
verificationCodeInput.focus();
}
});
} else {
showToast(res.msg || "获取验证码失败");
}
}
}
);
}
function startCountdown() {

View File

@@ -1,12 +1,14 @@
<script setup>
import { ref, computed, nextTick } from "vue";
import { useDialogStore } from "@/stores/dialogStore";
import { useAliyunCaptcha } from "@/composables/useAliyunCaptcha";
const emit = defineEmits(['bind-success'])
const router = useRouter();
const dialogStore = useDialogStore();
const agentStore = useAgentStore();
const userStore = useUserStore();
const { runWithCaptcha } = useAliyunCaptcha();
const appName = import.meta.env.VITE_APP_NAME || '全能查';
const phoneNumber = ref("");
const verificationCode = ref("");
@@ -37,25 +39,29 @@ async function sendVerificationCode() {
showToast({ message: "请输入有效的手机号" });
return;
}
const { data, error } = await useApiFetch("auth/sendSms")
.post({ mobile: phoneNumber.value, actionType: "bindMobile" })
.json();
if (data.value && !error.value) {
if (data.value.code === 200) {
showToast({ message: "获取成功" });
startCountdown();
// 聚焦到验证码输入框
nextTick(() => {
const verificationCodeInput = document.getElementById('bindPhoneVerificationCode');
if (verificationCodeInput) {
verificationCodeInput.focus();
}
});
} else {
showToast(data.value.msg);
// 使用滑块验证码保护发送短信接口
runWithCaptcha(
(captchaVerifyParam) =>
useApiFetch("auth/sendSms")
.post({ mobile: phoneNumber.value, actionType: "bindMobile", captchaVerifyParam })
.json(),
(res) => {
if (res.code === 200) {
showToast({ message: "获取成功" });
startCountdown();
// 聚焦到验证码输入框
nextTick(() => {
const verificationCodeInput = document.getElementById('bindPhoneVerificationCode');
if (verificationCodeInput) {
verificationCodeInput.focus();
}
});
} else {
showToast(res.msg || "获取验证码失败");
}
}
}
);
}
function startCountdown() {

View File

@@ -0,0 +1,12 @@
<template>
<div class="flex flex-col items-center justify-center py-16">
<img src="@/assets/images/empty.svg" alt="空状态" class="w-48 h-48 mb-4" />
<p class="text-gray-400 text-base">{{ text }}</p>
</div>
</template>
<script setup>
defineProps({
text: { type: String, default: '暂无数据' }
})
</script>

View File

@@ -132,7 +132,7 @@ import { useRoute, useRouter } from "vue-router";
import { useUserStore } from "@/stores/userStore";
import { useDialogStore } from "@/stores/dialogStore";
import { useEnv } from "@/composables/useEnv";
import { showConfirmDialog } from "vant";
import { useAliyunCaptcha } from "@/composables/useAliyunCaptcha";
import Payment from "@/components/Payment.vue";
import BindPhoneOnlyDialog from "@/components/BindPhoneOnlyDialog.vue";
@@ -200,6 +200,7 @@ const dialogStore = useDialogStore();
const userStore = useUserStore();
const { isWeChat } = useEnv();
const appStore = useAppStore();
const { runWithCaptcha } = useAliyunCaptcha();
// 响应式数据
const showPayment = ref(false);
@@ -320,26 +321,11 @@ function handleBindSuccess() {
}
}
// 处理输入框点击事件
// 处理输入框点击事件(浏览/填写表单无需登录,仅付款时需要登录)
const handleInputClick = async () => {
if (!isLoggedIn.value) {
if (!isWeChat.value && props.type !== 'promotion') {
try {
await showConfirmDialog({
title: '提示',
message: '您需要登录后才能进行查询,是否前往登录?',
confirmButtonText: '前往登录',
cancelButtonText: '取消',
});
router.push('/login');
} catch {
// 用户点击取消,什么都不做
}
}
} else {
if (isWeChat.value && !userStore.mobile && props.type !== 'promotion') {
dialogStore.openBindPhone();
}
// 已登录且在微信环境下未绑定手机号时,提示绑定
if (isLoggedIn.value && isWeChat.value && !userStore.mobile && props.type !== 'promotion') {
dialogStore.openBindPhone();
}
};
@@ -440,22 +426,27 @@ async function sendVerificationCode() {
return;
}
const { data, error } = await useApiFetch("/auth/sendSms")
.post({ mobile: formData.mobile, actionType: "query" })
.json();
if (!error.value && data.value.code === 200) {
showToast({ message: "验证码发送成功", type: "success" });
startCountdown();
nextTick(() => {
const verificationCodeInput = document.getElementById('verificationCode');
if (verificationCodeInput) {
verificationCodeInput.focus();
// 使用滑块验证码保护发送短信接口
runWithCaptcha(
(captchaVerifyParam) =>
useApiFetch("/auth/sendSms")
.post({ mobile: formData.mobile, actionType: "query", captchaVerifyParam })
.json(),
(res) => {
if (res.code === 200) {
showToast({ message: "验证码发送成功", type: "success" });
startCountdown();
nextTick(() => {
const verificationCodeInput = document.getElementById('verificationCode');
if (verificationCodeInput) {
verificationCodeInput.focus();
}
});
} else {
showToast({ message: res.msg || "验证码发送失败,请重试" });
}
});
} else {
showToast({ message: "验证码发送失败,请重试" });
}
}
);
}
let timer = null;

View File

@@ -70,6 +70,7 @@
import { ref, computed, onMounted } from "vue";
import { useRouter } from "vue-router";
const { isWeChat } = useEnv();
import { showToast } from "vant";
const props = defineProps({
data: {
@@ -84,6 +85,10 @@ const props = defineProps({
type: String,
required: true,
},
returnUrl: {
type: String,
default: "",
},
});
const show = defineModel();
@@ -113,6 +118,15 @@ const router = useRouter();
const discountPrice = ref(false); // 是否应用折扣
async function getPayment() {
// 重要安全声明(原弹窗文案,已取消弹窗确认)
// 为保障您的个人信息与资金安全,请您务必知悉以下事项:
//
// 关于平台业务:本平台官方服务仅限于大数据报告查询,不涉及也从未开展“央行征信修复”、“贷款办理”或“征信洗白”等相关业务。请注意,本平台出具的报告仅供决策参考,不可作为任何官方征信凭证或贷款依据。
//
// 关于诈骗警示:任何自称与本平台合作,或以“内部渠道”、“百分百包下款”、“修复征信”等为由,诱导您进行支付的行为,均属欺诈。请您切勿相信,谨慎对待任何支付要求。
//
// 关于安全提示:请您时刻保持警惕,妥善保管个人敏感信息。如遇任何索款要求或可疑承诺,请务必首先通过我平台官方公布的联系方式进行核实,切勿轻信他人。
const { data, error } = await useApiFetch("/pay/payment")
.post({
id: props.id,
@@ -125,10 +139,9 @@ async function getPayment() {
// 测试支付模式:直接跳转到结果页面
if (selectedPaymentMethod.value === "test" || selectedPaymentMethod.value === "test_empty") {
orderNo.value = data.value.data.order_no;
router.push({
path: "/payment/result",
query: { orderNo: data.value.data.order_no },
});
const query = { orderNo: data.value.data.order_no };
if (props.returnUrl) query.returnUrl = props.returnUrl;
router.push({ path: "/payment/result", query });
} else if (selectedPaymentMethod.value === "alipay") {
orderNo.value = data.value.data.order_no;
// 存储订单ID以便支付宝返回时获取
@@ -146,11 +159,13 @@ async function getPayment() {
payload,
function (res) {
if (res.err_msg == "get_brand_wcpay_request:ok") {
// 支付成功,直接跳转到结果页面
router.push({
path: "/payment/result",
query: { orderNo: data.value.data.order_no },
});
// 支付成功:短延迟再跳转,给后端回调与异步任务留出时间,避免结果页查报告报错
showToast({ message: "支付成功,正在跳转...", type: "success" });
setTimeout(() => {
const query = { orderNo: data.value.data.order_no };
if (props.returnUrl) query.returnUrl = props.returnUrl;
router.push({ path: "/payment/result", query });
}, 1500);
}
}
);

View File

@@ -15,11 +15,11 @@
@blur="onBlurPrice" class="!text-3xl" />
</div>
<div class="flex items-center justify-between mt-2">
<div>推广收益为<span class="text-orange-500"> {{ promotionRevenue }} </span></div>
<div>推广收益为<span class="text-orange-500"> {{ pricingResult.promotionRevenue }} </span></div>
</div>
<div class="flex items-center justify-between mt-2">
<div>底价成本为<span class="text-orange-500"> {{ baseCost }} </span></div>
<div>提价成本为<span class="text-orange-500"> {{ raiseCost }} </span></div>
<div>底价成本为<span class="text-orange-500"> {{ pricingResult.baseCost }} </span></div>
<div>提价成本为<span class="text-orange-500"> {{ pricingResult.raiseCost }} </span></div>
</div>
</div>
<div class="card m-4">
@@ -42,6 +42,8 @@
</template>
<script setup>
import { calculatePromotionPricing, validatePrice } from '@/utils/promotionPricing'
const props = defineProps({
defaultPrice: {
type: Number,
@@ -57,108 +59,22 @@ const emit = defineEmits(["change"])
const show = defineModel("show")
const price = ref(null)
watch(show, () => {
price.value = defaultPrice.value
})
const costPrice = computed(() => {
if (!productConfig.value) return 0.00
// 新代理系统:成本价 = 实际底价actual_base_price+ 提价成本
const actualBasePrice = Number(productConfig.value.actual_base_price) || 0;
const priceNum = Number(price.value) || 0;
const priceThreshold = Number(productConfig.value.price_threshold) || 0;
const priceFeeRate = Number(productConfig.value.price_fee_rate) || 0;
// 计算提价成本
let priceCost = 0;
if (priceNum > priceThreshold) {
priceCost = (priceNum - priceThreshold) * priceFeeRate;
}
// 总成本 = 实际底价 + 提价成本
const totalCost = actualBasePrice + priceCost;
return safeTruncate(totalCost);
const pricingResult = computed(() => {
return calculatePromotionPricing(price.value, productConfig.value)
})
const rateFormat = (rate) => {
return rate * 100 + '%';
}
const baseCost = computed(() => {
if (!productConfig.value) return "0.00";
const actualBasePrice = Number(productConfig.value.actual_base_price) || 0;
return safeTruncate(actualBasePrice);
})
const raiseCost = computed(() => {
if (!productConfig.value) return "0.00";
const priceNum = Number(price.value) || 0;
const priceThreshold = Number(productConfig.value.price_threshold) || 0;
const priceFeeRate = Number(productConfig.value.price_fee_rate) || 0;
let priceCost = 0;
if (priceNum > priceThreshold) {
priceCost = (priceNum - priceThreshold) * priceFeeRate;
}
return safeTruncate(priceCost);
})
const promotionRevenue = computed(() => {
return safeTruncate(price.value - costPrice.value)
});
// 价格校验与修正逻辑
const validatePrice = (currentPrice) => {
const min = productConfig.value.price_range_min;
const max = productConfig.value.price_range_max;
let newPrice = Number(currentPrice);
let message = '';
// 处理无效输入
if (isNaN(newPrice)) {
newPrice = defaultPrice.value;
return { newPrice, message: '输入无效,请输入价格' };
}
// 处理小数位数(兼容科学计数法)
try {
const priceString = newPrice.toString()
const [_, decimalPart = ""] = priceString.split('.');
console.log(priceString, decimalPart)
// 当小数位数超过2位时处理
if (decimalPart.length > 2) {
newPrice = parseFloat(safeTruncate(newPrice));
message = '价格已自动格式化为两位小数';
}
} catch (e) {
console.error('价格格式化异常:', e);
}
// 范围校验(基于可能格式化后的值)
if (newPrice < min) {
message = `价格不能低于 ${min}`;
newPrice = min;
} else if (newPrice > max) {
message = `价格不能高于 ${max}`;
newPrice = max;
}
console.log(newPrice, message)
return { newPrice, message };
}
function safeTruncate(num, decimals = 2) {
if (isNaN(num) || !isFinite(num)) return "0.00";
const factor = 10 ** decimals;
const scaled = Math.trunc(num * factor);
const truncated = scaled / factor;
return truncated.toFixed(decimals);
}
const isManualConfirm = ref(false)
const onConfirm = () => {
if (isManualConfirm.value) return
const { newPrice, message } = validatePrice(price.value)
const { newPrice, message } = validatePrice(price.value, productConfig.value, defaultPrice.value)
if (message) {
price.value = newPrice
showToast({ message });
@@ -166,11 +82,10 @@ const onConfirm = () => {
emit("change", price.value)
show.value = false
}
}
const onBlurPrice = () => {
const { newPrice, message } = validatePrice(price.value)
const { newPrice, message } = validatePrice(price.value, productConfig.value, defaultPrice.value)
if (message) {
isManualConfirm.value = true
price.value = newPrice

View File

@@ -6,11 +6,14 @@ import { useAgentStore } from "@/stores/agentStore";
import { useUserStore } from "@/stores/userStore";
import { showToast } from "vant";
import { realNameAuth } from "@/api/agent";
import useApiFetch from "@/composables/useApiFetch";
import { useAliyunCaptcha } from "@/composables/useAliyunCaptcha";
const router = useRouter();
const dialogStore = useDialogStore();
const agentStore = useAgentStore();
const userStore = useUserStore();
const { runWithCaptcha } = useAliyunCaptcha();
// 表单数据
const realName = ref("");
const idCard = ref("");
@@ -59,18 +62,22 @@ async function sendVerificationCode() {
showToast({ message: "请输入有效的手机号" });
return;
}
const { data, error } = await useApiFetch("auth/sendSms")
.post({ mobile: phoneNumber.value, actionType: "realName" })
.json();
if (data.value && !error.value) {
if (data.value.code === 200) {
showToast({ message: "获取成功" });
startCountdown();
} else {
showToast(data.value.msg);
// 使用滑块验证码保护发送短信接口
runWithCaptcha(
(captchaVerifyParam) =>
useApiFetch("auth/sendSms")
.post({ mobile: phoneNumber.value, actionType: "realName", captchaVerifyParam })
.json(),
(res) => {
if (res.code === 200) {
showToast({ message: "获取成功" });
startCountdown();
} else {
showToast(res.msg || "获取验证码失败");
}
}
}
);
}
function startCountdown() {

View File

@@ -203,6 +203,19 @@ const getFeatureIcon = (apiId) => {
JRZQ4AA8: "/inquire_icons/huankuanyali.svg",
QCXG7A2B: "/inquire_icons/mingxiacheliang.svg",
QCXG9P1C: "/inquire_icons/mingxiacheliang.svg",
QCXG4D2E: "/inquire_icons/mingxiacheliang.svg",
QCXG5F3A: "/inquire_icons/mingxiacheliang.svg",
QCXG5U0Z: "/inquire_icons/mingxiacheliang.svg",
QCXGGB2Q: "/inquire_icons/mingxiacheliang.svg",
QCXGYTS2: "/inquire_icons/mingxiacheliang.svg",
QCXG1H7Y: "/inquire_icons/mingxiacheliang.svg",
QCXG4I1Z: "/inquire_icons/mingxiacheliang.svg",
QCXG1U4U: "/inquire_icons/mingxiacheliang.svg",
QCXG3Y6B: "/inquire_icons/mingxiacheliang.svg",
QCXG3Z3L: "/inquire_icons/mingxiacheliang.svg",
QCXG6B4E: "/inquire_icons/mingxiacheliang.svg",
QCXGP00W: "/inquire_icons/mingxiacheliang.svg",
QCXGY7F2: "/inquire_icons/mingxiacheliang.svg",
BehaviorRiskScan: "/inquire_icons/fengxianxingwei.svg",
IVYZ5733: "/inquire_icons/hunyinzhuangtai.svg",
IVYZ81NC: "/inquire_icons/hunyinzhuangtai.svg",

View File

@@ -0,0 +1,172 @@
import { showToast, showLoadingToast, closeToast } from "vant";
import useApiFetch from "@/composables/useApiFetch";
// 阿里云验证码场景 ID
const ALIYUN_CAPTCHA_SCENE_ID = "wynt39to";
// 是否启用加密模式(通过环境变量控制,非加密模式时前端不调用后端获取 EncryptedSceneId
const ENABLE_ENCRYPTED =
import.meta.env.VITE_ALIYUN_CAPTCHA_ENCRYPTED === "false";
let captchaInitialised = false;
/** 首次初始化后SDK 会异步调用 getInstance用此 Promise 在实例就绪后再 show */
let captchaReadyPromise = null;
let captchaReadyResolve = null;
async function ensureCaptchaInit() {
if (captchaInitialised || typeof window === "undefined") return;
if (typeof window.initAliyunCaptcha !== "function") return;
captchaInitialised = true;
window.captcha = null;
window.__lastBizResponse = null;
window.__onCaptchaBizSuccess = null;
captchaReadyPromise = new Promise((resolve) => {
captchaReadyResolve = resolve;
});
// 非加密模式:仅传 SceneId不调用后端接口
if (!ENABLE_ENCRYPTED) {
window.initAliyunCaptcha({
SceneId: ALIYUN_CAPTCHA_SCENE_ID,
mode: "popup",
element: "#captcha-element",
getInstance(instance) {
window.captcha = instance;
if (typeof captchaReadyResolve === "function") {
captchaReadyResolve();
captchaReadyResolve = null;
}
},
captchaVerifyCallback(param) {
return typeof window.__captchaVerifyCallback === "function"
? window.__captchaVerifyCallback(param)
: Promise.resolve({
captchaResult: false,
bizResult: false,
});
},
onBizResultCallback(bizResult) {
if (typeof window.__onBizResultCallback === "function") {
window.__onBizResultCallback(bizResult);
}
window.__lastBizResponse = null;
window.__onCaptchaBizSuccess = null;
},
slideStyle: { width: 360, height: 40 },
language: "cn",
});
return;
}
// 加密模式:先从后端获取 EncryptedSceneId再初始化
const { data, error } = await useApiFetch("/captcha/encryptedSceneId")
.post()
.json();
const resp = data?.value;
const encryptedSceneId = resp?.data?.encryptedSceneId;
if (error?.value || !encryptedSceneId) {
showToast({ message: "获取验证码参数失败,请稍后重试" });
captchaInitialised = false;
captchaReadyPromise = null;
captchaReadyResolve = null;
return;
}
window.initAliyunCaptcha({
SceneId: ALIYUN_CAPTCHA_SCENE_ID,
EncryptedSceneId: encryptedSceneId,
mode: "popup",
element: "#captcha-element",
getInstance(instance) {
window.captcha = instance;
if (typeof captchaReadyResolve === "function") {
captchaReadyResolve();
captchaReadyResolve = null;
}
},
captchaVerifyCallback(param) {
return typeof window.__captchaVerifyCallback === "function"
? window.__captchaVerifyCallback(param)
: Promise.resolve({ captchaResult: false, bizResult: false });
},
onBizResultCallback(bizResult) {
if (typeof window.__onBizResultCallback === "function") {
window.__onBizResultCallback(bizResult);
}
window.__lastBizResponse = null;
window.__onCaptchaBizSuccess = null;
},
slideStyle: { width: 360, height: 40 },
language: "cn",
});
}
/**
* 阿里云滑块验证码通用封装。
* 依赖 index.html 中已加载的 AliyunCaptcha.js初始化在首次调起时执行。
*
* @param { (captchaVerifyParam: string) => Promise<{ data: Ref, error: Ref }> } bizVerify - 业务请求函数,接收滑块参数,返回 useApiFetch 的 { data, error }
* @param { (res: any) => void } [onSuccess] - 业务成功回调code===200 时调用,传入接口返回的 data.value
*/
export function useAliyunCaptcha() {
/**
* 先弹出滑块,通过后执行 bizVerify(captchaVerifyParam),再根据结果调用 onSuccess。
*/
async function runWithCaptcha(bizVerify, onSuccess) {
if (typeof window === "undefined") {
showToast({ message: "验证码仅支持浏览器环境" });
return;
}
const loading = showLoadingToast({
message: "安全验证加载中...",
forbidClick: true,
duration: 0,
loadingType: "spinner",
});
try {
window.__captchaVerifyCallback = async (captchaVerifyParam) => {
window.__lastBizResponse = null;
const { data, error } = await bizVerify(captchaVerifyParam);
const result = data?.value ?? data;
if (error?.value || !result) {
return { captchaResult: false, bizResult: false };
}
window.__lastBizResponse = result;
const captchaOk = result.captchaVerifyResult !== false;
const bizOk = result.code === 200;
return { captchaResult: captchaOk, bizResult: bizOk };
};
window.__onBizResultCallback = (bizResult) => {
if (
bizResult === true &&
window.__lastBizResponse &&
typeof window.__onCaptchaBizSuccess === "function"
) {
window.__onCaptchaBizSuccess(window.__lastBizResponse);
}
};
await ensureCaptchaInit();
// 首次初始化时 SDK 会异步调用 getInstance需等待实例就绪后再 show
if (captchaReadyPromise) {
await captchaReadyPromise;
captchaReadyPromise = null;
}
if (!window.captcha) {
showToast({ message: "验证码未加载,请刷新页面重试" });
return;
}
window.__onCaptchaBizSuccess = onSuccess;
window.captcha.show();
} finally {
closeToast();
}
}
return { runWithCaptcha };
}
export default useAliyunCaptcha;

View File

@@ -6,12 +6,12 @@ export function useSEO() {
// 默认SEO信息
const defaultSEO = {
title: "全能查|大数据风险报告查询与代理平台,支持个人和企业多场景风控应用",
title: "全能查官网_个人婚姻状态报告_综合风险排查工具箱",
description:
"全能查,专业大数据风险报告查询与代理平台,支持个人信用查询、小微企业风控、贷前风险背调等多场景报告应用,免费开通代理权限,助力高效识别信用与风险。",
"全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。",
keywords:
"大数据风险报告查询、大数据风险评估、大数据分析报告、个人大数据风险查询、小微企业风险、贷前风险背调、代理管理平台、免费开通代理、风险管控平台、信用风险分析、企业风险报告、贷前信用审核、失信人名单查询、被执行人信息、信用黑名单查询",
url: "https://www.zhinengcha.cn",
"全能查,婚姻状态核实,风险排查工具,个人风险预警,第三方背调,商业信用评估",
url: "https://www.quannengcha.com",
};
// 页面SEO配置
@@ -137,7 +137,7 @@ export function useSEO() {
mainEntity: {
"@type": "Organization",
name: "全能查",
url: "https://www.zhinengcha.cn/",
url: "https://www.quannengcha.com/",
description:
"专业大数据风险报告查询与代理平台,支持个人和企业多场景风控应用",
},
@@ -153,11 +153,59 @@ export function useSEO() {
const updateSEOByRoute = () => {
const routeConfigs = {
"/": {
title: "全能查|大数据风险报告查询与代理平台,支持个人和企业多场景风控应用",
title: "全能查官网_个人婚姻状态报告_综合风险排查工具箱",
description:
"全能查,专业大数据风险报告查询与代理平台,支持个人信用查询、小微企业风控、贷前风险背调等多场景报告应用,免费开通代理权限,助力高效识别信用与风险。",
"全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。",
keywords:
"大数据风险报告查询、大数据风险评估、大数据分析报告、个人大数据风险查询、小微企业风险、贷前风险背调、代理管理平台、免费开通代理、风险管控平台、信用风险分析、企业风险报告、贷前信用审核、失信人名单查询、被执行人信息、信用黑名单查询",
"全能查,婚姻状态核实,风险排查工具,个人风险预警,第三方背调,商业信用评估",
},
"/agent/system-guide": {
title: "全能查合作政策指南_合作伙伴权益与结算说明_官方文档",
description:
"全能查官方合作体系说明文档。详细解读合作伙伴的等级权益、服务费结算标准及晋升机制。致力于构建公平、透明的商业合作生态,助力合作伙伴快速上手业务。",
keywords: "合作伙伴政策,服务费结算,渠道等级说明,业务操作指南,代理系统后台",
},
// 个人查询
'/inquire/riskassessment': {
title: '个人综合风险分析_履约能力画像_多维数据检测_全能查',
description: '全能查个人风险报告为您提供全方位的信用健康度参考。基于公开数据深度解析综合风险指数、司法关联风险、历史履约趋势及潜在的负面标签。数据客观中立,帮助用户建立良好的个人履约记录管理意识。',
keywords: '个人风险检测,履约能力分析,综合风险指数,信用健康度,个人数据画像'
},
// 企业查询
'/inquire/companyinfo': {
title: '企业工商信用画像_经营异常与商业风险透视_全能查',
description: '全能查企业版深度透视商业真相。聚合工商、司法及税务公开数据,核验企业经营异常名录、行政处罚、法律诉讼及股权穿透信息。全方位评估合作伙伴的商业健康度,规避合同违约风险。',
keywords: '企业信用评估,工商背景核验,商业风险评估,公司经营异常,合作方背景核实'
},
// 贷前风险
'/inquire/preloanbackgroundcheck': {
title: '综合履约评分检测_多平台履约记录分析_个人财务履约报告_天远助手',
description: '天远助手提供专业的个人履约健康度体检服务。基于多维大数据分析,检测您的综合评分波动、历史履约记录及潜在的风险标签。本服务旨在帮助用户优化个人数据画像,提升信用管理意识,不提供任何信贷金融服务。',
keywords: '综合评分检测,多重履约压力分析,履约能力评估,综合评分优化,个人数据画像'
},
// 婚恋风险
'/inquire/marriage': {
title: '婚前综合背景了解_情感安全风险评估_家庭履约分析_全能查',
description: '全能查婚恋风险报告基于合法公开数据,辅助评估对象的婚前背景。核心核验司法涉诉记录、失信被执行历史、多重履约能力及不良社会标签。拒绝情感盲区,用数据守护您的家庭与财产安全。',
keywords: '婚前背景报告,恋爱对象风险,情感安全评估,司法记录核验,家庭风险防范'
},
// 入职背调
'/inquire/backgroundcheck': {
title: '职场背景核验报告_候选人职业风险与竞业核验_全能查',
description: '全能查为企业提供专业的入职背调服务。一键筛查候选人的学历背景、涉及的商业利益冲突、劳动仲裁记录及社会不良风险。数据实时合规,降低企业用工试错成本,提升招聘决策效率。',
keywords: '员工入职背调,职业背景核实,竞业限制评估,职场信用报告,候选人风险筛查'
},
// 家政风险
'/inquire/homeservice': {
title: '家政人员背景核实_保姆月嫂司法安全评估_全能查',
description: '全能查针对家庭用工场景,提供客观的家政人员背景核验服务。重点核验身份信息、司法涉诉记录及失信历史。辅助雇主识别高危人员,让居家养老育儿更安心。',
keywords: '保姆背景核验,家政风险筛查,月嫂司法记录,雇佣安全评估,家政人员核验'
},
'/inquire/consumerFinanceReport': {
title: '个人履约能力评估_经济风险与收支压力参考_全能查',
description: '全能查履约报告基于大数据算法,提供个人经济稳定性的客观分析。多维度检测综合履约分、经济关联风险及潜在的资金压力指数。本服务仅提供大数据层面的风险参考,助您优化财务管理。',
keywords: '履约能力评估,经济风险指数,综合评分波动,资金压力分析,财务健康度'
},
"/agent": {
title: "全能查代理 - 免费开通代理权限 | 大数据风险报告代理",
@@ -191,6 +239,11 @@ export function useSEO() {
"全能查客服中心,提供在线客服支持、技术咨询、问题反馈等服务,确保用户获得及时有效的帮助。",
keywords: "客服中心, 在线客服, 技术支持, 问题反馈, 全能查客服",
},
"/promote": {
title: "全能查合伙人计划_风控平台系统招商_渠道合作平台_全能查",
description:"全能查开放全国渠道合作,提供零门槛的风险评估系统接入服务。一键开通独立后台,支持婚恋、职场、家政及商业风控等多场景报告推广。正规项目,结算透明,赋能流量方实现合规商业价值。",
keywords:"风控系统代理,风险评估平台招商,平台渠道合作,企业服务创业,全能查合伙人",
},
};
const currentPath = route?.path || "/";
@@ -198,7 +251,7 @@ export function useSEO() {
updateSEO({
...config,
url: `https://www.zhinengcha.cn${currentPath}`,
url: `https://www.quannengcha.com${currentPath}`,
});
};

View File

@@ -0,0 +1,19 @@
import { computed } from 'vue'
import { payloadAsArray, payloadAsObject, unwrapVehiclePayload } from '@/utils/vehiclePayload'
const defaultProps = {
data: null,
params: () => ({}),
apiId: '',
index: 0,
notifyRiskStatus: () => {},
}
export function useVehiclePayload(props = defaultProps) {
const payload = computed(() => unwrapVehiclePayload(props.data))
const obj = computed(() => payloadAsObject(props.data))
const list = computed(() => payloadAsArray(props.data))
const params = computed(() => props.params || {})
return { payload, obj, list, params }
}

View File

@@ -0,0 +1,41 @@
import { defineAsyncComponent } from 'vue'
import { VEHICLE_API_TITLES } from './vehicleReportRegistry'
/** 已单独实现 UI 的 apiID */
const VEHICLE_COMPONENT_LOADERS = {
QCXG7A2B: () => import('@/ui/CQCXG7A2B.vue'),
QCXG9P1C: () => import('@/ui/CQCXG9P1C.vue'),
QCXG4D2E: () => import('@/ui/CQCXG4D2E.vue'),
QCXG5F3A: () => import('@/ui/CQCXG5F3A.vue'),
QCXG5U0Z: () => import('@/ui/CQCXG5U0Z.vue'),
QCXGGB2Q: () => import('@/ui/CQCXGGB2Q.vue'),
QCXGYTS2: () => import('@/ui/CQCXGYTS2.vue'),
QCXG1H7Y: () => import('@/ui/CQCXG1H7Y.vue'),
QCXG4I1Z: () => import('@/ui/CQCXG4I1Z.vue'),
QCXG1U4U: () => import('@/ui/CQCXG1U4U.vue'),
QCXG3Y6B: () => import('@/ui/CQCXG3Y6B.vue'),
QCXG3Z3L: () => import('@/ui/CQCXG3Z3L.vue'),
QCXG6B4E: () => import('@/ui/CQCXG6B4E.vue'),
QCXGP00W: () => import('@/ui/CQCXGP00W.vue'),
QCXGY7F2: () => import('@/ui/CQCXGY7F2.vue'),
}
const fallbackLoader = () => import('@/ui/CQCXGFallback.vue')
export const vehicleFeatureMap = Object.fromEntries(
Object.entries(VEHICLE_API_TITLES).map(([apiId, name]) => {
const loader = VEHICLE_COMPONENT_LOADERS[apiId] || fallbackLoader
return [
apiId,
{
name,
component: defineAsyncComponent(loader),
},
]
}),
)
/** 车辆类模块默认低风险权重 */
export const vehicleFeatureRiskLevels = Object.fromEntries(
Object.keys(VEHICLE_API_TITLES).map(id => [id, 10]),
)

View File

@@ -0,0 +1,45 @@
/**
* 车辆类报告模块apiID产品能力编码→ 展示名称
* 与 qncV4uni-app、后端 ProductFeature / 上游 api 约定对齐
*/
export const VEHICLE_API_TITLES = {
QCXG9F5C: '疑似营运车辆注册平台数',
QCXG3B8Z: '疑似运营车辆查询(月度里程)',
QCXGP1W3: '疑似运营车辆查询(季度里程)',
QCXGM7R9: '疑似运营车辆查询(半年度里程)',
QCXGU2K4: '疑似运营车辆查询(年度里程)',
QCXGY7F2: '二手车 VIN 估值',
QCXG5U0Z: '车辆静态信息查询',
QCXG3M7Z: '人车关系核验ETC',
QCXG1U4U: '车辆里程记录(混合查询)',
QCXG2T6S: '车辆里程记录(品牌查询)',
QCXG3Y6B: '车辆维保简版查询',
QCXG3Z3L: '车辆维保详细版查询',
QCXG1H7Y: '车辆过户简版查询',
QCXG4I1Z: '车辆过户详版查询',
QCXGGB2Q: '车辆二要素核验简版',
QCXGP00W: '车辆出险详版查询',
QCXGYTS2: '人车核验(详版)',
QCXGGJ3A: '车辆 VIN 码查询号牌简版',
QCXGJJ2A: '车辆 VIN 码查车辆信息详版',
QCXG5F3A: '名下车辆车牌查询 B',
QCXG4D2E: '名下车辆数量查询',
QCXG6B4E: '车辆出险记录核验',
QCXG8A3D: '车辆七项信息核验',
QCXG9P1C: '名下车辆车牌查询 A',
QCXG7A2B: '名下车辆',
}
export function getVehicleModuleTitle(apiId, featureName) {
if (!apiId || apiId === '__UNLABELED__') {
if (featureName?.trim())
return featureName.trim()
return '报告模块'
}
const t = VEHICLE_API_TITLES[apiId]
if (t)
return t
if (featureName?.trim())
return featureName.trim()
return apiId
}

View File

@@ -2,9 +2,10 @@
<router-view />
<van-popup v-model:show="showPopup" round @click-overlay="onClickOverlay">
<div class="popup-content text-center p-8">
<div v-if="currentNotify?.title" class="text-lg font-bold mb-4">{{ currentNotify.title }}</div>
<div v-html="currentNotify?.content"></div>
<div class="flex justify-center">
<van-button type="primary" @click="showPopup = false" class="w-24">关闭</van-button>
<van-button type="primary" @click="onClosePopup" class="w-24">关闭</van-button>
</div>
</div>
</van-popup>
@@ -18,12 +19,16 @@ import { useRoute } from 'vue-router'
const showPopup = ref(false)
const notify = ref([])
const currentNotify = ref(null)
const pendingNotifyQueue = ref([])
const shownNotificationKeys = ref(new Set())
const SESSION_KEY = 'qnc_webview_shown_notifications'
// 获取当前页面路径
const route = useRoute()
// 获取通知数据
onMounted(() => {
loadShownNotificationKeys()
getGlobalNotify()
})
@@ -33,60 +38,128 @@ const getGlobalNotify = async () => {
.get()
.json()
if (data.value && !error.value) {
if (data.value !== 200) {
notify.value = data.value.data.notifications
checkNotification() // 在获取数据后检查通知
}
if (data.value && !error.value && data.value.code === 200 && data.value.data) {
notify.value = data.value.data.notifications ?? []
checkNotification()
}
}
// 判断当前时间是否在通知的时间范围内
/** 与后台「展示时间」一致:未配置或起止相同视为「全天」,任意时刻都算在范围内 */
const isWithinTimeRange = (startTime, endTime) => {
const s = (startTime || '').trim()
const e = (endTime || '').trim()
if (!s && !e) return true
if (s === e) return true
const now = new Date()
// 获取当前时间的小时和分钟
const currentMinutes = now.getHours() * 60 + now.getMinutes()
// 将 startTime 和 endTime 转换为分钟数
const startParts = startTime.split(':').map(Number)
const endParts = endTime.split(':').map(Number)
const startMinutes = startParts[0] * 60 + startParts[1]
const endMinutes = endParts[0] * 60 + endParts[1]
// 如果 endTime 小于 startTime表示跨越了午夜
if (endMinutes < startMinutes) {
// 判断当前时间是否在 [startTime, 23:59:59] 或 [00:00:00, endTime] 之间
return currentMinutes >= startMinutes || currentMinutes < endMinutes
const toMinutes = (t) => {
const parts = t.split(':').map(Number)
const h = parts[0] ?? 0
const m = parts[1] ?? 0
return h * 60 + m
}
const startMinutes = toMinutes(s)
const endMinutes = toMinutes(e)
// 普通情况,直接判断时间是否在范围内
if (endMinutes < startMinutes) {
return currentMinutes >= startMinutes || currentMinutes <= endMinutes
}
return currentMinutes >= startMinutes && currentMinutes <= endMinutes
}
/** 当前路由是否与通知配置的页面一致 */
const matchesNotificationPage = (page) => {
const p = (page || '').trim()
const cur = route.path || ''
if (p === cur) return true
if (p === '/' && (cur === '/' || cur === '')) return true
if (cur.startsWith('/app/') && p === cur.replace('/app/', '/')) return true
if (p.startsWith('/app/') && cur === p.replace('/app/', '/')) return true
return false
}
const buildNotificationKey = (notification) => {
return [
notification.title ?? '',
notification.content ?? '',
notification.notificationPage ?? '',
notification.startDate ?? '',
notification.endDate ?? '',
notification.startTime ?? '',
notification.endTime ?? ''
].join('|')
}
const loadShownNotificationKeys = () => {
const raw = sessionStorage.getItem(SESSION_KEY)
if (!raw) return
try {
const parsed = JSON.parse(raw)
if (Array.isArray(parsed)) {
shownNotificationKeys.value = new Set(parsed)
}
} catch {
shownNotificationKeys.value = new Set()
}
}
const saveShownNotificationKeys = () => {
sessionStorage.setItem(SESSION_KEY, JSON.stringify([...shownNotificationKeys.value]))
}
const hasShownNotification = (notification) => {
return shownNotificationKeys.value.has(buildNotificationKey(notification))
}
const markNotificationShown = (notification) => {
shownNotificationKeys.value.add(buildNotificationKey(notification))
saveShownNotificationKeys()
}
const showNextNotification = () => {
const next = pendingNotifyQueue.value.shift()
if (!next) {
currentNotify.value = null
showPopup.value = false
return
}
currentNotify.value = next
showPopup.value = true
markNotificationShown(next)
}
// 检查通知并更新showPopup
const checkNotification = () => {
// 遍历通知数组,找到第一个符合条件的通知
for (let notification of notify.value) {
// 判断时间是否符合当前时间
const isTimeValid = isWithinTimeRange(notification.startTime, notification.endTime)
// 判断页面是否符合
if (showPopup.value) return
if (isTimeValid && notification.notificationPage === route.path) {
currentNotify.value = notification
showPopup.value = true
break // 只显示第一个符合的通知
const matchedNotifications = []
for (let notification of notify.value) {
const isTimeValid = isWithinTimeRange(notification.startTime, notification.endTime)
const isPageValid = matchesNotificationPage(notification.notificationPage)
const isShown = hasShownNotification(notification)
if (isTimeValid && isPageValid && !isShown) {
matchedNotifications.push(notification)
}
}
pendingNotifyQueue.value = matchedNotifications
showNextNotification()
}
// 监听路由变化
watch(() => route.path, () => {
checkNotification() // 每次路由变化时重新判断通知
checkNotification()
})
// 关闭弹窗
const onClosePopup = () => {
showNextNotification()
}
const onClickOverlay = () => {
showPopup.value = false
onClosePopup()
}
</script>

View File

@@ -509,7 +509,7 @@ router.afterEach((to) => {
const seoConfig = {
title: `${to.meta.title} - 全能查`,
description: `全能查${to.meta.title}页面,提供专业的大数据风险管控服务。`,
url: `https://www.zhinengcha.cn${to.path}`,
url: `https://www.quannengcha.com${to.path}`,
};
updateSEO(seoConfig);
}

View File

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

View File

@@ -465,12 +465,12 @@ const riskScore = computed(() => {
const lowRiskCount = summaryData.value.byRiskLevel.find(item => item.id === 'low')?.triggered || 0;
// 计算风险分数
// 高风险项:每个扣 30 分
// 中风险项:每个扣 15
// 低风险项:每个扣 5 分
// 高风险项(无法收回):每个扣 40 分
// 中风险项(严重逾期):每个扣 20
// 低风险项(短期逾期):每个扣 5 分
let score = 100;
score -= highRiskCount * 30;
score -= mediumRiskCount * 15;
score -= highRiskCount * 40;
score -= mediumRiskCount * 20;
score -= lowRiskCount * 5;
return Math.max(0, Math.min(100, score));
@@ -544,11 +544,11 @@ onMounted(() => {
</div>
<p class="text-sm text-gray-600 mt-1">
{{
riskSeverity.level === 'critical'
? '存在无法收回风险,请立即处理'
: riskSeverity.level === 'warning'
? '存在严重逾期风险,建议尽快处理'
: '存在短期逾期风险,请注意处理'
riskSeverity.level === 'critical'
? '存在无法收回风险,请立即处理'
: riskSeverity.level === 'warning'
? '存在严重逾期风险,建议尽快处理'
: '存在短期逾期风险,请注意处理'
}}
</p>
</div>

View File

@@ -57,7 +57,7 @@
<!-- 所有风险类型列表 -->
<div class="space-y-3">
<!-- 正常人员 -->
<!-- 人员状态 -->
<div class="rounded-lg p-4 border-2 relative" :class="getRiskItemClass('0')">
<div
:class="['absolute top-0 right-0 px-1.5 py-0.5 text-sm font-bold text-white rounded-bl-lg rounded-tr-lg', getRiskBadgeClass('0')]">
@@ -65,13 +65,15 @@
</div>
<div class="flex items-center pr-12">
<div class="w-8 h-8 mr-3 flex-shrink-0 flex items-center justify-center">
<img :src="getRiskItemIcon('0')" alt="正常人员" class="w-8 h-8 object-contain" />
<img :src="getRiskItemIcon('0')" :alt="isNormalPerson ? '正常人员' : '存在风险'"
class="w-8 h-8 object-contain" />
</div>
<div class="flex-1">
<div class="font-bold text-sm" :class="getRiskItemTextColor('0')">
{{ getRiskTypeInfo('0').text }}
{{ isNormalPerson ? '正常人员' : '人员状态' }}
</div>
<div class="text-sm text-[#999999] mt-0.5">{{ getRiskTypeInfo('0').description }}</div>
<div class="text-sm text-[#999999] mt-0.5">{{ isNormalPerson ? '无不良记录,属于正常人员' : '存在不良记录风险'
}}</div>
</div>
</div>
</div>
@@ -460,12 +462,12 @@ const isHit = (code) => {
// 如果level是'0',则正常人员命中
return hitRiskCodes.value.includes('0')
}
// 如果直接包含该代码,则命中
if (hitRiskCodes.value.includes(code)) {
return true
}
// 对于父级类型A、B、C、D如果子类型命中父类型也算命中
if (code === 'A') {
// 如果 A1、A2、A3、A4、A5 任何一个命中A 也算命中
@@ -483,7 +485,7 @@ const isHit = (code) => {
// 如果 D1、D2、D3、D4、D5 任何一个命中D 也算命中
return ['D1', 'D2', 'D3', 'D4', 'D5'].some(subCode => hitRiskCodes.value.includes(subCode))
}
return false
}

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

@@ -0,0 +1,481 @@
<script setup lang="ts">
import { computed } from 'vue';
// 接收父组件传入的 props
const props = defineProps({
data: Object,
params: Object,
});
// 定义组件名称,用于在控制台输出调试信息
const componentName = 'IVYZ0S0D';
// 将 props.data 赋值给 reportData 变量
let reportData: any = props.data || {};
// 如果 reportData 不为空,则将其赋值给变量
if (reportData) {
console.log(`${componentName} 组件接收到的数据:`, reportData);
} else {
console.log(`${componentName} 组件未接收到数据`);
}
// 获取状态描述文本
const getStatusText = (value: number) => {
if (value === 1) return '未命中';
if (value === 2) return '命中';
return '未知';
};
// 获取通知函状态描述文本
const getNoticeLetterStatusText = (value: number) => {
if (value === 1) return '未命中';
if (value === 2) return '命中';
return '未知';
};
// 获取通知函期间描述文本
const getNoticeLetterPeriodText = (period: number) => {
const periodMap: Record<number, string> = {
0: '没有被发送通知函',
1: '近2年内',
2: '2-4年',
3: '5年以上'
};
return periodMap[period] || '未知期间';
};
// 获取背景颜色
const getBackgroundColor = (value: number) => {
if (value === 1) return '#e8f5e8'; // 浅绿色
if (value === 2) return '#ffe8e8'; // 浅红色
return '#f5f5f5'; // 默认灰色
};
// 获取边框颜色
const getBorderColor = (value: number) => {
if (value === 1) return '#4caf50'; // 绿色边框
if (value === 2) return '#f44336'; // 红色边框
return '#ccc'; // 默认灰色边框
};
// 判断是否应该隐藏该条目(如果是带时间范围的"未命中"
const shouldHideItem = (itemText: string) => {
// 检查是否包含时间范围关键词并且结果是"未命中"
const timeRangeKeywords = ['近2年', '近3年', '近4年', '近5年', '2-4年', '5年以上'];
const isTimeRangeItem = timeRangeKeywords.some(keyword => itemText.includes(keyword));
const isNoRisk = itemText.includes('未命中');
// 如果是时间范围项目且结果是"未命中",则隐藏
return isTimeRangeItem && isNoRisk;
};
// 获取风险类型数组 - 所有模块都显示
const riskTypes = computed(() => {
const risks: {title: string, value: number, details: string | string[], bgColor: string, borderColor: string}[] = [];
// 总体风险
if (reportData.risk_flag !== undefined) {
risks.push({
title: '总体风险',
value: reportData.risk_flag,
details: getStatusText(reportData.risk_flag),
bgColor: getBackgroundColor(reportData.risk_flag),
borderColor: getBorderColor(reportData.risk_flag)
});
}
// 失信风险
if (reportData.dishonesty && reportData.dishonesty.dishonesty !== undefined) {
risks.push({
title: '失信风险',
value: reportData.dishonesty.dishonesty,
details: getStatusText(reportData.dishonesty.dishonesty),
bgColor: getBackgroundColor(reportData.dishonesty.dishonesty),
borderColor: getBorderColor(reportData.dishonesty.dishonesty)
});
}
// 高消费限制风险
if (reportData.high_consumption && reportData.high_consumption.high_consumption !== undefined) {
risks.push({
title: '高消费限制风险',
value: reportData.high_consumption.high_consumption,
details: getStatusText(reportData.high_consumption.high_consumption),
bgColor: getBackgroundColor(reportData.high_consumption.high_consumption),
borderColor: getBorderColor(reportData.high_consumption.high_consumption)
});
}
// 劳动争议风险
if (reportData.labor_disputes) {
let details: string[] = [];
if (reportData.labor_disputes.labor_disputes !== undefined) {
details.push(`当前: ${getStatusText(reportData.labor_disputes.labor_disputes)}`);
}
if (reportData.labor_disputes.labor_disputes_3y !== undefined) {
const detail = `近3年: ${getStatusText(reportData.labor_disputes.labor_disputes_3y)}`;
if (!shouldHideItem(detail)) {
details.push(detail);
}
}
if (reportData.labor_disputes.labor_disputes_5y !== undefined) {
const detail = `近5年: ${getStatusText(reportData.labor_disputes.labor_disputes_5y)}`;
if (!shouldHideItem(detail)) {
details.push(detail);
}
}
if (reportData.labor_disputes.labor_contract !== undefined) {
details.push(`劳动合同: ${getStatusText(reportData.labor_disputes.labor_contract)}`);
}
if (reportData.labor_disputes.labor_relation !== undefined) {
details.push(`劳动关系: ${getStatusText(reportData.labor_disputes.labor_relation)}`);
}
if (reportData.labor_disputes.labor_relation_3y !== undefined) {
const detail = `近3年劳动关系: ${getStatusText(reportData.labor_disputes.labor_relation_3y)}`;
if (!shouldHideItem(detail)) {
details.push(detail);
}
}
if (reportData.labor_disputes.labor_relation_5y !== undefined) {
const detail = `近5年劳动关系: ${getStatusText(reportData.labor_disputes.labor_relation_5y)}`;
if (!shouldHideItem(detail)) {
details.push(detail);
}
}
if (details.length > 0) {
risks.push({
title: '劳动争议风险',
value: Math.max(...Object.values(reportData.labor_disputes).filter(v => typeof v === 'number')),
details: details,
bgColor: getBackgroundColor(Math.max(...Object.values(reportData.labor_disputes).filter(v => typeof v === 'number'))),
borderColor: getBorderColor(Math.max(...Object.values(reportData.labor_disputes).filter(v => typeof v === 'number')))
});
}
}
// 社会保险纠纷风险
if (reportData.social_insurance) {
let details: string[] = [];
if (reportData.social_insurance.social_insurance !== undefined) {
details.push(`社保纠纷: ${getStatusText(reportData.social_insurance.social_insurance)}`);
}
if (reportData.social_insurance.pension !== undefined) {
details.push(`养老纠纷: ${getStatusText(reportData.social_insurance.pension)}`);
}
if (reportData.social_insurance.pension_3y !== undefined) {
const detail = `近3年养老: ${getStatusText(reportData.social_insurance.pension_3y)}`;
if (!shouldHideItem(detail)) {
details.push(detail);
}
}
if (reportData.social_insurance.pension_5y !== undefined) {
const detail = `近5年养老: ${getStatusText(reportData.social_insurance.pension_5y)}`;
if (!shouldHideItem(detail)) {
details.push(detail);
}
}
if (reportData.social_insurance.injury_insurance !== undefined) {
details.push(`工伤纠纷: ${getStatusText(reportData.social_insurance.injury_insurance)}`);
}
if (reportData.social_insurance.injury_insurance_3y !== undefined) {
const detail = `近3年工伤: ${getStatusText(reportData.social_insurance.injury_insurance_3y)}`;
if (!shouldHideItem(detail)) {
details.push(detail);
}
}
if (reportData.social_insurance.injury_insurance_5y !== undefined) {
const detail = `近5年工伤: ${getStatusText(reportData.social_insurance.injury_insurance_5y)}`;
if (!shouldHideItem(detail)) {
details.push(detail);
}
}
if (reportData.social_insurance.unemployment_insurance !== undefined) {
details.push(`失业纠纷: ${getStatusText(reportData.social_insurance.unemployment_insurance)}`);
}
if (reportData.social_insurance.unemployment_insurance_3y !== undefined) {
const detail = `近3年失业: ${getStatusText(reportData.social_insurance.unemployment_insurance_3y)}`;
if (!shouldHideItem(detail)) {
details.push(detail);
}
}
if (reportData.social_insurance.unemployment_insurance_5y !== undefined) {
const detail = `近5年失业: ${getStatusText(reportData.social_insurance.unemployment_insurance_5y)}`;
if (!shouldHideItem(detail)) {
details.push(detail);
}
}
if (reportData.social_insurance.medical_insurance !== undefined) {
details.push(`医疗纠纷: ${getStatusText(reportData.social_insurance.medical_insurance)}`);
}
if (reportData.social_insurance.medical_insurance_3y !== undefined) {
const detail = `近3年医疗: ${getStatusText(reportData.social_insurance.medical_insurance_3y)}`;
if (!shouldHideItem(detail)) {
details.push(detail);
}
}
if (reportData.social_insurance.medical_insurance_5y !== undefined) {
const detail = `近5年医疗: ${getStatusText(reportData.social_insurance.medical_insurance_5y)}`;
if (!shouldHideItem(detail)) {
details.push(detail);
}
}
if (reportData.social_insurance.maternity_insurance !== undefined) {
details.push(`生育纠纷: ${getStatusText(reportData.social_insurance.maternity_insurance)}`);
}
if (reportData.social_insurance.maternity_insurance_3y !== undefined) {
const detail = `近3年生育: ${getStatusText(reportData.social_insurance.maternity_insurance_3y)}`;
if (!shouldHideItem(detail)) {
details.push(detail);
}
}
if (reportData.social_insurance.maternity_insurance_5y !== undefined) {
const detail = `近5年生育: ${getStatusText(reportData.social_insurance.maternity_insurance_5y)}`;
if (!shouldHideItem(detail)) {
details.push(detail);
}
}
if (details.length > 0) {
risks.push({
title: '社会保险纠纷风险',
value: Math.max(...Object.values(reportData.social_insurance).filter(v => typeof v === 'number')),
details: details,
bgColor: getBackgroundColor(Math.max(...Object.values(reportData.social_insurance).filter(v => typeof v === 'number'))),
borderColor: getBorderColor(Math.max(...Object.values(reportData.social_insurance).filter(v => typeof v === 'number')))
});
}
}
// 福利待遇纠纷
if (reportData.welfare_disputes && reportData.welfare_disputes.welfare !== undefined) {
risks.push({
title: '福利待遇纠纷',
value: reportData.welfare_disputes.welfare,
details: getStatusText(reportData.welfare_disputes.welfare),
bgColor: getBackgroundColor(reportData.welfare_disputes.welfare),
borderColor: getBorderColor(reportData.welfare_disputes.welfare)
});
}
// 人事争议类纠纷
if (reportData.personnel_disputes) {
let details: string[] = [];
if (reportData.personnel_disputes.personnel_dispute !== undefined) {
details.push(`人事争议: ${getStatusText(reportData.personnel_disputes.personnel_dispute)}`);
}
if (reportData.personnel_disputes.resignation_dispute !== undefined) {
details.push(`辞职争议: ${getStatusText(reportData.personnel_disputes.resignation_dispute)}`);
}
if (reportData.personnel_disputes.resignation_dispute_3y !== undefined) {
const detail = `近3年辞职: ${getStatusText(reportData.personnel_disputes.resignation_dispute_3y)}`;
if (!shouldHideItem(detail)) {
details.push(detail);
}
}
if (reportData.personnel_disputes.resignation_dispute_5y !== undefined) {
const detail = `近5年辞职: ${getStatusText(reportData.personnel_disputes.resignation_dispute_5y)}`;
if (!shouldHideItem(detail)) {
details.push(detail);
}
}
if (reportData.personnel_disputes.dismissal_dispute !== undefined) {
details.push(`辞退争议: ${getStatusText(reportData.personnel_disputes.dismissal_dispute)}`);
}
if (reportData.personnel_disputes.dismissal_dispute_3y !== undefined) {
const detail = `近3年辞退: ${getStatusText(reportData.personnel_disputes.dismissal_dispute_3y)}`;
if (!shouldHideItem(detail)) {
details.push(detail);
}
}
if (reportData.personnel_disputes.dismissal_dispute_5y !== undefined) {
const detail = `近5年辞退: ${getStatusText(reportData.personnel_disputes.dismissal_dispute_5y)}`;
if (!shouldHideItem(detail)) {
details.push(detail);
}
}
if (details.length > 0) {
risks.push({
title: '人事争议类纠纷',
value: Math.max(...Object.values(reportData.personnel_disputes).filter(v => typeof v === 'number')),
details: details,
bgColor: getBackgroundColor(Math.max(...Object.values(reportData.personnel_disputes).filter(v => typeof v === 'number'))),
borderColor: getBorderColor(Math.max(...Object.values(reportData.personnel_disputes).filter(v => typeof v === 'number')))
});
}
}
// 仲裁相关案件
if (reportData.arbitration) {
let details: string[] = [];
if (reportData.arbitration.arbitration_confirmation !== undefined) {
details.push(`仲裁确认: ${getStatusText(reportData.arbitration.arbitration_confirmation)}`);
}
if (reportData.arbitration.arbitration_confirmation_3y !== undefined) {
const detail = `近3年仲裁确认: ${getStatusText(reportData.arbitration.arbitration_confirmation_3y)}`;
if (!shouldHideItem(detail)) {
details.push(detail);
}
}
if (reportData.arbitration.arbitration_confirmation_5y !== undefined) {
const detail = `近5年仲裁确认: ${getStatusText(reportData.arbitration.arbitration_confirmation_5y)}`;
if (!shouldHideItem(detail)) {
details.push(detail);
}
}
if (reportData.arbitration.arbitration_revocation !== undefined) {
details.push(`仲裁撤销: ${getStatusText(reportData.arbitration.arbitration_revocation)}`);
}
if (reportData.arbitration.arbitration_revocation_3y !== undefined) {
const detail = `近3年仲裁撤销: ${getStatusText(reportData.arbitration.arbitration_revocation_3y)}`;
if (!shouldHideItem(detail)) {
details.push(detail);
}
}
if (reportData.arbitration.arbitration_revocation_5y !== undefined) {
const detail = `近5年仲裁撤销: ${getStatusText(reportData.arbitration.arbitration_revocation_5y)}`;
if (!shouldHideItem(detail)) {
details.push(detail);
}
}
if (details.length > 0) {
risks.push({
title: '仲裁相关案件',
value: Math.max(...Object.values(reportData.arbitration).filter(v => typeof v === 'number')),
details: details,
bgColor: getBackgroundColor(Math.max(...Object.values(reportData.arbitration).filter(v => typeof v === 'number'))),
borderColor: getBorderColor(Math.max(...Object.values(reportData.arbitration).filter(v => typeof v === 'number')))
});
}
}
// 通知函触达
if (reportData.notice_letter && reportData.notice_letter.notice_letter !== undefined) {
let statusText = getNoticeLetterStatusText(reportData.notice_letter.notice_letter);
let periodText = '';
if (reportData.notice_letter.notice_letter_period !== undefined) {
periodText = `期间: ${getNoticeLetterPeriodText(reportData.notice_letter.notice_letter_period)}`;
}
const detailParts = [`状态: ${statusText}`];
if (periodText) {
detailParts.push(periodText);
}
risks.push({
title: '通知函触达',
value: reportData.notice_letter.notice_letter,
details: detailParts,
bgColor: getBackgroundColor(reportData.notice_letter.notice_letter),
borderColor: getBorderColor(reportData.notice_letter.notice_letter)
});
}
return risks;
});
// 检查是否至少有一个数据类别有内容
const hasAnyData = computed(() => {
return riskTypes.value.length > 0;
});
</script>
<template>
<div class="ivyz0s0d-container">
<!-- 风险卡片网格 -->
<div v-if="hasAnyData" class="risk-cards-grid">
<div
v-for="(risk, index) in riskTypes"
:key="index"
class="risk-card"
:style="{ backgroundColor: risk.bgColor, borderLeft: `4px solid ${risk.borderColor}` }"
>
<div class="risk-card__content">
<h4 class="risk-card__title">{{ risk.title }}</h4>
<div class="risk-card__status">
<!-- details 是字符串时显示单行 -->
<p v-if="typeof risk.details === 'string'" class="risk-detail-item">{{ risk.details }}</p>
<!-- details 是数组时每项占一行 -->
<p
v-else
v-for="(detail, idx) in risk.details"
:key="idx"
class="risk-detail-item"
>{{ detail }}</p>
</div>
</div>
</div>
</div>
<!-- 无数据提示 -->
<div v-if="!hasAnyData" class="no-data">
<p>暂无相关风险数据</p>
</div>
</div>
</template>
<style scoped>
.ivyz0s0d-container {
padding: 20px;
font-family: Arial, sans-serif;
}
.risk-cards-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.risk-card {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.risk-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.risk-card__content {
display: flex;
flex-direction: column;
}
.risk-card__title {
margin: 0 0 8px 0;
font-size: 16px;
font-weight: bold;
color: #333;
}
.risk-card__status {
margin: 0;
font-size: 14px;
color: #666;
}
.risk-detail-item {
margin: 0 0 4px 0;
line-height: 1.4;
}
.risk-detail-item:last-child {
margin-bottom: 0;
}
.no-data {
text-align: center;
color: #999;
font-style: italic;
padding: 20px;
}
</style>

View File

@@ -62,16 +62,17 @@ 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 {

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

@@ -0,0 +1,76 @@
<script setup>
import { computed } from 'vue'
import { useRiskNotifier } from '@/composables/useRiskNotifier'
import { useVehiclePayload } from '@/composables/useVehiclePayload'
const props = defineProps({
data: Object,
params: Object,
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => {} },
})
const { obj } = useVehiclePayload(props)
const hasData = computed(() => Object.keys(obj.value).length > 0)
const flag = computed(() => obj.value.transferFlag)
const flagText = computed(() => {
if (flag.value === '1' || flag.value === 1) return '已过户'
if (flag.value === '0' || flag.value === 0) return '未过户'
return '未知'
})
const formattedTransferDate = computed(() => {
const raw = obj.value.transferDate
if (!raw) return '-'
if (raw === '近一年内过户') return raw
const s = String(raw)
if (s.length === 6) return `${s.slice(0, 4)}${s.slice(4, 6)}`
return s
})
const transferNum = computed(() => {
const n = obj.value.transferNum
return n === '' || n == null ? '0' : String(n)
})
const bandClass = computed(() => {
if (flag.value === '1' || flag.value === 1) return 'bg-amber-50 border-amber-200'
if (flag.value === '0' || flag.value === 0) return 'bg-green-50 border-green-200'
return 'bg-gray-50 border-gray-200'
})
const riskScore = computed(() => ((flag.value === '1' || flag.value === 1) ? 70 : 100))
useRiskNotifier(props, riskScore)
</script>
<template>
<div class="card space-y-3">
<div class="bg-gradient-to-br from-sky-50 to-blue-50 rounded-lg p-4 border border-sky-100">
<h3 class="text-lg font-bold text-sky-900">车辆过户简版查询</h3>
<p class="text-sm text-sky-700 mt-1">最近是否发生过户及过户次数</p>
<div class="mt-3 inline-block bg-white rounded-lg px-3 py-2 text-right">
<div class="text-xs text-gray-500">是否过户</div>
<div class="text-xl font-bold">{{ flagText }}</div>
</div>
</div>
<div v-if="hasData" :class="['rounded-lg p-4 border', bandClass]">
<div class="font-semibold">最近过户情况</div>
<p class="text-sm text-gray-600 mt-1">
{{ flag === 1 || flag === '1' ? '该车辆存在过户记录' : flag === 0 || flag === '0' ? '该车辆暂无过户记录' : '未能识别过户状态' }}
</p>
<div class="flex justify-between mt-3 text-sm">
<span class="text-gray-500">最近过户时间</span>
<span class="font-medium">{{ formattedTransferDate }}</span>
</div>
<div class="flex justify-between mt-2 text-sm">
<span class="text-gray-500">累计过户次数</span>
<span class="font-bold">{{ transferNum }} </span>
</div>
</div>
<div v-else class="text-center py-10 text-gray-500">暂无过户信息</div>
</div>
</template>

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

@@ -0,0 +1,104 @@
<script setup>
import { computed } from 'vue'
import { useRiskNotifier } from '@/composables/useRiskNotifier'
import { formatDateZh, formatMileageKm } from '@/utils/vehicleReportBlockMaps'
import { useVehiclePayload } from '@/composables/useVehiclePayload'
const props = defineProps({
data: Object,
params: Object,
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => {} },
})
const { obj } = useVehiclePayload(props)
const hasData = computed(() => Object.keys(obj.value).length > 0)
const vin = computed(() => obj.value.vehicleInfo?.vin || '')
const mileageList = computed(() => obj.value.mileageInfo?.mileageList || [])
const adjustList = computed(() => obj.value.mileageInfo?.suspectedAdjustMileageList || [])
const suspectedAdjust = computed(() => obj.value.mileageInfo?.suspectedAdjust)
const imageUrl = computed(() => obj.value.imageUrl || '')
const latestRecord = computed(() => {
const list = mileageList.value
return list.length ? list[list.length - 1] : null
})
const suspectedText = computed(() => {
if (suspectedAdjust.value === 'true') return '存在异常里程行为'
if (suspectedAdjust.value === 'false') return '未发现里程异常'
return '未知'
})
function sourceText(source) {
if (source === '0') return '诊断里程'
if (source === '1') return '维保里程'
return '其他'
}
const riskScore = computed(() => (suspectedAdjust.value === 'true' ? 45 : 95))
useRiskNotifier(props, riskScore)
</script>
<template>
<div class="card space-y-3">
<div class="bg-gradient-to-br from-sky-50 to-blue-50 rounded-lg p-4 border border-sky-100">
<h3 class="text-lg font-bold text-sky-900">车辆里程记录混合查询</h3>
<p class="text-sm text-sky-700 mt-1">里程变化与调表嫌疑综合展示</p>
</div>
<template v-if="hasData">
<div
class="rounded-lg p-4 border"
:class="suspectedAdjust === 'true' ? 'bg-amber-50 border-amber-200' : suspectedAdjust === 'false' ? 'bg-green-50 border-green-200' : 'bg-gray-50 border-gray-200'"
>
<div class="flex justify-between gap-4 flex-wrap">
<div>
<div class="text-xs text-gray-500">VIN</div>
<div class="font-mono text-sm">{{ vin || '-' }}</div>
</div>
<div class="text-right">
<div class="text-xs text-gray-500">最新里程</div>
<div class="text-xl font-bold text-blue-600">{{ latestRecord ? formatMileageKm(latestRecord.mileage) : '-' }}</div>
<div class="text-xs text-gray-500">最近{{ latestRecord?.reportTime || '-' }}</div>
</div>
</div>
<p class="text-sm mt-2">里程是否异常{{ suspectedText }}</p>
<img v-if="imageUrl" :src="imageUrl" class="w-full mt-2 rounded-lg" alt="里程凭证" />
</div>
<div class="bg-gray-50 rounded-lg p-4 border border-gray-200">
<h4 class="font-semibold mb-3">里程记录时间轴</h4>
<div v-if="mileageList.length" class="space-y-3">
<div v-for="(item, idx) in mileageList" :key="idx" class="flex gap-3">
<div class="w-2 rounded-full shrink-0" :class="item.mileageStatus === '1' ? 'bg-red-500' : 'bg-blue-500'" />
<div class="flex-1 text-sm">
<div class="flex justify-between">
<span>{{ formatDateZh(item.reportTime) }}</span>
<span class="font-semibold text-blue-600">{{ formatMileageKm(item.mileage) }}</span>
</div>
<div class="text-gray-500 mt-1">
来源{{ sourceText(item.source) }}
<span v-if="item.mileageStatus === '1'" class="ml-2 text-red-600 bg-red-50 px-1 rounded text-xs">异常里程</span>
</div>
</div>
</div>
</div>
<div v-else class="text-gray-500 text-sm">暂无里程记录</div>
</div>
<div v-if="adjustList.length" class="bg-gray-50 rounded-lg p-4 border border-gray-200">
<h4 class="font-semibold mb-3">疑似调表记录</h4>
<div v-for="(item, idx) in adjustList" :key="idx" class="py-2 border-b border-gray-200 last:border-0 text-sm">
<div>{{ formatDateZh(item.reportTime) }}</div>
<div class="text-gray-600 mt-1">
调整前 {{ formatMileageKm(item.beforeMileage) }} 调整后 {{ formatMileageKm(item.afterMileage) }}
</div>
</div>
</div>
</template>
<div v-else class="text-center py-10 text-gray-500">暂无里程数据</div>
</div>
</template>

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

@@ -0,0 +1,66 @@
<script setup>
import { computed } from 'vue'
import { useRiskNotifier } from '@/composables/useRiskNotifier'
import { formatDateZh, formatMileageKm } from '@/utils/vehicleReportBlockMaps'
import { useVehiclePayload } from '@/composables/useVehiclePayload'
const props = defineProps({
data: Object,
params: Object,
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => {} },
})
const { obj, params } = useVehiclePayload(props)
const records = computed(() => (Array.isArray(obj.value.record) ? obj.value.record : []))
const hasData = computed(() => Object.keys(obj.value).length > 0)
const vin = computed(() => (records.value[0]?.vin) || params.value.vin_code || '')
const lastRecord = computed(() => (records.value.length ? records.value[records.value.length - 1] : null))
const riskScore = computed(() => 95)
useRiskNotifier(props, riskScore)
</script>
<template>
<div class="card space-y-3">
<div class="bg-gradient-to-br from-sky-50 to-blue-50 rounded-lg p-4 border border-sky-100">
<h3 class="text-lg font-bold">车辆维保简版查询</h3>
<p class="text-sm text-gray-600 mt-1">按时间轴展示维保与材料明细</p>
</div>
<template v-if="hasData">
<div class="bg-gray-50 rounded-lg p-4 border text-sm">
<div class="font-mono">VIN{{ vin || '-' }}</div>
<div class="text-gray-600 mt-1">维保 {{ records.length }}
<span v-if="lastRecord"> · 最近 {{ formatDateZh(lastRecord.lastTime) }} · {{ formatMileageKm(lastRecord.mileage) }}</span>
</div>
</div>
<div v-if="records.length" class="space-y-3">
<div v-for="(item, idx) in records" :key="idx" class="bg-white rounded-lg p-4 border border-gray-200">
<div class="flex justify-between font-medium">
<span>{{ formatDateZh(item.lastTime) }}</span>
<span class="text-blue-600">{{ formatMileageKm(item.mileage) }}</span>
</div>
<p class="text-sm text-gray-500 mt-1">{{ item.repairType || '维保' }} · VIN {{ item.vin || vin || '-' }}</p>
<div v-if="item.details?.length" class="mt-2 text-sm">
<div class="text-xs text-gray-500 mb-1">维修项目</div>
<div v-for="(det, di) in item.details" :key="di" class="py-1">
<span v-if="det.type" class="text-xs bg-blue-50 text-blue-700 px-1 rounded mr-1">{{ det.type }}</span>{{ det.content }}
</div>
</div>
<div v-if="item.materials?.length" class="mt-2 text-sm">
<div class="text-xs text-gray-500 mb-1">使用材料</div>
<div v-for="(m, mi) in item.materials" :key="mi" class="py-1">
<span v-if="m.type" class="text-xs bg-gray-100 px-1 rounded mr-1">{{ m.type }}</span>{{ m.content }}
</div>
</div>
</div>
</div>
<div v-else class="text-center text-gray-500 py-6">暂无维保记录</div>
</template>
<div v-else class="text-center py-10 text-gray-500">暂无维保数据</div>
</div>
</template>

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

@@ -0,0 +1,53 @@
<script setup>
import { computed } from 'vue'
import { useRiskNotifier } from '@/composables/useRiskNotifier'
import { formatDateZh, formatMileageKm } from '@/utils/vehicleReportBlockMaps'
import { useVehiclePayload } from '@/composables/useVehiclePayload'
const props = defineProps({
data: Object,
params: Object,
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => {} },
})
const { obj } = useVehiclePayload(props)
const records = computed(() => (Array.isArray(obj.value.record) ? obj.value.record : []))
const hasData = computed(() => Object.keys(obj.value).length > 0)
const riskScore = computed(() => 95)
useRiskNotifier(props, riskScore)
</script>
<template>
<div class="card space-y-3">
<div class="bg-gradient-to-br from-sky-50 to-blue-50 rounded-lg p-4 border border-sky-100">
<h3 class="text-lg font-bold">车辆维保详细版查询</h3>
<p class="text-sm text-gray-600 mt-1">品牌VIN 及每次维保详细内容</p>
</div>
<template v-if="hasData">
<div class="bg-gray-50 rounded-lg p-4 border text-sm grid grid-cols-2 gap-2">
<div><span class="text-gray-500">品牌</span><div class="font-medium">{{ obj.brandName || '未知' }}</div></div>
<div><span class="text-gray-500">VIN</span><div class="font-mono">{{ obj.vin || '-' }}</div></div>
<div class="col-span-2 text-gray-600">车牌 {{ obj.licensePlate || '未提供' }} · 发动机 {{ obj.engine || '-' }}</div>
</div>
<div v-if="records.length" class="space-y-3">
<div v-for="(item, idx) in records" :key="idx" class="bg-white rounded-lg p-4 border">
<div class="flex justify-between font-medium">
<span>{{ formatDateZh(item.date) }}</span>
<span class="text-blue-600">{{ formatMileageKm(item.mileage) }}</span>
</div>
<p class="text-sm text-gray-500">{{ item.type || '维保' }}</p>
<p v-if="item.content" class="text-sm mt-2">维修内容{{ item.content }}</p>
<p v-if="item.remark" class="text-xs text-gray-500 mt-1">{{ item.remark }}</p>
</div>
</div>
<div v-else class="text-center text-gray-500 py-6">暂无维保记录</div>
</template>
<div v-else class="text-center py-10 text-gray-500">暂无维保数据</div>
</div>
</template>

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

@@ -0,0 +1,15 @@
<script setup>
import VehiclePlateList from '@/ui/vehicle/VehiclePlateList.vue'
defineProps({
data: Object,
params: Object,
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => {} },
})
</script>
<template>
<VehiclePlateList v-bind="$props" title="名下车辆数量查询" />
</template>

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

@@ -0,0 +1,84 @@
<script setup>
import { computed } from 'vue'
import { useRiskNotifier } from '@/composables/useRiskNotifier'
import { payloadAsArray } from '@/utils/vehiclePayload'
const props = defineProps({
data: { default: null },
params: Object,
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => {} },
})
const transfers = computed(() => {
return payloadAsArray(props.data).map((item) => {
const changeMonth = item.changeMonth
let changeMonthFormatted = '-'
if (changeMonth === '近一年内过户') changeMonthFormatted = changeMonth
else if (typeof changeMonth === 'string' && changeMonth.length === 6) {
changeMonthFormatted = `${changeMonth.slice(0, 4)}${changeMonth.slice(4, 6)}`
}
else if (changeMonth) changeMonthFormatted = String(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(() => (transfers.value.length > 2 ? 65 : 90))
useRiskNotifier(props, riskScore)
</script>
<template>
<div class="card space-y-3">
<div class="bg-gradient-to-br from-sky-50 to-blue-50 rounded-lg p-4 border border-sky-100 flex justify-between items-start gap-3">
<div>
<h3 class="text-lg font-bold text-sky-900">车辆过户详版查询</h3>
<p class="text-sm text-sky-700 mt-1">按时间轴展示过户与车牌变更</p>
</div>
<div v-if="totalTimes" class="bg-white rounded-lg px-3 py-2 text-right shrink-0">
<div class="text-xs text-gray-500">总过户次数</div>
<div class="text-lg font-bold">{{ totalTimes }} </div>
</div>
</div>
<div v-if="transfers.length" class="space-y-3">
<div
v-for="(item, index) in transfers"
:key="index"
class="bg-gray-50 rounded-lg p-4 border border-gray-200"
>
<div class="flex justify-between mb-2">
<span class="font-semibold">{{ item.changeMonthFormatted }}</span>
<span class="text-sm text-blue-600"> {{ item.transTimeSum }} 次过户</span>
</div>
<div class="grid grid-cols-2 gap-3 text-sm">
<div>
<div class="text-gray-500 text-xs">过户前车牌</div>
<div class="font-mono font-medium">{{ item.oldCp || '未知' }}</div>
<div v-if="item.cityBefore" class="text-xs text-gray-500 mt-1">城市{{ item.cityBefore }}</div>
</div>
<div>
<div class="text-gray-500 text-xs">过户后车牌</div>
<div class="font-mono font-medium">{{ item.newCp || '未知' }}</div>
<div v-if="item.cityAfter" class="text-xs text-gray-500 mt-1">城市{{ item.cityAfter }}</div>
</div>
</div>
<div v-if="item.intervalText !== '-'" class="text-xs text-gray-500 mt-2">距上次过户{{ item.intervalText }}</div>
</div>
</div>
<div v-else class="text-center py-10 text-gray-500">暂无过户记录</div>
</div>
</template>

15
src/ui/CQCXG5F3A.vue Normal file
View File

@@ -0,0 +1,15 @@
<script setup>
import VehiclePlateList from '@/ui/vehicle/VehiclePlateList.vue'
defineProps({
data: Object,
params: Object,
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => {} },
})
</script>
<template>
<VehiclePlateList v-bind="$props" title="名下车辆车牌查询 B" />
</template>

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

@@ -0,0 +1,52 @@
<script setup>
import { computed } from 'vue'
import { useRiskNotifier } from '@/composables/useRiskNotifier'
import { useVehiclePayload } from '@/composables/useVehiclePayload'
import { payloadAsArray } from '@/utils/vehiclePayload'
const props = defineProps({
data: { default: null },
params: Object,
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => {} },
})
const records = computed(() => payloadAsArray(props.data))
const riskScore = computed(() => 100)
useRiskNotifier(props, riskScore)
</script>
<template>
<div class="card space-y-3">
<div class="bg-gradient-to-br from-blue-50 to-indigo-50 rounded-lg p-4 border border-blue-100">
<h3 class="text-lg font-bold text-gray-900">车辆静态信息查询</h3>
<p class="text-sm text-gray-600 mt-1">生产排放燃料等静态信息</p>
</div>
<template v-if="records.length">
<div
v-for="(item, idx) in records"
:key="idx"
class="bg-gray-50 rounded-lg p-4 border border-gray-200"
>
<div class="flex flex-wrap items-center gap-2 mb-3">
<span class="text-xs text-blue-700 bg-blue-50 px-2 py-0.5 rounded">车辆 {{ idx + 1 }}</span>
<span class="font-semibold text-gray-900">{{ item.vType || '未知车型' }}</span>
<span class="text-xs text-blue-600 bg-white px-2 py-0.5 rounded-full border">{{ item.vFuelType || '燃料未知' }}</span>
</div>
<div class="grid grid-cols-2 gap-3 text-sm">
<div><span class="text-gray-500 block text-xs">发动机号</span><span class="font-mono">{{ item.engineNO || '-' }}</span></div>
<div><span class="text-gray-500 block text-xs">发动机型号</span><span class="font-mono">{{ item.engineType || '-' }}</span></div>
<div><span class="text-gray-500 block text-xs">生产日期</span>{{ item.vScdate || '-' }}</div>
<div><span class="text-gray-500 block text-xs">排放阶段</span>{{ item.dischargeStage || '-' }}</div>
<div><span class="text-gray-500 block text-xs">车辆分类</span>{{ item.vClassification || '-' }}</div>
<div class="col-span-2"><span class="text-gray-500 block text-xs">生产企业</span>{{ item.vManufacturer || '-' }}</div>
<div class="col-span-2"><span class="text-gray-500 block text-xs">生产厂地址</span>{{ item.vSccdz || '-' }}</div>
</div>
</div>
</template>
<div v-else class="text-center py-10 text-gray-500">暂无车辆静态信息</div>
</div>
</template>

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

@@ -0,0 +1,76 @@
<script setup>
import { computed } from 'vue'
import { useRiskNotifier } from '@/composables/useRiskNotifier'
import { useVehiclePayload } from '@/composables/useVehiclePayload'
const props = defineProps({
data: Object,
params: Object,
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => {} },
})
const { obj: data } = useVehiclePayload(props)
const hasData = computed(() => Object.keys(data.value).length > 0)
function yesNoText(val, yesText, noText = '否') {
if (val === '1') return yesText
if (val === '0') return noText
return '-'
}
const riskLevelText = computed(() => {
const d = data.value
if (d.IfHighriskVehicle === '1') return '高风险'
if (d.IsMajorAccidentLevel && d.IsMajorAccidentLevel !== '一般') return '较高风险'
if (d.IsMajorAccidentData && d.IsMajorAccidentData !== '0') return '有事故记录'
return '风险可控'
})
const riskScore = computed(() => {
const t = riskLevelText.value
if (t === '高风险') return 30
if (t === '较高风险' || t === '有事故记录') return 50
return 90
})
useRiskNotifier(props, riskScore)
</script>
<template>
<div class="card space-y-3">
<div class="bg-gradient-to-br from-orange-50 to-red-50 rounded-lg p-4 border border-orange-100">
<h3 class="text-lg font-bold">车辆出险记录核验</h3>
<p class="text-sm text-gray-600 mt-1">综合出险脱保重大事故等信息</p>
</div>
<template v-if="hasData">
<div
class="rounded-lg p-4 text-center border"
:class="riskLevelText === '高风险' ? 'bg-red-50 border-red-200' : riskLevelText.includes('风险') && riskLevelText !== '风险可控' ? 'bg-amber-50 border-amber-200' : 'bg-green-50 border-green-200'"
>
<div class="text-sm text-gray-500">风险等级</div>
<div class="text-2xl font-bold">{{ riskLevelText }}</div>
</div>
<div class="bg-gray-50 rounded-lg p-4 border text-sm space-y-2">
<div v-if="data.LicensePlate" class="text-xl font-bold font-mono">{{ data.LicensePlate }}</div>
<div>{{ data.CarType || '未知车型' }}</div>
<div class="text-gray-600">二手车参考价 {{ data.UsedCarPrice ? `${data.UsedCarPrice}` : '-' }} · 新车 {{ data.PurchasePrice ? `${data.PurchasePrice}` : '-' }}</div>
<div class="text-gray-500 text-xs">车龄 {{ data.CarAge ? `${data.CarAge} 个月` : '-' }} · 初登 {{ data.DebutDate || '-' }}</div>
</div>
<div class="bg-white rounded-lg border divide-y text-sm">
<div class="px-4 py-2 font-semibold bg-gray-50">核心风险指标</div>
<div class="flex justify-between px-4 py-2"><span class="text-gray-500">高风险车辆</span><span>{{ yesNoText(data.IfHighriskVehicle, '是') }}</span></div>
<div class="flex justify-between px-4 py-2"><span class="text-gray-500">营运车辆</span><span>{{ yesNoText(data.IsOperation, '是') }}</span></div>
<div class="flex justify-between px-4 py-2"><span class="text-gray-500">车损险</span><span>{{ yesNoText(data.IfCarDamage, '已投保', '未投保') }}</span></div>
<div class="flex justify-between px-4 py-2"><span class="text-gray-500">连续投保</span><span>{{ yesNoText(data.IsConInsure, '是') }}</span></div>
<div class="flex justify-between px-4 py-2"><span class="text-gray-500">历史脱保</span><span>{{ yesNoText(data.IfTuoBao, '有', '无') }}</span></div>
<div class="flex justify-between px-4 py-2"><span class="text-gray-500">全损情况</span><span>{{ data.TotalLoss === '1' ? '存在全损' : data.TotalLoss === '0' ? '无全损' : '-' }}</span></div>
<div class="flex justify-between px-4 py-2"><span class="text-gray-500">综合评分</span><span class="font-bold">{{ data.Total || '-' }}</span></div>
</div>
</template>
<div v-else class="text-center py-10 text-gray-500">暂无出险核验数据</div>
</div>
</template>

View File

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

View File

@@ -1,130 +1,15 @@
<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-sm 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';
// 接收父组件传入的 props
const props = defineProps({
data: Object,
params: Object,
});
// 车牌颜色映射
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] || '未知类型';
};
onMounted(() => {
console.log('车辆数据:', props.data);
});
import VehiclePlateList from '@/ui/vehicle/VehiclePlateList.vue'
defineProps({
data: Object,
params: Object,
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => {} },
})
</script>
<style scoped>
/* 自定义样式 - 仅保留必要的样式 */
</style>
<template>
<VehiclePlateList v-bind="$props" title="名下车辆车牌查询 A" />
</template>

43
src/ui/CQCXGFallback.vue Normal file
View File

@@ -0,0 +1,43 @@
<script setup>
import { computed } from 'vue'
import { useRiskNotifier } from '@/composables/useRiskNotifier'
import { useVehiclePayload } from '@/composables/useVehiclePayload'
const props = defineProps({
data: { default: null },
params: Object,
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => {} },
})
const { payload } = useVehiclePayload(props)
const bodyText = computed(() => {
const p = payload.value
if (p === '000' || p === 0)
return '(本模块暂无示例明细)'
if (p == null || p === '')
return '暂无数据'
if (typeof p === 'string')
return p
try {
return JSON.stringify(p, null, 2)
}
catch {
return String(p)
}
})
const riskScore = computed(() => 100)
useRiskNotifier(props, riskScore)
</script>
<template>
<div class="card">
<p class="text-xs text-gray-500 mb-2">
以下为结构化数据预览专用版式可联系产品补充独立模块
</p>
<pre class="text-xs text-gray-800 bg-gray-50 rounded-lg p-3 overflow-auto max-h-80 whitespace-pre-wrap break-all">{{ bodyText }}</pre>
</div>
</template>

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

@@ -0,0 +1,58 @@
<script setup>
import { computed } from 'vue'
import { useRiskNotifier } from '@/composables/useRiskNotifier'
import { maskName } from '@/utils/vehicleReportBlockMaps'
import { useVehiclePayload } from '@/composables/useVehiclePayload'
const props = defineProps({
data: Object,
params: Object,
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => {} },
})
const { obj, params } = useVehiclePayload(props)
/** verify_code1 一致0 不一致(上游接口约定) */
const isMatch = computed(() => {
const n = Number(obj.value.verify_code)
if (n === 1) return true
if (n === 0) return false
return null
})
const resultText = computed(() => {
if (isMatch.value === true) return '一致'
if (isMatch.value === false) return '不一致'
return '暂无结果'
})
const sectionClass = computed(() => {
if (isMatch.value === true) return 'bg-green-50 border-green-200'
if (isMatch.value === false) return 'bg-red-50 border-red-200'
return 'bg-gray-50 border-gray-200'
})
const riskScore = computed(() => (isMatch.value === false ? 40 : 100))
useRiskNotifier(props, riskScore)
</script>
<template>
<div class="card space-y-4">
<div class="bg-gradient-to-br from-blue-600 to-blue-700 text-white rounded-lg p-4">
<h3 class="text-lg font-bold">车辆二要素核验</h3>
<p class="text-sm opacity-90 mt-1">校验人员姓名与车辆号牌是否匹配</p>
</div>
<div :class="['rounded-lg p-6 text-center border', sectionClass]">
<div class="text-4xl font-bold mb-2">{{ isMatch === true ? '✓' : isMatch === false ? '✕' : '?' }}</div>
<div class="text-sm text-gray-500">核验结果</div>
<div class="text-2xl font-bold mt-1">{{ resultText }}</div>
</div>
<div class="divide-y text-sm">
<div class="flex py-2"><span class="w-24 text-gray-500">姓名</span><span>{{ maskName(params.name) }}</span></div>
<div class="flex py-2"><span class="w-24 text-gray-500">车牌号</span><span class="font-mono">{{ params.plate_no || params.car_license || '-' }}</span></div>
<div class="flex py-2"><span class="w-24 text-gray-500">号牌类型</span><span>{{ params.carplate_type || params.car_type || '-' }}</span></div>
</div>
</div>
</template>

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

@@ -0,0 +1,99 @@
<script setup>
import { computed } from 'vue'
import { useRiskNotifier } from '@/composables/useRiskNotifier'
import { useVehiclePayload } from '@/composables/useVehiclePayload'
const props = defineProps({
data: Object,
params: Object,
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => {} },
})
const { obj } = useVehiclePayload(props)
const retdata = computed(() => {
const p = obj.value
if (p.retdata && typeof p.retdata === 'object') return p.retdata
return p
})
const hasData = computed(() => Object.keys(retdata.value).length > 0)
const clxx = computed(() => retdata.value.clxx || {})
const tjxx = computed(() => retdata.value.tjxx || {})
const pzRecords = computed(() => {
const mx = retdata.value.pzlsmx || {}
return Array.isArray(mx.records) ? mx.records : []
})
function formatFen(val) {
if (val !== 0 && !val) return '-'
const n = Number(val)
if (Number.isNaN(n)) return `${val}`
return `${(n / 100).toLocaleString()}`
}
function dangerTypeText(t) {
if (t === '1') return '更换'
if (t === '2') return '维修'
if (t === '3') return '材料'
return '其他'
}
const riskScore = computed(() => {
const count = Number(tjxx.value.claimCount)
if (count > 3) return 40
if (count > 0) return 60
return 90
})
useRiskNotifier(props, riskScore)
</script>
<template>
<div class="card space-y-3">
<div class="bg-gradient-to-br from-red-50 to-orange-50 rounded-lg p-4 border border-red-100">
<h3 class="text-lg font-bold">车辆出险详版查询</h3>
<p class="text-sm text-gray-600 mt-1">碰撞记录与统计精简展示</p>
</div>
<template v-if="hasData">
<div class="bg-gray-50 rounded-lg p-4 border text-sm">
<div class="grid grid-cols-2 gap-2">
<div>
<div class="text-gray-500 text-xs">品牌</div>
<div class="font-medium">{{ clxx.brandName || '未知' }}</div>
<div v-if="clxx.vehicleStyle" class="text-xs text-gray-500">{{ clxx.vehicleStyle }}</div>
</div>
<div>
<div class="text-gray-500 text-xs">VIN</div>
<div class="font-mono text-xs">{{ pzRecords[0]?.vin || clxx.vin || '-' }}</div>
<div v-if="clxx.licensePlate" class="text-xs">车牌 {{ clxx.licensePlate }}</div>
</div>
</div>
<div class="flex flex-wrap gap-3 mt-3 text-xs text-gray-600">
<span>事故 {{ tjxx.claimCount ?? '-' }} </span>
<span>总维修 {{ tjxx.totalAmount || '-' }}</span>
<span>最大单次 {{ tjxx.largestAmount || '-' }}</span>
<span>结案 {{ tjxx.claimCacCount ?? 0 }} / 未结案 {{ tjxx.claimUnCacCount ?? 0 }}</span>
</div>
</div>
<div v-if="pzRecords.length" class="space-y-2">
<h4 class="font-semibold text-sm">碰撞出险记录</h4>
<div v-for="(rec, idx) in pzRecords" :key="idx" class="bg-white rounded-lg p-3 border text-sm">
<div class="flex justify-between">
<span>{{ rec.date || '-' }}</span>
<span class="font-semibold text-red-600">{{ formatFen(rec.serviceMoney) }}</span>
</div>
<p v-if="rec.dangerSingleList?.length" class="text-xs text-gray-500 mt-1">
<span v-for="(d, di) in rec.dangerSingleList" :key="di" class="mr-2">
{{ dangerTypeText(d.dangerSingleType) }}{{ d.dangerSingleName || d.dangerSingleDesc }}
</span>
</p>
</div>
</div>
</template>
<div v-else class="text-center py-10 text-gray-500">暂无出险详版数据</div>
</div>
</template>

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

@@ -0,0 +1,51 @@
<script setup>
import { computed } from 'vue'
import { useRiskNotifier } from '@/composables/useRiskNotifier'
import { useVehiclePayload } from '@/composables/useVehiclePayload'
const props = defineProps({
data: Object,
params: Object,
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => {} },
})
const { obj } = useVehiclePayload(props)
const hasData = computed(() => Object.keys(obj.value).length > 0)
const riskScore = computed(() => 100)
useRiskNotifier(props, riskScore)
</script>
<template>
<div class="card space-y-3">
<div class="bg-gradient-to-br from-violet-50 to-purple-50 rounded-lg p-4 border flex justify-between items-start">
<div>
<h3 class="text-lg font-bold">二手车 VIN 估值</h3>
<p class="text-sm text-gray-600 mt-1">基于车型排量排放等给出参考估值</p>
</div>
<div class="text-right bg-white rounded-lg px-3 py-2 border">
<div class="text-xs text-gray-500">估值</div>
<div class="text-lg font-bold text-violet-700">{{ obj.estimatedValue || '-' }}</div>
</div>
</div>
<template v-if="hasData">
<div class="text-center py-4 bg-violet-50 rounded-lg border border-violet-100">
<div class="text-3xl font-bold text-violet-800">{{ obj.estimatedValue || '-' }}</div>
<p class="text-xs text-gray-500 mt-1">参考估值仅供参考</p>
<p class="text-sm mt-2">{{ obj.seriesName || '未知车系' }} · {{ obj.manufacturerName || '未知厂商' }}</p>
<p v-if="obj.productionDate" class="text-sm text-gray-600">{{ obj.productionDate }} 年出厂</p>
</div>
<div class="divide-y text-sm">
<div class="flex py-2 justify-between"><span class="text-gray-500">厂商</span><span>{{ obj.manufacturerName || '-' }}</span></div>
<div class="flex py-2 justify-between"><span class="text-gray-500">车系</span><span>{{ obj.seriesName || '-' }}</span></div>
<div class="flex py-2 justify-between"><span class="text-gray-500">车型年款</span><span>{{ obj.modelYear || obj.productionDate || '-' }}</span></div>
<div class="flex py-2 justify-between"><span class="text-gray-500">座位数</span><span>{{ obj.seatingCapacity ?? '-' }}</span></div>
<div class="flex py-2 justify-between"><span class="text-gray-500">车型名称</span><span>{{ obj.modelName || '-' }}</span></div>
<div class="flex py-2 justify-between"><span class="text-gray-500">指导价</span><span>{{ obj.msrp || '-' }}</span></div>
</div>
</template>
<div v-else class="text-center py-10 text-gray-500">暂无估值结果</div>
</div>
</template>

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

@@ -0,0 +1,76 @@
<script setup>
import { computed } from 'vue'
import { useRiskNotifier } from '@/composables/useRiskNotifier'
import { maskName } from '@/utils/vehicleReportBlockMaps'
import { useVehiclePayload } from '@/composables/useVehiclePayload'
const props = defineProps({
data: Object,
params: Object,
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => {} },
})
const { obj, params } = useVehiclePayload(props)
const status = computed(() => {
const raw = obj.value
const s = Number(raw?.status ?? raw?.result?.status)
if ([0, -1, -2, -4].includes(s)) 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 sectionClass = computed(() => {
const s = status.value
if (s === 0) return 'bg-green-50 border-green-200'
if (s === -1) return 'bg-red-50 border-red-200'
if (s === -2) return 'bg-orange-50 border-orange-200'
if (s === -4) return 'bg-gray-100 border-gray-300'
return 'bg-gray-50 border-gray-200'
})
const riskScore = computed(() => {
const s = status.value
if (s === -1) return 35
if (s === -2 || s === -4) return 55
return 100
})
useRiskNotifier(props, riskScore)
</script>
<template>
<div class="card space-y-4">
<div class="bg-gradient-to-br from-indigo-600 to-indigo-800 text-white rounded-lg p-4">
<h3 class="text-lg font-bold">人车核验详版</h3>
<p class="text-sm opacity-90 mt-1">人员与车辆的详细匹配结果</p>
</div>
<div :class="['rounded-lg p-6 text-center border', sectionClass]">
<div class="text-4xl font-bold mb-2">{{ status === 0 ? '✓' : status === -1 ? '✕' : status === -2 ? '!' : '—' }}</div>
<div class="text-sm text-gray-500">认证结果</div>
<div class="text-2xl font-bold mt-1">{{ resultText }}</div>
<p v-if="resultDesc" class="text-sm text-gray-600 mt-2">{{ resultDesc }}</p>
</div>
<div class="divide-y text-sm">
<div class="flex py-2"><span class="w-24 text-gray-500">姓名</span><span>{{ maskName(params.name) }}</span></div>
<div class="flex py-2"><span class="w-24 text-gray-500">车牌号</span><span class="font-mono">{{ params.plate_no || params.car_license || '-' }}</span></div>
<div class="flex py-2"><span class="w-24 text-gray-500">号牌类型</span><span>{{ params.carplate_type || params.car_type || '-' }}</span></div>
</div>
</div>
</template>

View File

@@ -221,7 +221,7 @@
<span class="text-[#EB3C3C] ml-1">{{ getLawsuitStats(company).total
||
0
}}</span>
}}</span>
</div>
<!-- 企业状态 -->
<span v-if="company.basicInfo && company.basicInfo.regStatus"
@@ -273,13 +273,13 @@
<span class="text-base font-medium text-[#333333]">{{
company.basicInfo.regStatus ||
'-'
}}</span>
}}</span>
<span class="text-base text-[#666666]">统一社会信用代码:</span>
<span class="text-base font-medium text-[#333333]">{{
company.basicInfo.creditCode
|| '-'
}}</span>
}}</span>
<span class="text-base text-[#666666]">注册资本:</span>
<span class="text-base font-medium text-[#333333]">{{
@@ -1162,29 +1162,18 @@ const getLawsuitStats = (company) => {
return stats;
};
// 格式化金额显示(单位:万元)
const formatLawsuitMoney = (money) => {
if (!money) return "—";
const value = parseFloat(money);
if (isNaN(value)) return "—";
// 超过1亿显示亿元
if (value >= 10000) {
return (
(value / 10000).toLocaleString("zh-CN", {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
}) + " 亿元"
);
}
// 否则显示万元
// 直接显示原始金额(元)
return (
value.toLocaleString("zh-CN", {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
}) + " 元"
}) + " 元"
);
};

View File

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

524
src/ui/IVYZ0S0D.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>

View File

@@ -0,0 +1,34 @@
[
{
"value": "A",
"label": "985院校"
},
{
"value": "B",
"label": "双一流"
},
{
"value": "C",
"label": "211院校"
},
{
"value": "D",
"label": "一本院校"
},
{
"value": "E",
"label": "二本院校"
},
{
"value": "F",
"label": "大专院校"
},
{
"value": "G",
"label": "成人本科"
},
{
"value": "H",
"label": "其他"
}
]

View File

@@ -0,0 +1,22 @@
[
{
"value": "9",
"label": "博士研究生"
},
{
"value": "8",
"label": "硕士研究生"
},
{
"value": "7",
"label": "本科"
},
{
"value": "6",
"label": "大专"
},
{
"value": "5",
"label": "其他"
}
]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

282
src/ui/IVYZ4Y27/index.vue Normal file
View File

@@ -0,0 +1,282 @@
<script setup>
import { computed, onMounted } from 'vue';
import { useRiskNotifier } from '@/composables/useRiskNotifier';
import abilityCompetitive from './abilityCompetitive.json';
import abilityCompetitiveDegree from './abilityCompetitiveDegree.json';
import abilityName from './abilityName.json';
import abilityField from './abilityField.json';
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';
// 计算风险评分
const riskScore = computed(() => {
return 100;
});
// 使用 composable 通知父组件风险评分
useRiskNotifier(props, riskScore);
// 暴露给父组件
defineExpose({
riskScore
});
// 获取列表数据
const abilityList = computed(() => {
return props.data?.abilityInfo || [];
});
// 字典映射
const competitiveMap = {};
abilityCompetitive.forEach(item => {
competitiveMap[item.value] = item.label;
});
const competitiveDegreeMap = {};
abilityCompetitiveDegree.forEach(item => {
competitiveDegreeMap[item.value] = item.label;
});
const nameMap = {};
abilityName.forEach(item => {
nameMap[item.value] = item.label;
});
const fieldMap = {};
abilityField.forEach(item => {
fieldMap[item.value] = item.label;
});
// 翻译函数
const getCompetitiveLabel = (val) => competitiveMap[val] || val || '未知';
const getCompetitiveDegreeLabel = (val) => competitiveDegreeMap[val] || val || '未知';
const getNameLabel = (val) => nameMap[val] || val || '未知';
// 专业方向翻译C99999/B99999 等以 99999 结尾的表示未匹配到相关专业
const getFieldLabel = (val) => {
if (!val) return '未知';
if (val.endsWith('99999')) {
const prefix = val.charAt(0);
const prefixMap = { 'A': '硕士研究生/博士研究生', 'B': '本科', 'C': '专科' };
return `${prefixMap[prefix] || ''}(未匹配到相关专业)`;
}
return fieldMap[val] || val || '未知';
};
// 格式化日期 yyyy-MM-dd -> yyyy年MM月dd日
const formatDate = (dateStr) => {
if (!dateStr) return '未知';
const parts = dateStr.split('-');
if (parts.length === 3) {
return `${parts[0]}${parts[1]}${parts[2]}`;
}
if (parts.length === 2) {
return `${parts[0]}${parts[1]}`;
}
return dateStr;
};
// 获取核心竞争力等级对应的颜色样式
const getCompetitiveBadgeClass = (val) => {
const classMap = {
'A': 'bg-amber-500 text-white',
'B': 'bg-green-500 text-white',
'C': 'bg-blue-500 text-white',
'D': 'bg-indigo-500 text-white',
'E': 'bg-cyan-500 text-white',
'F': 'bg-gray-500 text-white',
'G': 'bg-orange-500 text-white',
'H': 'bg-gray-400 text-white',
};
return classMap[val] || 'bg-gray-400 text-white';
};
// 获取学历等级对应的颜色样式
const getDegreeBadgeClass = (val) => {
const classMap = {
'9': 'bg-purple-500 text-white',
'8': 'bg-blue-600 text-white',
'7': 'bg-blue-500 text-white',
'6': 'bg-cyan-600 text-white',
'5': 'bg-gray-500 text-white',
};
return classMap[val] || 'bg-gray-400 text-white';
};
// 判断是否有数据
const hasData = computed(() => {
return abilityList.value.length > 0;
});
</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">学历信息</h2>
<p class="text-sm text-gray-500">Education Information</p>
</div>
</div>
<div class="text-sm text-gray-500">
{{ abilityList.length }} 条记录
</div>
</div>
</div>
<!-- 学历列表 -->
<div class="space-y-6">
<div v-for="(item, idx) in abilityList" :key="idx"
class="bg-white border border-gray-200 rounded-xl overflow-hidden hover:shadow-md transition-shadow">
<!-- 顶部学历等级与核心竞争力 -->
<div class="bg-gradient-to-r from-blue-50 to-indigo-50 px-5 py-4 border-b border-blue-100">
<div class="flex items-center justify-between flex-wrap gap-2">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full bg-blue-500 flex items-center justify-center">
<span class="text-white text-lg font-bold">{{ idx + 1 }}</span>
</div>
<div>
<div class="flex items-center gap-2 flex-wrap">
<span v-if="item.abilityCompetitiveDegree"
:class="['inline-flex items-center px-3 py-1 rounded-full text-sm font-medium', getDegreeBadgeClass(item.abilityCompetitiveDegree)]">
{{ getCompetitiveDegreeLabel(item.abilityCompetitiveDegree) }}
</span>
<span v-if="item.abilityCompetitive"
:class="['inline-flex items-center px-3 py-1 rounded-full text-xs font-medium', getCompetitiveBadgeClass(item.abilityCompetitive)]">
{{ getCompetitiveLabel(item.abilityCompetitive) }}
</span>
</div>
</div>
</div>
</div>
</div>
<!-- 详细信息 -->
<div class="p-5">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- 院校名称 -->
<div v-if="item.abilityName"
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="text-base font-medium text-gray-900">{{ getNameLabel(item.abilityName)
}}</div>
</div>
</div>
</div>
<!-- 学习类型 -->
<div v-if="item.abilityType"
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">{{ item.abilityType }}</div>
</div>
</div>
</div>
<!-- 专业方向 -->
<div v-if="item.abilityField"
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">{{ getFieldLabel(item.abilityField)
}}</div>
</div>
</div>
</div>
<!-- 学习时间 -->
<div v-if="item.abilityStartDate || item.abilityEndDate"
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(item.abilityStartDate) }} ~ {{ formatDate(item.abilityEndDate) }}
</div>
</div>
</div>
</div>
</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

@@ -0,0 +1,63 @@
<script setup>
import { computed } from 'vue'
import { plateColorLabel, vehicleTypeLabel } from '@/utils/vehicleReportBlockMaps'
import { useVehiclePayload } from '@/composables/useVehiclePayload'
const props = defineProps({
data: Object,
params: Object,
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => {} },
title: { type: String, default: '个人名下车辆' },
})
const { obj } = useVehiclePayload(props)
const vehicleCount = computed(() => {
const n = obj.value.vehicleCount ?? obj.value.carNum
if (n === '' || n == null)
return null
const num = Number(n)
return Number.isFinite(num) ? num : null
})
const vehicleList = computed(() => {
const list = obj.value.list
return Array.isArray(list) ? list : []
})
</script>
<template>
<div class="card bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div class="flex justify-between items-center mb-4 pb-4 border-b border-gray-100">
<div class="text-lg font-semibold text-gray-900">{{ title }}</div>
<div class="bg-blue-50 text-blue-700 px-4 py-2 rounded-full text-sm font-medium">
{{ vehicleCount != null ? vehicleCount : '—' }}
</div>
</div>
<div v-if="vehicleList.length" class="space-y-3">
<div
v-for="(vehicle, index) in vehicleList"
:key="index"
class="bg-gray-50 rounded-lg p-4 border border-gray-200"
>
<div class="text-xl font-bold text-gray-900 font-mono tracking-wider">
{{ vehicle.plateNum ?? '—' }}
</div>
<div class="flex items-center gap-3 mt-2 flex-wrap">
<span class="inline-flex items-center px-3 py-1 rounded text-sm font-medium text-white bg-blue-500">
{{ plateColorLabel(vehicle.plateColor) }}
</span>
<span class="text-sm text-gray-600">
车辆类型<span class="font-medium text-gray-900">{{ vehicleTypeLabel(vehicle.vehicleType) }}</span>
</span>
</div>
</div>
</div>
<div v-else class="text-center py-12 text-gray-500">
<div class="text-lg font-medium mb-1">暂无车辆信息</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,162 @@
/**
* 推广定价计算工具
* 统一管理价格计算逻辑,供 Promote.vue 和 PriceInputPopup.vue 共享
*/
/**
* 安全截断小数位数(四舍五入)
* 用于前端显示和发送给后端的价格格式化
* @param {number} num - 要格式化的数值
* @param {number} decimals - 保留小数位数默认2位
* @returns {string} 格式化后的字符串,如 "12.34"
*/
export function safeTruncate(num, decimals = 2) {
if (Number.isNaN(num) || !Number.isFinite(num)) {
return '0.00'
}
const factor = 10 ** decimals
const scaled = Math.round(num * factor)
return (scaled / factor).toFixed(decimals)
}
/**
* 将元转为分(整数),避免浮点运算精度问题
* @param {number} num - 元为单位的数值
* @returns {number} 分为单位的整数
*/
function toTruncatedCents(num) {
if (Number.isNaN(num) || !Number.isFinite(num)) {
return 0
}
return Math.round(num * 100)
}
/**
* 将分转回元返回固定2位小数字符串
* @param {number} cents - 分为单位的整数
* @returns {string} 元为单位的字符串,如 "12.34"
*/
function centsToFixed(cents) {
return (cents / 100).toFixed(2)
}
/**
* 计算提价成本
* 当客户查询价超过价格阈值时,超出部分按费率收取
* @param {number} price - 客户查询价
* @param {object} config - 产品配置
* @returns {number} 提价成本(元)
*/
function calculateRaiseCost(price, config) {
const priceThreshold = Number(config.price_threshold) || 0
const priceFeeRate = Number(config.price_fee_rate) || 0
if (priceThreshold <= 0 || priceFeeRate <= 0) {
return 0
}
if (price <= priceThreshold) {
return 0
}
return (price - priceThreshold) * priceFeeRate
}
/**
* 计算推广定价(核心函数)
* 所有金额先转为分(整数)再计算,避免浮点精度问题
*
* @param {number|string} priceInput - 客户查询价
* @param {object} config - 产品配置,包含以下字段:
* - actual_base_price: 实际底价(含等级加成)
* - price_threshold: 价格阈值(超过此价格收取提价费用)
* - price_fee_rate: 提价费率0-1之间的小数
* @returns {{ costPrice: string, baseCost: string, raiseCost: string, promotionRevenue: string }}
* 各项价格均为 "xx.xx" 格式的字符串
*/
export function calculatePromotionPricing(priceInput, config) {
if (!config) {
return {
costPrice: '0.00',
baseCost: '0.00',
raiseCost: '0.00',
promotionRevenue: '0.00',
}
}
const price = Number(priceInput)
if (!Number.isFinite(price)) {
return {
costPrice: '0.00',
baseCost: '0.00',
raiseCost: '0.00',
promotionRevenue: '0.00',
}
}
const baseCost = Number(config.actual_base_price) || 0
const raiseCost = calculateRaiseCost(price, config)
const totalCost = baseCost + raiseCost
// 转为分计算,避免浮点精度问题
const priceCents = toTruncatedCents(price)
const baseCostCents = toTruncatedCents(baseCost)
const raiseCostCents = toTruncatedCents(raiseCost)
const totalCostCents = toTruncatedCents(totalCost)
const revenueCents = Math.max(0, priceCents - totalCostCents)
return {
costPrice: centsToFixed(totalCostCents),
baseCost: centsToFixed(baseCostCents),
raiseCost: centsToFixed(raiseCostCents),
promotionRevenue: centsToFixed(revenueCents),
}
}
/**
* 验证价格输入
* @param {number|string} currentPrice - 当前输入的价格
* @param {object} productConfig - 产品配置
* @param {number} defaultPrice - 默认价格(用于无效输入时的回退)
* @returns {{ newPrice: number, message: string }}
*/
export function validatePrice(currentPrice, productConfig, defaultPrice) {
if (!productConfig) {
return { newPrice: Number(defaultPrice || 0), message: '产品配置未就绪,请稍后再试' }
}
const min = Number(productConfig.price_range_min) || 0
const max = Number(productConfig.price_range_max) || Infinity
let newPrice = Number(currentPrice)
let message = ''
// 处理无效输入
if (Number.isNaN(newPrice)) {
newPrice = Number(defaultPrice || 0)
return { newPrice, message: '输入无效,请输入价格' }
}
// 处理小数位数(兼容科学计数法)
try {
const priceString = newPrice.toString()
const [_, decimalPart = ''] = priceString.split('.')
if (decimalPart.length > 2) {
newPrice = Number.parseFloat(safeTruncate(newPrice))
message = '价格已自动格式化为两位小数'
}
} catch (e) {
console.error('价格格式化异常:', e)
}
// 范围校验
if (newPrice < min) {
message = `价格不能低于 ${min}`
newPrice = min
} else if (newPrice > max) {
message = `价格不能高于 ${max}`
newPrice = max
}
return { newPrice, message }
}

View File

@@ -0,0 +1,73 @@
/**
* 解包车辆报告业务体:兼容 BaseReport 传入的 data 仍带一层 { apiID, data } 或 JSON 字符串
*/
export function unwrapVehiclePayload(raw, depth = 0) {
if (raw == null || depth > 5)
return raw
if (typeof raw === 'string') {
const s = raw.trim()
if (!s)
return null
try {
return unwrapVehiclePayload(JSON.parse(s), depth + 1)
}
catch {
return raw
}
}
if (Array.isArray(raw))
return raw
if (typeof raw !== 'object')
return raw
const hasApiWrapper = ('apiID' in raw || 'apiId' in raw) && 'data' in raw
if (hasApiWrapper)
return unwrapVehiclePayload(raw.data ?? raw, depth + 1)
if (typeof raw.data === 'string' || (raw.data && typeof raw.data === 'object' && !Array.isArray(raw.data))) {
const inner = raw.data
const onlyDataKey = Object.keys(raw).length <= 2 && 'data' in raw
if (onlyDataKey || (typeof inner === 'object' && inner && !Array.isArray(inner) && Object.keys(inner).length > 0 && !('list' in raw) && !('vehicleCount' in raw) && !('carNum' in raw)))
return unwrapVehiclePayload(inner, depth + 1)
}
return raw
}
/** 解析为数组直连数组、list、record、retdata 等 */
export function payloadAsArray(raw) {
const p = unwrapVehiclePayload(raw)
if (!p)
return []
if (Array.isArray(p))
return p
if (typeof p !== 'object')
return []
if (Array.isArray(p.list))
return p.list
if (Array.isArray(p.record))
return p.record
if (Array.isArray(p.retdata))
return p.retdata
if (typeof p.data === 'string') {
try {
const parsed = JSON.parse(p.data)
return Array.isArray(parsed) ? parsed : []
}
catch {
return []
}
}
return []
}
/** 解析为对象 */
export function payloadAsObject(raw) {
const p = unwrapVehiclePayload(raw)
if (!p || typeof p !== 'object' || Array.isArray(p))
return {}
return p
}

View File

@@ -0,0 +1,73 @@
/** 车辆报告共用车牌颜色、车辆类型、日期里程文案 */
export const PLATE_COLOR_LABELS = {
0: '蓝色',
1: '黄色',
2: '黑色',
3: '白色',
4: '渐变绿',
5: '黄绿双拼',
6: '蓝白渐变',
7: '临牌',
11: '绿色',
12: '红色',
}
export function plateColorLabel(c) {
const n = Number(c)
if (Number.isNaN(n))
return '—'
return PLATE_COLOR_LABELS[n] ?? '其他'
}
const VEHICLE_TYPE_LABELS = {
1: '一型客车',
2: '二型客车',
3: '三型客车',
4: '四型客车',
11: '一型货车',
12: '二型货车',
13: '三型货车',
14: '四型货车',
15: '五型货车',
16: '六型货车',
21: '一型专项作业车',
22: '二型专项作业车',
23: '三型专项作业车',
24: '四型专项作业车',
25: '五型专项作业车',
26: '六型专项作业车',
}
export function vehicleTypeLabel(t) {
if (t == null || t === '')
return '—'
const n = Number(t)
if (!Number.isNaN(n) && VEHICLE_TYPE_LABELS[n])
return VEHICLE_TYPE_LABELS[n]
return String(t)
}
export function formatDateZh(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 String(val)
}
export function formatMileageKm(val) {
if (val !== 0 && !val)
return '-'
const num = Number(val)
if (Number.isNaN(num))
return `${val} km`
return `${num.toLocaleString()} km`
}
export function maskName(name) {
if (!name)
return '-'
return name.length > 1 ? `${name[0]}${'*'.repeat(name.length - 1)}` : '*'
}

View File

@@ -0,0 +1,44 @@
import { unwrapVehiclePayload } from './vehiclePayload'
/**
* 规范 query_data 单项,供 BaseReport 使用:{ data: { apiID, data: 业务体 } }
*/
export function normalizeVehicleReportItem(item) {
if (!item || typeof item !== 'object')
return item
const outer = item.data
if (outer == null)
return item
let apiID = ''
let payload = outer
if (typeof outer === 'object' && !Array.isArray(outer)) {
apiID = String(outer.apiID ?? outer.apiId ?? '')
if ('data' in outer)
payload = outer.data
}
payload = unwrapVehiclePayload(payload)
if (!apiID && payload && typeof payload === 'object' && !Array.isArray(payload)) {
apiID = String(payload.apiID ?? payload.apiId ?? '')
if (apiID && 'data' in payload)
payload = unwrapVehiclePayload(payload.data)
}
return {
...item,
data: {
apiID: apiID || '',
data: payload,
},
}
}
export function normalizeVehicleReportData(items) {
if (!Array.isArray(items))
return []
return items.map(normalizeVehicleReportItem)
}

View File

@@ -1,7 +1,7 @@
<template>
<div class="min-h-screen bg-gray-50">
<!-- 收益列表 -->
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
<van-list v-model:loading="loading" :finished="finished" :finished-text="data.list.length > 0 ? '没有更多了' : ''" @load="onLoad">
<div v-for="(item, index) in data.list" :key="index" class="mx-4 my-2 bg-white rounded-lg p-4 shadow-sm">
<div class="flex justify-between items-center mb-2">
<span class="text-gray-500 text-sm">{{ item.create_time || '-' }}</span>
@@ -19,11 +19,15 @@
</div>
</div>
</van-list>
<!-- 空状态 -->
<EmptyState v-if="!loading && !data.list.length" text="暂无佣金记录" />
</div>
</template>
<script setup>
import { getCommissionList } from '@/api/agent'
import EmptyState from '@/components/EmptyState.vue'
// 颜色配置(根据产品名称映射)
const typeColors = {
@@ -60,16 +64,11 @@ const getDotColor = (name) => {
// 加载更多数据
const onLoad = async () => {
if (!finished.value) {
page.value++
await getData()
}
await getData()
}
// 获取数据
const getData = async () => {
if (loading.value || finished.value) return
try {
loading.value = true
const { data: res, error } = await getCommissionList({
@@ -78,37 +77,33 @@ const getData = async () => {
})
if (res.value?.code === 200 && !error.value) {
// 首次加载
const list = res.value.data.list || []
const total = res.value.data.total || 0
if (page.value === 1) {
data.value = res.value.data
data.value = { total, list }
} else {
// 分页加载
data.value.list.push(...res.value.data.list)
data.value.list.push(...list)
}
// 判断是否加载完成
if (data.value.list.length >= res.value.data.total ||
res.value.data.list.length < pageSize.value) {
if (data.value.list.length >= total ||
list.length < pageSize.value) {
finished.value = true
} else {
page.value++
}
} else {
// 接口返回错误或请求失败,停止翻页
finished.value = true
console.error('获取佣金列表失败:', res.value?.msg || error.value || '未知错误')
}
} catch (err) {
// 捕获异常,停止翻页
finished.value = true
console.error('获取佣金列表失败:', err)
} finally {
loading.value = false
}
}
// 初始化加载
onMounted(() => {
getData()
})
</script>
<style scoped>

View File

@@ -1,5 +1,7 @@
<script setup>
import { ref, onMounted } from 'vue'
import { ref } from 'vue'
import EmptyState from '@/components/EmptyState.vue'
const router = useRouter()
const page = ref(1)
const pageSize = ref(10)
@@ -9,7 +11,6 @@ const loading = ref(false)
const finished = ref(false)
async function fetchData() {
if (loading.value || finished.value) return
loading.value = true
try {
const { data, error } = await useApiFetch(`/agent/promotion/query/list?page=${page.value}&page_size=${pageSize.value}`).get().json()
@@ -17,11 +18,17 @@ async function fetchData() {
if (data.value.code === 200) {
total.value = data.value.data.total
if (data.value.data.list && data.value.data.list.length > 0) {
reportList.value.push(...data.value.data.list)
page.value += 1
if (page.value === 1) {
reportList.value = data.value.data.list
} else {
reportList.value.push(...data.value.data.list)
}
}
if (reportList.value.length >= total.value) {
if (reportList.value.length >= total.value || (data.value.data.list || []).length < pageSize.value) {
finished.value = true
} else {
page.value++
}
} else {
finished.value = true
@@ -39,14 +46,8 @@ async function fetchData() {
}
}
onMounted(() => {
fetchData()
})
const onLoad = () => {
if (!finished.value) {
fetchData()
}
fetchData()
}
function toDetail(item) {
@@ -87,7 +88,7 @@ function statusClass(state) {
<template>
<div class="flex flex-col gap-4 p-4">
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
<van-list v-model:loading="loading" :finished="finished" :finished-text="reportList.length > 0 ? '没有更多了' : ''" @load="onLoad">
<div v-for="item in reportList" :key="item.id" @click="toDetail(item)"
class="bg-white rounded-lg shadow-sm p-4 mb-4 relative cursor-pointer">
<div class="flex flex-col">
@@ -100,9 +101,10 @@ function statusClass(state) {
</div>
</div>
</van-list>
<!-- 空状态 -->
<EmptyState v-if="!loading && reportList.length === 0" text="暂无推广查询记录" />
</div>
<van-empty v-if="!loading && reportList.length === 0" description="暂无推广查询记录" />
</template>
<style scoped>

View File

@@ -7,7 +7,7 @@
</van-tabs>
<!-- 收益列表 -->
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
<van-list v-model:loading="loading" :finished="finished" :finished-text="data.list.length > 0 ? '没有更多了' : ''" @load="onLoad">
<div v-for="(item, index) in data.list" :key="index" class="mx-4 my-2 bg-white rounded-lg p-4 shadow-sm">
<div class="flex justify-between items-center mb-2">
<span class="text-gray-500 text-sm">{{ item.create_time || '-' }}</span>
@@ -45,12 +45,16 @@
</div>
</div>
</van-list>
<!-- 空状态 -->
<EmptyState v-if="!loading && !data.list.length" text="暂无返佣记录" />
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref } from 'vue'
import { getRebateList, getUpgradeRebateList } from '@/api/agent'
import EmptyState from '@/components/EmptyState.vue'
// 返佣类型映射配置(推广返佣)
const typeConfig = {
@@ -162,16 +166,11 @@ const onTabChange = (name) => {
// 加载更多数据
const onLoad = async () => {
if (!finished.value) {
page.value++
await getData()
}
await getData()
}
// 获取数据
const getData = async () => {
if (loading.value || finished.value) return
try {
loading.value = true
@@ -227,6 +226,8 @@ const getData = async () => {
// 判断是否加载完成
if (data.value.list.length >= total || list.length < pageSize.value) {
finished.value = true
} else {
page.value++
}
} else {
// 接口返回错误或请求失败,停止翻页
@@ -241,11 +242,6 @@ const getData = async () => {
loading.value = false
}
}
// 初始化加载
onMounted(() => {
getData()
})
</script>
<style scoped>

View File

@@ -1,5 +1,5 @@
<script setup>
import { ref, onMounted, computed } from 'vue'
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/stores/userStore'
@@ -20,11 +20,9 @@ const max = ref(60)
const loading = ref(false)
const finished = ref(false)
const showBindNotice = computed(() => isLoggedIn.value && !mobile.value && reportList.value.length > 0)
const hasNoRecords = computed(() => reportList.value.length === 0)
// 初始加载数据
async function fetchData() {
if (loading.value || finished.value) return
// 加载数据
async function fetchData() {
loading.value = true
try {
const { data, error } = await useApiFetch(`query/list?page=${page.value}&page_size=${pageSize.value}`)
@@ -34,24 +32,27 @@ async function fetchData() {
if (data.value.code === 200) {
total.value = data.value.data.total
if (data.value.data.list && data.value.data.list.length > 0) {
reportList.value.push(...data.value.data.list)
page.value += 1
if (page.value === 1) {
reportList.value = data.value.data.list
} else {
reportList.value.push(...data.value.data.list)
}
}
if (reportList.value.length >= total.value) {
if (reportList.value.length >= total.value || (data.value.data.list || []).length < pageSize.value) {
finished.value = true
} else {
page.value++
}
} else {
// 接口返回错误,停止翻页
finished.value = true
console.error('获取查询列表失败:', data.value.msg || '未知错误')
}
} else {
// 请求失败或返回错误,停止翻页
finished.value = true
console.error('获取查询列表失败:', error.value || '请求失败')
}
} catch (err) {
// 捕获异常,停止翻页
finished.value = true
console.error('获取查询列表失败:', err)
} finally {
@@ -59,21 +60,9 @@ async function fetchData() {
}
}
// 初始加载
onMounted(async () => {
fetchData()
})
// 下拉触底加载更多
// 触底加载更多
const onLoad = () => {
if (!finished.value) {
console.log("finished", finished.value)
if (num.value >= max.value) {
finished.value = true
} else {
fetchData()
}
}
fetchData()
}
function toDetail(item) {
@@ -91,7 +80,6 @@ function handleBindSuccess() {
page.value = 1
finished.value = false
loading.value = false
fetchData()
}
// 状态文字映射
@@ -137,13 +125,7 @@ function statusClass(state) {
@click="dialogStore.openBindPhone">绑定手机号</button>
</div>
<div class="text-xs text-gray-500">为保障用户隐私及数据安全报告保留{{ appStore.queryRetentionDays || 30 }}过期自动清理</div>
<div v-if="hasNoRecords"
class="bg-white rounded-lg shadow-sm p-6 flex flex-col items-center justify-center gap-3">
<div class="text-gray-600 text-sm">暂无历史报告</div>
<button v-if="!isLoggedIn || !mobile" class="px-4 py-2 bg-blue-500 text-white rounded"
@click="toLogin">登录</button>
</div>
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
<van-list v-model:loading="loading" :finished="finished" :finished-text="reportList.length > 0 ? '没有更多了' : ''" @load="onLoad">
<div v-for="item in reportList" :key="item.id" @click="toDetail(item)"
class="bg-white rounded-lg shadow-sm p-4 mb-4 relative cursor-pointer">
<div class="flex flex-col">
@@ -157,6 +139,14 @@ function statusClass(state) {
</div>
</van-list>
<!-- 空状态 -->
<div v-if="!loading && reportList.length === 0"
class="bg-white rounded-lg shadow-sm p-6 flex flex-col items-center justify-center gap-3">
<div class="text-gray-600 text-sm">暂无历史报告</div>
<button v-if="!isLoggedIn || !mobile" class="px-4 py-2 bg-blue-500 text-white rounded"
@click="toLogin">登录</button>
</div>
</div>
</template>

View File

@@ -18,45 +18,127 @@ const feature = ref(route.params.feature);
// 获取产品信息
const featureData = ref({});
// 检查是否可以查询:已登录且已绑定手机号
// 检查是否可以查询:不需要登录,直接允许查询
const canQuery = computed(() => {
return isLoggedIn.value && mobile.value && mobile.value.trim() !== '';
return true; // 允许未登录用户查询
});
// 检查登录状态和手机号绑定
// 初始化:检查支付回调并加载产品信息
onMounted(async () => {
// 检查支付回调
isFinishPayment();
// 检查是否已登录
// 如果有 token尝试加载用户信息但不强制
const token = localStorage.getItem("token");
if (!token) {
showToast({ message: "请先登录才能使用查询功能" });
router.replace("/login");
return;
if (token) {
try {
await userStore.fetchUserInfo();
} catch (error) {
console.warn("获取用户信息失败(可选):", error);
// 不影响查询功能,继续执行
}
}
// 获取用户信息(包括手机号)
try {
await userStore.fetchUserInfo();
} catch (error) {
console.error("获取用户信息失败:", error);
showToast({ message: "获取用户信息失败,请重新登录" });
router.replace("/login");
return;
// 添加婚姻查询的特殊处理
if (feature.value === 'marriage') {
showMarriageUpgradeNotice();
}
// 检查是否已绑定手机号
if (!mobile.value || mobile.value.trim() === '') {
showToast({ message: "请先绑定手机号才能使用查询功能" });
router.replace("/me");
return;
}
// 已登录且已绑定手机号,可以查询
// 直接加载产品信息,不需要登录
await getProduct();
});
function showMarriageUpgradeNotice() {
// 创建自定义弹窗
const modal = document.createElement('div');
modal.style.position = 'fixed';
modal.style.top = '0';
modal.style.left = '0';
modal.style.width = '100%';
modal.style.height = '100%';
modal.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
modal.style.display = 'flex';
modal.style.justifyContent = 'center';
modal.style.alignItems = 'center';
modal.style.zIndex = '9999';
modal.innerHTML = `
<div style="
background: white;
border-radius: 12px;
padding: 24px;
margin: 20px;
max-width: 400px;
width: 90%;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
position: relative;
overflow-y: auto;
max-height: 90vh;
">
<h3 style="
color: #333;
text-align: center;
margin-top: 0;
margin-bottom: 16px;
font-size: 18px;
">亲爱的用户,您好!</h3>
<p style="
color: #666;
line-height: 1.6;
margin: 8px 0;
text-align: center;
font-size: 14px;
">婚恋报告正在优化升级中,需要查询请前往天远查。</p>
<div style="display: flex; gap: 12px; margin-bottom: 16px;">
<button id="jumpNowBtn" style="
flex: 1;
background: linear-gradient(135deg, #ff6b6b, #ee5a24);
color: white;
border: none;
padding: 12px 0;
border-radius: 25px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
">是</button>
<button id="remindLaterBtn" style="
flex: 1;
background: #f8f9fa;
color: #666;
border: 1px solid #ddd;
padding: 12px 0;
border-radius: 25px;
cursor: pointer;
font-size: 14px;
">否</button>
</div>
</div>
`;
document.body.appendChild(modal);
// 绑定按钮事件
const jumpNowBtn = modal.querySelector('#jumpNowBtn');
const remindLaterBtn = modal.querySelector('#remindLaterBtn');
jumpNowBtn.addEventListener('click', () => {
// 跳转到新版首页
window.open('https://www.tianyuancha.cn/', '_blank');
document.body.removeChild(modal);
});
remindLaterBtn.addEventListener('click', () => {
// 跳转到指定网站
window.location.href = 'https://www.quannengcha.com/';
});
}
function isFinishPayment() {
const query = new URLSearchParams(window.location.search);
let orderNo = query.get("out_trade_no");
@@ -88,20 +170,6 @@ async function getProduct() {
</script>
<template>
<!-- 未登录或未绑定手机号提示 -->
<div v-if="!canQuery" class="min-h-screen flex items-center justify-center p-6">
<div class="text-center">
<div class="text-lg font-bold mb-4">无法使用查询功能</div>
<div class="text-gray-600 mb-6">
<span v-if="!isLoggedIn">请先登录</span>
<span v-else>请先绑定手机号</span>
</div>
<button @click="router.push(isLoggedIn ? '/me' : '/login')"
class="px-6 py-3 bg-primary text-white rounded-lg">
{{ isLoggedIn ? '去绑定手机号' : '去登录' }}
</button>
</div>
</div>
<!-- 已登录且已绑定手机号可以使用查询功能 -->
<InquireForm v-else :type="'normal'" :feature="feature" :feature-data="featureData" />
<!-- 直接显示查询表单不需要登录 -->
<InquireForm :type="'normal'" :feature="feature" :feature-data="featureData" />
</template>

View File

@@ -33,6 +33,8 @@
<script setup>
import { aesDecrypt } from "@/utils/crypto";
import { useAliyunCaptcha } from '@/composables/useAliyunCaptcha'
const { runWithCaptcha } = useAliyunCaptcha()
const showApplyPopup = ref(false);
const route = useRoute();
const router = useRouter();
@@ -48,6 +50,7 @@ const agentApply = () => {
showApplyPopup.value = true;
};
// 跳转到首页
const goToHome = () => {
router.replace("/promote");
@@ -77,50 +80,41 @@ onBeforeMount(async () => {
await store.fetchAgentStatus();
}
});
import { applyForAgent, registerByInviteCode } from "@/api/agent";
import useApiFetch from '@/composables/useApiFetch'
const submitApplication = async (formData) => {
const { region, mobile, wechat_id, code, referrer } = formData;
// 根据是否已登录选择不同的API
const isLoggedIn = !!localStorage.getItem("token");
const apiCall = isLoggedIn ? applyForAgent : registerByInviteCode;
let postData = {
region,
mobile,
wechat_id,
code,
referrer,
};
const apiUrl = isLoggedIn ? 'agent/apply' : 'agent/register/invite';
const { data, error } = await apiCall(postData);
if (data.value && !error.value) {
if (data.value.code === 200) {
showApplyPopup.value = false;
showToast({ message: "注册成功,您已成为代理!" });
// 更新token和状态
if (data.value.data.accessToken) {
localStorage.setItem("token", data.value.data.accessToken);
localStorage.setItem(
"refreshAfter",
data.value.data.refreshAfter
);
localStorage.setItem(
"accessExpire",
data.value.data.accessExpire
);
// 重新获取代理状态
await store.fetchAgentStatus();
await userStore.fetchUserInfo();
// 跳转到代理主页
router.replace("/agent");
// 使用滑块验证码保护申请接口
runWithCaptcha(
(captchaVerifyParam) =>
useApiFetch(apiUrl)
.post({ region, mobile, wechat_id, code, referrer, captchaVerifyParam })
.json(),
(res) => {
if (res.code === 200) {
showApplyPopup.value = false;
showToast({ message: "注册成功,您已成为代理!" });
// 更新token和状态
if (res.data.accessToken) {
localStorage.setItem("token", res.data.accessToken);
localStorage.setItem("refreshAfter", res.data.refreshAfter);
localStorage.setItem("accessExpire", res.data.accessExpire);
// 重新获取代理状态
store.fetchAgentStatus();
userStore.fetchUserInfo();
// 跳转到代理主页
router.replace("/agent");
}
} else {
showToast({ message: res.msg || "申请失败" });
}
} else {
console.log("申请失败", data.value);
}
}
);
};
</script>

View File

@@ -6,11 +6,13 @@ import { useUserStore } from '@/stores/userStore'
import { useRouter, useRoute } from 'vue-router'
import { mobileCodeLogin } from '@/api/user'
import useApiFetch from '@/composables/useApiFetch'
import { useAliyunCaptcha } from '@/composables/useAliyunCaptcha'
const router = useRouter()
const route = useRoute()
const agentStore = useAgentStore()
const userStore = useUserStore()
const { runWithCaptcha } = useAliyunCaptcha()
const phoneNumber = ref('')
const verificationCode = ref('')
@@ -35,25 +37,29 @@ async function sendVerificationCode() {
return
}
const { data, error } = await useApiFetch('auth/sendSms')
.post({ mobile: phoneNumber.value, actionType: 'login' })
.json()
if (data.value && !error.value) {
if (data.value.code === 200) {
showToast({ message: "获取成功" });
startCountdown()
// 聚焦到验证码输入框
nextTick(() => {
const verificationCodeInput = document.getElementById('verificationCode');
if (verificationCodeInput) {
verificationCodeInput.focus();
}
});
} else {
showToast(data.value.msg)
// 使用滑块验证码保护发送短信接口
runWithCaptcha(
(captchaVerifyParam) =>
useApiFetch('auth/sendSms')
.post({ mobile: phoneNumber.value, actionType: 'login', captchaVerifyParam })
.json(),
(res) => {
if (res.code === 200) {
showToast({ message: "获取成功" });
startCountdown()
// 聚焦到验证码输入框
nextTick(() => {
const verificationCodeInput = document.getElementById('verificationCode');
if (verificationCodeInput) {
verificationCodeInput.focus();
}
});
} else {
showToast(res.msg || "获取验证码失败")
}
}
}
);
}
function startCountdown() {
@@ -196,7 +202,7 @@ function toPrivacyPolicy() {
<!-- 注册按钮推广链接来源的登录不展示 -->
<button v-if="!hideRegister" class="register-btn" @click="goToRegister">
注册成为代理
成为代理
</button>
</div>
</div>

View File

@@ -203,8 +203,8 @@
<img src="@/assets/images/me/yqmgl.svg" class="w-8 h-8 object-contain" alt="邀请码管理" />
<span class="text-xs text-gray-700 font-medium text-center">邀请码管理</span>
</button>
<!-- 实名认证入口所有代理 -->
<button v-if="isAgent"
<!-- 实名认证入口所有代理未实名时显示 -->
<button v-if="isAgent && !agentStore.isRealName"
class="flex flex-col items-center justify-center gap-2 py-4 rounded-lg hover:bg-green-50 transition-colors"
@click="toRealNameAuth">
<img src="@/assets/images/me/smrz.svg" class="w-8 h-8 object-contain" alt="提现" />
@@ -259,6 +259,7 @@
</div>
</div>
<BindPhoneDialog />
<RealNameAuthDialog />
</div>
</template>
@@ -274,6 +275,7 @@ import { useDialogStore } from "@/stores/dialogStore";
import useApiFetch from "@/composables/useApiFetch";
import { getRevenueInfo } from '@/api/agent';
import BindPhoneDialog from "@/components/BindPhoneDialog.vue";
import RealNameAuthDialog from "@/components/RealNameAuthDialog.vue";
const router = useRouter();
const agentStore = useAgentStore();

View File

@@ -32,7 +32,7 @@ onMounted(() => {
title: '404 - 页面未找到 | 全能查',
description: '抱歉,您访问的页面不存在。全能查专业大数据风险管控平台,提供大数据风险报告查询、婚姻状况查询、个人信用评估等服务。',
keywords: '404, 页面未找到, 全能查, 大数据风险管控',
url: 'https://www.zhinengcha.cn/404'
url: 'https://www.quannengcha.com/404'
})
})
</script>

View File

@@ -31,19 +31,26 @@
<span class="text-gray-800">{{
paymentType === "agent_upgrade"
? "代理升级"
: "查询服务"
: paymentType === "whitelist"
? "模块下架"
: "查询服务"
}}</span>
</div>
</div>
<div v-if="paymentType === 'agent_upgrade'" class="text-center text-gray-600 mb-4">恭喜你升级代理等级成功享受更多权益
</div>
<div v-else-if="paymentType === 'whitelist'" class="text-center text-gray-600 mb-4">
模块下架成功该身份证号查询时将不显示此模块的数据
</div>
<div class="action-buttons grid grid-cols-1 gap-4">
<van-button block type="primary" class="rounded-lg" @click="handleNavigation">
{{
paymentType === "agent_upgrade"
? "查看代理信息"
: "查看查询结果"
: paymentType === "whitelist"
? "返回报告"
: "查看查询结果"
}}
</van-button>
</div>
@@ -74,7 +81,9 @@
<span class="text-gray-800">{{
paymentType === "agent_upgrade"
? "代理升级"
: "查询服务"
: paymentType === "whitelist"
? "模块下架"
: "查询服务"
}}</span>
</div>
<div class="flex justify-between">
@@ -137,7 +146,9 @@
<span class="text-gray-800">{{
paymentType === "agent_upgrade"
? "代理升级"
: "查询服务"
: paymentType === "whitelist"
? "模块下架"
: "查询服务"
}}</span>
</div>
<div class="flex justify-between">
@@ -329,6 +340,20 @@ const checkPaymentStatus = async () => {
return;
}
// 对于白名单类型,如果状态是已支付,跳转回报告页
if (paymentType.value === "whitelist" && newStatus === "paid") {
stopPolling();
const returnUrl = route.query.returnUrl;
if (returnUrl) {
router.replace(returnUrl);
} else if (window.history.length > 1) {
router.go(-1);
} else {
router.replace("/");
}
return;
}
// 如果状态不是 pending停止轮询
if (newStatus !== "pending") {
stopPolling();
@@ -414,6 +439,16 @@ function handleNavigation() {
router.replace("/agent");
agentStore.fetchAgentStatus();
userStore.fetchUserInfo();
} else if (paymentType.value === "whitelist") {
// 白名单支付:优先使用 returnUrl否则返回上一页
const returnUrl = route.query.returnUrl;
if (returnUrl) {
router.replace(returnUrl);
} else if (window.history.length > 1) {
router.go(-1);
} else {
router.replace("/");
}
} else {
// 跳转到查询结果页面
router.replace({

View File

@@ -93,6 +93,7 @@ import PriceInputPopup from '@/components/PriceInputPopup.vue';
import QRcode from '@/components/QRcode.vue';
import ReportFeatures from '@/components/ReportFeatures.vue';
import { getProductConfig, generateLink } from '@/api/agent';
import { calculatePromotionPricing, safeTruncate } from '@/utils/promotionPricing';
// 导入logo图片
import personalDataLogo from '@/assets/images/promote/personal_data_logo.png';
@@ -140,62 +141,14 @@ const currentLogo = computed(() => {
return logoMap[currentFeature.value] || null;
});
const costPrice = computed(() => {
if (!pickerProductConfig.value) return 0.00
// 新系统:成本价 = 实际底价actual_base_price
// actual_base_price = base_price + 等级加成
const actualBasePrice = Number(pickerProductConfig.value.actual_base_price) || 0;
const clientPriceNum = Number(clientPrice.value) || 0;
const priceThreshold = Number(pickerProductConfig.value.price_threshold) || 0;
const priceFeeRate = Number(pickerProductConfig.value.price_fee_rate) || 0;
// 计算提价成本
let priceCost = 0;
if (clientPriceNum > priceThreshold) {
priceCost = (clientPriceNum - priceThreshold) * priceFeeRate;
}
// 总成本 = 实际底价 + 提价成本
const totalCost = actualBasePrice + priceCost;
return safeTruncate(totalCost);
const pricingResult = computed(() => {
return calculatePromotionPricing(clientPrice.value, pickerProductConfig.value)
});
const baseCost = computed(() => {
if (!pickerProductConfig.value) return "0.00";
const actualBasePrice = Number(pickerProductConfig.value.actual_base_price) || 0;
return safeTruncate(actualBasePrice);
});
const raiseCost = computed(() => {
if (!pickerProductConfig.value) return "0.00";
const clientPriceNum = Number(clientPrice.value) || 0;
const priceThreshold = Number(pickerProductConfig.value.price_threshold) || 0;
const priceFeeRate = Number(pickerProductConfig.value.price_fee_rate) || 0;
let priceCost = 0;
if (clientPriceNum > priceThreshold) {
priceCost = (clientPriceNum - priceThreshold) * priceFeeRate;
}
return safeTruncate(priceCost);
});
const promotionRevenue = computed(() => {
const clientPriceNum = Number(clientPrice.value) || 0;
const costPriceNum = parseFloat(costPrice.value) || 0; // costPrice 返回字符串,需要转换为数字
const revenue = clientPriceNum - costPriceNum;
return safeTruncate(revenue >= 0 ? revenue : 0); // 确保收益不为负数
});
function safeTruncate(num, decimals = 2) {
if (isNaN(num) || !isFinite(num)) return "0.00";
const factor = 10 ** decimals;
const scaled = Math.trunc(num * factor);
const truncated = scaled / factor;
return truncated.toFixed(decimals);
}
const costPrice = computed(() => pricingResult.value.costPrice);
const baseCost = computed(() => pricingResult.value.baseCost);
const raiseCost = computed(() => pricingResult.value.raiseCost);
const promotionRevenue = computed(() => pricingResult.value.promotionRevenue);
// 获取产品信息
const getProductInfo = async () => {
if (!currentFeature.value) {
@@ -336,7 +289,7 @@ const generatePromotionCode = async () => {
// 新系统API使用 product_id、set_price 和 target_path
const { data, error } = await generateLink({
product_id: pickerProductConfig.value.product_id,
set_price: priceNum,
set_price: safeTruncate(priceNum),
target_path: targetPath
});

View File

@@ -6,11 +6,13 @@ import { useAgentStore } from '@/stores/agentStore'
import { useUserStore } from '@/stores/userStore'
import { useRoute, useRouter } from 'vue-router'
import useApiFetch from '@/composables/useApiFetch'
import { useAliyunCaptcha } from '@/composables/useAliyunCaptcha'
const router = useRouter()
const route = useRoute()
const agentStore = useAgentStore()
const userStore = useUserStore()
const { runWithCaptcha } = useAliyunCaptcha()
const appName = import.meta.env.VITE_APP_NAME || '全能查';
const phoneNumber = ref('')
@@ -22,6 +24,11 @@ const countdown = ref(60)
const isPhoneDisabled = ref(false)
let timer = null
// 填充默认邀请码
function fillInviteCode() {
inviteCode.value = '16800'
}
// 从URL参数中读取邀请码并自动填入如果用户已登录且有手机号则自动填充
onMounted(async () => {
const inviteCodeParam = route.query.invite_code;
@@ -79,25 +86,28 @@ async function sendVerificationCode() {
return
}
const { data, error } = await useApiFetch('auth/sendSms')
.post({ mobile: phoneNumber.value, actionType: 'agentApply' })
.json()
if (data.value && !error.value) {
if (data.value.code === 200) {
showToast({ message: "获取成功" });
startCountdown()
// 聚焦到验证码输入框
nextTick(() => {
const verificationCodeInput = document.getElementById('verificationCode');
if (verificationCodeInput) {
verificationCodeInput.focus();
}
});
} else {
showToast(data.value.msg)
// 使用滑块验证码保护发送短信接口
runWithCaptcha(
(captchaVerifyParam) =>
useApiFetch('auth/sendSms')
.post({ mobile: phoneNumber.value, actionType: 'agentApply', captchaVerifyParam })
.json(),
(res) => {
if (res.code === 200) {
showToast({ message: "获取成功" });
startCountdown()
// 聚焦到验证码输入框
nextTick(() => {
const verificationCodeInput = document.getElementById('verificationCode');
if (verificationCodeInput) {
verificationCodeInput.focus();
}
});
} else {
showToast(res.msg || "获取验证码失败")
}
}
}
);
}
function startCountdown() {
@@ -245,7 +255,10 @@ const onClickLeft = () => {
<!-- 邀请码输入 -->
<div class="form-item">
<div class="form-label">邀请码</div>
<input v-model="inviteCode" class="form-input" type="text" placeholder="请输入邀请码" />
<div class="input-with-btn">
<input v-model="inviteCode" class="form-input" type="text" placeholder="请输入邀请码" />
<button class="get-invite-code-btn" @click="fillInviteCode">获取邀请码</button>
</div>
</div>
<!-- 手机号输入 -->
@@ -327,6 +340,30 @@ const onClickLeft = () => {
border-bottom: 1px solid var(--color-border-primary);
}
/* 输入框和按钮组合 */
.input-with-btn {
position: relative;
display: flex;
align-items: center;
flex: 1;
}
.input-with-btn .form-input {
padding-right: 6rem;
}
.get-invite-code-btn {
position: absolute;
right: 0;
background: none;
border: none;
color: var(--color-primary);
font-size: 0.875rem;
cursor: pointer;
padding: 0.5rem;
font-weight: 500;
}
.form-label {
font-size: 0.9375rem;
color: var(--color-text-primary);

View File

@@ -1,7 +1,7 @@
<template>
<BaseReport v-if="queryState === 'success'" :order-id="orderId" :order-no="orderNo" :feature="feature"
:reportData="reportData" :reportParams="reportParams" :reportName="reportName" :reportDateTime="reportDateTime"
:isEmpty="isEmpty" :isDone="isDone" :isExample="false" />
<BaseReport v-if="queryState === 'success'" :order-id="orderId" :order-no="orderNo" :query-id="queryId"
:feature="feature" :reportData="reportData" :reportParams="reportParams" :reportName="reportName"
:reportDateTime="reportDateTime" :isEmpty="isEmpty" :isDone="isDone" :isExample="false" />
<div v-else-if="queryState === 'pending'" class="loading-container">
<div class="loading-spinner"></div>
<p>报告生成中请稍候...</p>
@@ -27,6 +27,7 @@ const isEmpty = ref(false);
const isDone = ref(false);
const orderId = ref(null);
const orderNo = ref("");
const queryId = ref("");
const queryState = ref("");
const pollingInterval = ref(null);
@@ -98,6 +99,7 @@ const getReport = async () => {
queryState.value = decryptedData.query_state;
if (queryState.value === "success") {
feature.value = decryptedData.product || "";
queryId.value = decryptedData.id || decryptedData.query_id || "";
const sortedQueryData = Array.isArray(decryptedData.query_data)
? [...decryptedData.query_data].sort((a, b) => {
@@ -135,9 +137,30 @@ const getReport = async () => {
} else if (data.value.code === 200003) {
isEmpty.value = true;
isDone.value = true;
} else if (data.value.code === 200002) {
isPending.value = true;
isDone.value = true;
} else if (data.value.code === 200002 || data.value.code === 100005) {
// 200002报告生成中100005兼容旧后端“报告未就绪”时的 DB_ERROR按生成中轮询
queryState.value = "pending";
if (!pollingInterval.value) {
pollingInterval.value = setInterval(() => {
getReport();
}, 2000);
}
}
} else if (data.value?.code === 200002) {
// 接口返回 200002 时可能走不到上面分支,单独处理
queryState.value = "pending";
if (!pollingInterval.value) {
pollingInterval.value = setInterval(() => {
getReport();
}, 2000);
}
} else if (data.value?.code === 100005) {
// 兼容:报告未就绪时后端返回 100005按“生成中”轮询
queryState.value = "pending";
if (!pollingInterval.value) {
pollingInterval.value = setInterval(() => {
getReport();
}, 2000);
}
}
};

View File

@@ -1,7 +1,8 @@
<script setup>
import { ref, onMounted } from 'vue'
import { ref } from 'vue'
import { useRoute } from 'vue-router'
import useApiFetch from '@/composables/useApiFetch'
import EmptyState from '@/components/EmptyState.vue'
const route = useRoute()
const loading = ref(false)
@@ -23,9 +24,6 @@ const inviteListTotal = ref(0)
// 获取详情数据
const fetchDetail = async () => {
if (loading.value) return
if (finished.value && page.value > 1) return
loading.value = true
const tabType = activeTab.value
const { data, error } = await useApiFetch(
@@ -151,10 +149,6 @@ const formatNumber = num => {
if (!num) return '0.00'
return Number(num).toFixed(2)
}
onMounted(() => {
fetchDetail()
})
</script>
<template>
@@ -236,10 +230,9 @@ onMounted(() => {
<van-tabs v-model:active="activeTab" @change="switchTab">
<van-tab title="订单列表" name="order">
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="fetchDetail">
<van-list v-model:loading="loading" :finished="finished" :finished-text="orderList.length > 0 ? '没有更多了' : ''" @load="fetchDetail">
<div class="p-2">
<div v-if="orderList.length === 0" class="text-center text-gray-500 py-8">暂无订单记录</div>
<div v-else v-for="item in orderList" :key="item.order_no"
<div v-for="item in orderList" :key="item.order_no"
class="order-item mb-3 border-b border-gray-200 pb-3">
<div class="flex items-center justify-between">
<div class="flex-1">
@@ -260,13 +253,13 @@ onMounted(() => {
</div>
</van-list>
</van-pull-refresh>
<EmptyState v-if="!loading && orderList.length === 0" text="暂无订单记录" />
</van-tab>
<van-tab title="邀请列表" name="invite">
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="fetchDetail">
<van-list v-model:loading="loading" :finished="finished" :finished-text="inviteList.length > 0 ? '没有更多了' : ''" @load="fetchDetail">
<div class="p-2">
<div v-if="inviteList.length === 0" class="text-center text-gray-500 py-8">暂无邀请记录</div>
<div v-else v-for="item in inviteList" :key="item.agent_id"
<div v-for="item in inviteList" :key="item.agent_id"
class="invite-item mb-3 border-b border-gray-200 pb-3">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3 flex-1">
@@ -281,6 +274,7 @@ onMounted(() => {
</div>
</van-list>
</van-pull-refresh>
<EmptyState v-if="!loading && inviteList.length === 0" text="暂无邀请记录" />
</van-tab>
</van-tabs>
</div>

View File

@@ -1,8 +1,9 @@
<script setup>
import { ref, onMounted } from 'vue'
import { ref } from 'vue'
import { useAgentStore } from '@/stores/agentStore'
import { getSubordinateList } from '@/api/agent'
import { useRouter } from 'vue-router'
import EmptyState from '@/components/EmptyState.vue'
const agentStore = useAgentStore()
const subordinates = ref([])
@@ -15,16 +16,9 @@ const router = useRouter()
// 加载更多数据
const onLoad = async () => {
if (!finished.value) {
await fetchSubordinates()
}
await fetchSubordinates()
}
onBeforeMount(() => {
// 初始化时重置状态
page.value = 1
finished.value = false
})
// 计算统计数据
const statistics = ref({
totalSubordinates: 0,
@@ -32,8 +26,6 @@ const statistics = ref({
// 获取下级列表
const fetchSubordinates = async () => {
if (loading.value || finished.value) return
loading.value = true
try {
const { data, error } = await getSubordinateList({
@@ -131,10 +123,6 @@ const viewDetail = item => {
params: { id: item.agent_id || item.id },
})
}
onMounted(() => {
fetchSubordinates()
})
</script>
<template>
@@ -152,7 +140,7 @@ onMounted(() => {
</div>
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
<van-list v-model:loading="loading" :finished="finished" :finished-text="subordinates.length > 0 ? '没有更多了' : ''" @load="onLoad">
<div class="p-4">
<div v-for="(item, index) in subordinates" :key="item.id" class="subordinate-item">
<div class="flex flex-col p-5 bg-white rounded-xl shadow-sm mb-4">
@@ -199,6 +187,9 @@ onMounted(() => {
</div>
</van-list>
</van-pull-refresh>
<!-- 空状态 -->
<EmptyState v-if="!loading && !subordinates.length" text="暂无下级代理" />
</div>
</template>

View File

@@ -1,8 +1,9 @@
<script setup>
import { ref, onMounted, onBeforeMount } from 'vue'
import { ref, onMounted } from 'vue'
import { useAgentStore } from '@/stores/agentStore'
import { getTeamList } from '@/api/agent'
import { useRouter } from 'vue-router'
import EmptyState from '@/components/EmptyState.vue'
const agentStore = useAgentStore()
const teamMembers = ref([])
@@ -16,22 +17,12 @@ const searchMobile = ref('')
// 加载更多数据
const onLoad = async () => {
if (!finished.value) {
await fetchTeamMembers()
}
await fetchTeamMembers()
}
onBeforeMount(() => {
// 初始化时重置状态
page.value = 1
finished.value = false
})
// 获取团队列表
const fetchTeamMembers = async () => {
if (loading.value || finished.value) return
loading.value = true
try {
const params = {
@@ -155,10 +146,6 @@ const viewDetail = item => {
params: { id: item.agent_id || item.id },
})
}
onMounted(() => {
fetchTeamMembers()
})
</script>
<template>
@@ -176,7 +163,7 @@ onMounted(() => {
</div>
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
<van-list v-model:loading="loading" :finished="finished" :finished-text="teamMembers.length > 0 ? '没有更多了' : ''" @load="onLoad">
<div class="p-4">
<div v-for="(item, index) in teamMembers" :key="item.agent_id || item.id" class="team-member-item">
<div class="member-card">
@@ -236,6 +223,9 @@ onMounted(() => {
</div>
</van-list>
</van-pull-refresh>
<!-- 空状态 -->
<EmptyState v-if="!loading && !teamMembers.length" text="暂无团队成员" />
</div>
</template>

View File

@@ -1,118 +1,96 @@
<template>
<div class="upgrade-subordinate-page">
<div class="px-4 pt-3 pb-6 min-h-screen" style="background-color: var(--van-background);">
<!-- 页面头部 -->
<div class="page-header">
<div class="header-content">
<h1 class="page-title">调整下级级别</h1>
<p class="page-desc">钻石代理可以将团队中的普通代理升级为黄金代理</p>
</div>
</div>
<!-- 说明卡片 -->
<div class="info-card">
<div class="info-content">
<van-icon name="info-o" class="info-icon" />
<div class="info-text">
<p class="info-title">调整说明</p>
<ul class="info-list">
<li>仅可升级普通代理为黄金代理</li>
<li>调整操作免费无需支付费用</li>
</ul>
</div>
</div>
<div class="rounded-2xl px-5 pt-5 pb-4 mb-3" style="background: var(--van-theme-primary);">
<h1 class="text-[17px] font-semibold text-white leading-snug">调整下级级别</h1>
<p class="text-[13px] text-white/70 mt-1 leading-normal">将团队中的普通代理升级为黄金代理</p>
</div>
<!-- 搜索框 -->
<div class="search-section">
<van-field v-model="searchMobile" placeholder="输入手机号搜索" clearable @clear="handleClear"
@keyup.enter="handleSearch">
<template #button>
<van-button size="small" type="primary" @click="handleSearch" class="search-btn">搜索</van-button>
</template>
</van-field>
<div class="mb-3">
<van-search v-model="searchMobile" placeholder="输入手机号搜索" shape="round" show-action
@search="handleSearch" @cancel="handleClear" @clear="handleClear" />
</div>
<!-- 提示 -->
<div class="flex items-center gap-1.5 px-1 mb-4">
<van-icon name="info-o" class="text-[13px]" style="color: var(--van-text-color-2);" />
<p class="text-[12px] leading-normal" style="color: var(--van-text-color-2);">仅可升级普通代理为黄金代理操作免费且不可撤销</p>
</div>
<!-- 代理列表 -->
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
<div class="list-container">
<div v-if="teamMembers.length === 0 && !loading" class="empty-state">
<van-icon name="user-o" class="empty-icon" />
<p class="empty-text">暂无可调整的普通代理</p>
<van-list v-model:loading="loading" :finished="finished" :finished-text="teamMembers.length > 0 ? '没有更多了' : ''" @load="onLoad">
<div v-for="item in teamMembers" :key="item.agent_id || item.id"
class="bg-white rounded-2xl mb-2.5 overflow-hidden">
<!-- 顶部:用户信息 -->
<div class="flex items-center justify-between px-4 pt-4 pb-2.5">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<span class="text-[15px] font-medium truncate"
style="color: var(--van-text-color);">{{ item.mobile || '未绑定手机' }}</span>
<span class="inline-flex items-center px-1.5 py-[1px] rounded text-[11px] font-medium"
style="background: rgba(var(--van-theme-primary-rgb, 64,132,240), 0.08); color: var(--van-theme-primary);">普通代理</span>
</div>
<p class="text-[12px] mt-1" style="color: var(--van-text-color-2);">加入于 {{ formatTime(item.create_time) }}</p>
</div>
</div>
<div v-for="(item, index) in teamMembers" :key="item.agent_id || item.id" class="member-card">
<div class="member-header">
<div class="member-index">{{ index + 1 }}</div>
<div class="member-info">
<div class="member-mobile">{{ item.mobile || '未绑定手机' }}</div>
<div class="member-time">加入时间{{ formatTime(item.create_time) }}</div>
</div>
<div class="member-badge">
<span class="level-badge level-normal">普通代理</span>
</div>
<!-- 中间:数据统计 -->
<div class="grid grid-cols-3 mx-4 py-2.5"
style="border-top: 1px solid var(--van-border-color);">
<div class="text-center">
<div class="text-[11px] mb-1" style="color: var(--van-text-color-2);">查询量</div>
<div class="text-[14px] font-medium" style="color: var(--van-text-color);">{{ formatCount(item.total_queries || 0) }}</div>
</div>
<div class="text-center" style="border-left: 1px solid var(--van-border-color);">
<div class="text-[11px] mb-1" style="color: var(--van-text-color-2);">返佣</div>
<div class="text-[14px] font-medium" style="color: var(--van-text-color);">¥{{ formatNumber(item.total_rebate_amount || 0) }}</div>
</div>
<div class="text-center" style="border-left: 1px solid var(--van-border-color);">
<div class="text-[11px] mb-1" style="color: var(--van-text-color-2);">邀请</div>
<div class="text-[14px] font-medium" style="color: var(--van-text-color);">{{ formatCount(item.total_invites || 0) }}</div>
</div>
</div>
<div class="member-stats">
<div class="stat-item">
<div class="stat-label">总查询量</div>
<div class="stat-value">{{ formatCount(item.total_queries || 0) }}</div>
</div>
<div class="stat-item">
<div class="stat-label">返佣总额</div>
<div class="stat-value">¥{{ formatNumber(item.total_rebate_amount || 0) }}</div>
</div>
<div class="stat-item">
<div class="stat-label">邀请人数</div>
<div class="stat-value">{{ formatCount(item.total_invites || 0) }}</div>
</div>
</div>
<div class="member-action">
<van-button type="primary" size="small" :loading="item.upgrading"
@click="handleUpgrade(item)">
<van-icon name="arrow-up" class="mr-1" />
调整为黄金代理
</van-button>
</div>
<!-- 底部:操作 -->
<div class="flex justify-end px-4 pt-1 pb-3">
<van-button size="small" round :loading="item.upgrading" @click="handleUpgrade(item)"
style="background: var(--van-theme-primary); color: white; border: none; padding: 0 14px; font-size: 13px; height: 30px;">
升级为黄金代理
</van-button>
</div>
</div>
</van-list>
</van-pull-refresh>
<!-- 确认升级弹窗 -->
<van-popup v-model:show="showConfirmDialog" round position="center"
:style="{ width: '85%', maxWidth: '400px' }">
<div class="confirm-dialog">
<div class="confirm-header">
<van-icon name="warning-o" class="warning-icon" />
<h3 class="confirm-title">确认升级</h3>
</div>
<div class="confirm-content">
<p class="confirm-text">
确定要将 <span class="highlight">{{ currentUpgradeItem?.mobile }}</span> 升级为黄金代理吗
</p>
<div class="confirm-notice">
<p> 升级操作不可撤销</p>
<p> 升级操作免费</p>
</div>
</div>
<div class="confirm-actions">
<van-button plain @click="showConfirmDialog = false">取消</van-button>
<van-button type="primary" :loading="isUpgrading" @click="confirmUpgrade">确认升级</van-button>
<!-- 空状态 -->
<EmptyState v-if="!loading && !teamMembers.length" text="暂无可调整的普通代理" />
<!-- 确认弹窗 -->
<van-dialog v-model:show="showConfirmDialog" title="确认升级" show-cancel-button
:before-close="onBeforeClose" confirm-button-color="var(--van-theme-primary)">
<div class="px-6 pb-4 pt-4">
<p class="text-[14px] leading-relaxed mb-3" style="color: var(--van-text-color);">
确定将 <span class="font-semibold" style="color: var(--van-theme-primary);">{{ currentUpgradeItem?.mobile }}</span> 升级为黄金代理?
</p>
<div class="text-[12px] leading-loose" style="color: var(--van-text-color-2);">
<p>· 操作免费</p>
<p>· 升级后不可撤销</p>
</div>
</div>
</van-popup>
</van-dialog>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeMount, computed } from 'vue'
import { ref, onMounted, computed } from 'vue'
import { useAgentStore } from '@/stores/agentStore'
import { getTeamList, upgradeSubordinate } from '@/api/agent'
import { showToast, showSuccessToast, showFailToast } from 'vant'
import { useRouter } from 'vue-router'
import { storeToRefs } from 'pinia'
import EmptyState from '@/components/EmptyState.vue'
const router = useRouter()
const agentStore = useAgentStore()
@@ -128,28 +106,16 @@ const showConfirmDialog = ref(false)
const currentUpgradeItem = ref(null)
const isUpgrading = ref(false)
// 检查是否是钻石代理
const isDiamondAgent = computed(() => {
return isAgent.value && level.value === 3
})
// 加载更多数据
const onLoad = async () => {
if (!finished.value) {
await fetchTeamMembers()
}
await fetchTeamMembers()
}
onBeforeMount(() => {
page.value = 1
finished.value = false
})
// 获取团队列表(仅普通代理)
const fetchTeamMembers = async () => {
if (loading.value || finished.value) return
loading.value = true
loading.value = true
try {
const params = {
page: page.value,
@@ -164,8 +130,6 @@ const fetchTeamMembers = async () => {
if (data.value && !error.value) {
if (data.value.code === 200) {
let list = data.value.data.list || []
// 过滤出普通代理level === 1
list = list.filter(item => {
const agentLevel = item.level || (item.level_name === '普通' ? 1 : item.level_name === '黄金' ? 2 : item.level_name === '钻石' ? 3 : 1)
return agentLevel === 1
@@ -177,7 +141,6 @@ const fetchTeamMembers = async () => {
teamMembers.value.push(...list)
}
// 判断是否加载完成
if (list.length < pageSize) {
finished.value = true
} else if (teamMembers.value.length >= data.value.data.total) {
@@ -186,27 +149,21 @@ const fetchTeamMembers = async () => {
page.value++
}
} else {
// 接口返回错误,停止翻页
finished.value = true
showFailToast(data.value.msg || '获取团队列表失败')
}
} else {
// 请求失败或返回错误,停止翻页
finished.value = true
showFailToast('获取团队列表失败')
console.error('获取团队列表失败:', error.value || '请求失败')
}
} catch (err) {
// 捕获异常,停止翻页
finished.value = true
console.error('获取团队列表失败:', err)
showFailToast('获取团队列表失败')
} finally {
loading.value = false
}
}
// 下拉刷新
const onRefresh = () => {
finished.value = false
page.value = 1
@@ -215,14 +172,12 @@ const onRefresh = () => {
})
}
// 搜索功能
const handleSearch = () => {
finished.value = false
page.value = 1
fetchTeamMembers()
}
// 清空搜索
const handleClear = () => {
searchMobile.value = ''
finished.value = false
@@ -230,13 +185,19 @@ const handleClear = () => {
fetchTeamMembers()
}
// 处理升级
const handleUpgrade = (item) => {
currentUpgradeItem.value = item
showConfirmDialog.value = true
}
// 确认升级
const onBeforeClose = async (action) => {
if (action === 'confirm') {
await confirmUpgrade()
return false
}
return true
}
const confirmUpgrade = async () => {
if (!currentUpgradeItem.value) return
@@ -244,7 +205,7 @@ const confirmUpgrade = async () => {
try {
const { data, error } = await upgradeSubordinate({
subordinate_id: currentUpgradeItem.value.agent_id || currentUpgradeItem.value.id,
to_level: 2 // 只能升级为黄金代理
to_level: 2
})
if (data.value && !error.value) {
@@ -252,13 +213,9 @@ const confirmUpgrade = async () => {
showSuccessToast('升级成功')
showConfirmDialog.value = false
currentUpgradeItem.value = null
// 刷新列表
finished.value = false
page.value = 1
await fetchTeamMembers()
// 刷新代理状态
await agentStore.fetchAgentStatus()
} else {
showFailToast(data.value.msg || '升级失败')
@@ -267,307 +224,31 @@ const confirmUpgrade = async () => {
showFailToast('升级失败,请重试')
}
} catch (err) {
console.error('升级失败:', err)
showFailToast('升级失败,请重试')
} finally {
isUpgrading.value = false
}
}
// 格式化时间
const formatTime = (timeStr) => {
if (!timeStr) return '-'
return timeStr.split(' ')[0]
}
// 格式化金额
const formatNumber = (num) => {
if (!num) return '0.00'
return Number(num).toFixed(2)
}
// 格式化数字
const formatCount = (num) => {
if (!num) return '0'
return Number(num).toLocaleString()
}
onMounted(() => {
// 检查权限
if (!isDiamondAgent.value) {
showFailToast('只有钻石代理可以升级下级')
router.back()
return
}
fetchTeamMembers()
})
</script>
<style scoped>
.upgrade-subordinate-page {
min-height: 100vh;
background-color: #f5f7fa;
padding-bottom: 20px;
}
/* 页面头部 */
.page-header {
background: linear-gradient(120deg, #34c9ad 70%, #64d2ff 100%);
padding: 20px 16px;
color: white;
}
.header-content {
max-width: 100%;
}
.page-title {
font-size: 24px;
font-weight: 600;
margin-bottom: 8px;
}
.page-desc {
font-size: 14px;
opacity: 0.9;
margin: 0;
}
/* 说明卡片 */
.info-card {
margin: 16px;
background: white;
border-radius: 12px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.info-content {
display: flex;
gap: 12px;
}
.info-icon {
font-size: 24px;
color: #1677ff;
flex-shrink: 0;
margin-top: 2px;
}
.info-text {
flex: 1;
}
.info-title {
font-size: 16px;
font-weight: 600;
color: #1f2937;
margin-bottom: 8px;
}
.info-list {
margin: 0;
padding-left: 20px;
color: #6b7280;
font-size: 14px;
line-height: 1.8;
}
.info-list li {
margin-bottom: 4px;
}
/* 搜索区域 */
.search-section {
margin: 0 16px 16px;
}
.search-btn {
margin-left: 8px;
}
/* 列表容器 */
.list-container {
padding: 0 16px;
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 60px 20px;
color: #9ca3af;
}
.empty-icon {
font-size: 64px;
margin-bottom: 16px;
opacity: 0.5;
}
.empty-text {
font-size: 16px;
margin: 0;
}
/* 成员卡片 */
.member-card {
background: white;
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.member-header {
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: 12px;
}
.member-index {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: #e5e7eb;
color: #4b5563;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
flex-shrink: 0;
}
.member-info {
flex: 1;
min-width: 0;
}
.member-mobile {
font-size: 16px;
font-weight: 600;
color: #1f2937;
margin-bottom: 4px;
}
.member-time {
font-size: 12px;
color: #6b7280;
}
.member-badge {
flex-shrink: 0;
}
.level-badge {
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.level-normal {
background: #e5e7eb;
color: #4b5563;
}
.member-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 16px;
padding: 12px;
background: #f9fafb;
border-radius: 8px;
}
.stat-item {
text-align: center;
}
.stat-label {
font-size: 12px;
color: #6b7280;
margin-bottom: 4px;
}
.stat-value {
font-size: 14px;
font-weight: 600;
color: #1f2937;
}
.member-action {
display: flex;
justify-content: flex-end;
}
.mr-1 {
margin-right: 4px;
}
/* 确认弹窗 */
.confirm-dialog {
padding: 24px;
}
.confirm-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.warning-icon {
font-size: 24px;
color: #f59e0b;
}
.confirm-title {
font-size: 18px;
font-weight: 600;
color: #1f2937;
margin: 0;
}
.confirm-content {
margin-bottom: 24px;
}
.confirm-text {
font-size: 15px;
color: #374151;
line-height: 1.6;
margin-bottom: 16px;
}
.highlight {
color: #1677ff;
font-weight: 600;
}
.confirm-notice {
background: #fef3c7;
border-radius: 8px;
padding: 12px;
font-size: 13px;
color: #92400e;
line-height: 1.6;
}
.confirm-notice p {
margin: 4px 0;
}
.confirm-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
}
.confirm-actions .van-button {
flex: 1;
max-width: 120px;
}
</style>

View File

@@ -13,49 +13,122 @@
根据相关规定提现功能需要完成实名认证后才能使用提现金额将转入您实名认证的账户中
</p>
<van-button type="primary" block class="text-white rounded-xl h-10"
style="background-color: var(--van-theme-primary);"
@click="openRealNameAuth">
style="background-color: var(--van-theme-primary);" @click="openRealNameAuth">
立即实名认证
</van-button>
</div>
</div>
<div>
<!-- 提现卡片 -->
<div class="rounded-xl shadow-lg p-6 mb-4" style="background: linear-gradient(135deg, var(--van-theme-primary-light), rgba(255,255,255,0.8));">
<div class="flex items-center mb-6">
<van-icon name="alipay" class="text-xl mr-2" style="color: #1677FF;" />
<h1 class="text-xl font-bold" style="color: var(--van-text-color);">支付宝提现</h1>
<div class="rounded-xl shadow-lg p-6 mb-4"
style="background: linear-gradient(135deg, var(--van-theme-primary-light), rgba(255,255,255,0.8));">
<!-- 提现方式切换 -->
<div class="withdraw-tabs mb-5">
<div class="tab-container">
<div class="tab-slider" :class="withdrawalType === 2 ? 'tab-slider--right' : ''"></div>
<div class="tab-item" :class="{ 'tab-item--active': withdrawalType === 1 }"
@click="switchTab(1)">
<van-icon name="alipay" class="tab-vant-icon" />
<span class="tab-label">支付宝</span>
</div>
<div class="tab-item" :class="{ 'tab-item--active': withdrawalType === 2 }"
@click="switchTab(2)">
<van-icon name="card" class="tab-vant-icon" />
<span class="tab-label">银行卡</span>
</div>
</div>
</div>
<!-- 支付宝账号 -->
<div class="mb-6">
<label class="text-sm mb-2 block" style="color: var(--van-text-color);">支付宝账号</label>
<van-field v-model="alipayAccount" placeholder="请输入支付宝账号"
class="flex items-center rounded-lg bg-white/90 backdrop-blur-sm shadow-sm"
:rules="[{ required: true, message: ' ' }]">
<template #left-icon>
<van-icon name="phone-o" style="color: var(--van-text-color-2);" />
</template>
</van-field>
<small class="text-xs mt-1 block" style="color: var(--van-text-color-2);">可填写支付宝账户绑定的手机号</small>
<!-- 支付宝表单 -->
<div v-show="withdrawalType === 1" class="tab-content">
<div class="flex items-center mb-4">
<van-icon name="alipay" class="text-xl mr-2" style="color: #1677FF;" />
<span class="text-base font-medium" style="color: var(--van-text-color);">支付宝提现</span>
</div>
<!-- 支付宝账号 -->
<div class="mb-6">
<label class="text-sm mb-2 block" style="color: var(--van-text-color);">支付宝账号</label>
<van-field v-model="alipayAccount" placeholder="请输入支付宝账号"
class="flex items-center rounded-lg bg-white/90 backdrop-blur-sm shadow-sm"
:rules="[{ required: true, message: ' ' }]">
<template #left-icon>
<van-icon name="phone-o" style="color: var(--van-text-color-2);" />
</template>
</van-field>
<small class="text-xs mt-1 block"
style="color: var(--van-text-color-2);">可填写支付宝账户绑定的手机号</small>
</div>
<!-- 支付宝实名姓名 -->
<div class="mb-6">
<label class="text-sm mb-2 block" style="color: var(--van-text-color);">实名姓名</label>
<van-field v-model="realName" placeholder="请输入支付宝认证姓名"
class="flex items-center rounded-lg bg-white/90 backdrop-blur-sm shadow-sm" :rules="[
{
required: true,
message: ' ',
validator: (val) =>
/^[\u4e00-\u9fa5]{2,4}$/.test(val),
},
]">
<template #left-icon>
<van-icon name="contact-o" style="color: var(--van-text-color-2);" />
</template>
</van-field>
<small class="text-xs mt-1 block"
style="color: var(--van-text-color-2);">请填写支付宝账户认证的真实姓名</small>
</div>
</div>
<!-- 支付宝实名姓名 -->
<div class="mb-6">
<label class="text-sm mb-2 block" style="color: var(--van-text-color);">实名姓名</label>
<van-field v-model="realName" placeholder="请输入支付宝认证姓名"
class="flex items-center rounded-lg bg-white/90 backdrop-blur-sm shadow-sm" :rules="[
{
required: true,
message: ' ',
validator: (val) =>
/^[\u4e00-\u9fa5]{2,4}$/.test(val),
},
]">
<template #left-icon>
<van-icon name="contact-o" style="color: var(--van-text-color-2);" />
</template>
</van-field>
<small class="text-xs mt-1 block" style="color: var(--van-text-color-2);">请填写支付宝账户认证的真实姓名</small>
<!-- 银行卡表单 -->
<div v-show="withdrawalType === 2" class="tab-content">
<div class="flex items-center mb-4">
<van-icon name="card" class="text-xl mr-2"
style="color: var(--van-theme-primary);" />
<span class="text-base font-medium" style="color: var(--van-text-color);">银行卡提现</span>
</div>
<!-- 银行卡号 -->
<div class="mb-6">
<label class="text-sm mb-2 block" style="color: var(--van-text-color);">银行卡号</label>
<van-field v-model="bankCardNo" placeholder="请输入银行卡号" type="number"
class="flex items-center rounded-lg bg-white/90 backdrop-blur-sm shadow-sm"
:rules="[{ required: true, message: ' ' }]">
<template #left-icon>
<van-icon name="balance-list-o" style="color: var(--van-text-color-2);" />
</template>
</van-field>
<small class="text-xs mt-1 block"
style="color: var(--van-text-color-2);">请填写与实名一致的开户银行卡号</small>
</div>
<!-- 开户行名称 -->
<div class="mb-6">
<label class="text-sm mb-2 block" style="color: var(--van-text-color);">开户行名称</label>
<van-field v-model="bankName" placeholder="请输入开户行名称中国工商银行XX支行"
class="flex items-center rounded-lg bg-white/90 backdrop-blur-sm shadow-sm"
:rules="[{ required: true, message: ' ' }]">
<template #left-icon>
<van-icon name="shop-o" style="color: var(--van-text-color-2);" />
</template>
</van-field>
</div>
<!-- 收款人姓名 -->
<div class="mb-6">
<label class="text-sm mb-2 block" style="color: var(--van-text-color);">收款人姓名</label>
<van-field v-model="bankPayeeName" placeholder="请输入银行卡户主姓名"
class="flex items-center rounded-lg bg-white/90 backdrop-blur-sm shadow-sm" :rules="[
{
required: true,
message: ' ',
validator: (val) =>
/^[\u4e00-\u9fa5]{2,4}$/.test(val),
},
]">
<template #left-icon>
<van-icon name="contact-o" style="color: var(--van-text-color-2);" />
</template>
</van-field>
<small class="text-xs mt-1 block"
style="color: var(--van-text-color-2);">需与银行卡开户姓名一致</small>
</div>
</div>
<!-- 提现金额 -->
<div class="mb-4">
@@ -70,8 +143,7 @@
</template>
<template #right-icon> </template>
<template #button>
<van-button size="small" type="primary"
class="rounded-full px-3 shadow-sm"
<van-button size="small" type="primary" class="rounded-full px-3 shadow-sm"
style="background-color: var(--van-theme-primary); color: white;"
@click="fillMaxAmount">
全部提现
@@ -82,7 +154,8 @@
<!-- 金额提示 -->
<div class="text-sm mb-2" style="color: var(--van-text-color);">
可提现金额<span class="font-semibold" style="color: var(--van-theme-primary);">¥{{ availableAmount }}</span>
可提现金额<span class="font-semibold" style="color: var(--van-theme-primary);">¥{{ availableAmount
}}</span>
</div>
@@ -91,11 +164,19 @@
<div class="flex items-center text-sm mb-2" style="color: var(--van-theme-primary);">
<van-icon name="warning" class="mr-1" />提现须知
</div>
<ul class="text-xs space-y-1" style="color: var(--van-text-color);">
<!-- 支付宝提现说明 -->
<ul v-if="withdrawalType === 1" class="text-xs space-y-1" style="color: var(--van-text-color);">
<li>· 每日限提现1次最低50元</li>
<li>· 提现收取6%税收</li>
<li>· 到账时间24小时内</li>
</ul>
<!-- 银行卡提现说明 -->
<ul v-else class="text-xs space-y-1" style="color: var(--van-text-color);">
<li>· 每日限提现1次最低50元</li>
<li>· 提现收取6%税收</li>
<li>· 到账时间1-3个工作日需人工审核</li>
<li>· 银行卡提现请确保开户行户名与实名一致到账前会有审核</li>
</ul>
</div>
<!-- 税收说明 -->
@@ -146,7 +227,8 @@
<div class="border-t pt-2" style="border-color: var(--van-border-color);">
<div class="flex justify-between items-center">
<span class="font-medium" style="color: var(--van-text-color);">预估到账</span>
<span class="font-bold text-lg" style="color: var(--van-theme-primary);">¥{{ estimatedActualAmount.toFixed(2) }}</span>
<span class="font-bold text-lg" style="color: var(--van-theme-primary);">¥{{
estimatedActualAmount.toFixed(2) }}</span>
</div>
</div>
</div>
@@ -171,8 +253,8 @@
取消
</van-button>
<van-button block round class="flex-1 h-11 font-medium shadow-sm"
style="background-color: var(--van-theme-primary); color: white;"
:loading="isSubmitting" @click="confirmWithdraw">
style="background-color: var(--van-theme-primary); color: white;" :loading="isSubmitting"
@click="confirmWithdraw">
确认提现
</van-button>
</div>
@@ -201,11 +283,16 @@
</h2>
<template v-if="status === 2">
<p class="text-sm" style="color: var(--van-text-color-2);">
已向
<span style="color: var(--van-theme-primary);">{{
alipayAccount
}}</span>
转账
<template v-if="withdrawalType === 1">
已向
<span style="color: var(--van-theme-primary);">{{ alipayAccount }}</span>
转账
</template>
<template v-else>
已向
<span style="color: var(--van-theme-primary);">尾号{{ displayBankCardTail }}</span>
银行卡转账
</template>
</p>
<p class="text-2xl font-bold mt-2" style="color: #10b981;">
¥{{ amount }}
@@ -220,15 +307,15 @@
<!-- 进度条处理中状态 -->
<van-progress v-if="status === 1" :percentage="60" stroke-width="8"
:color="`linear-gradient(to right, var(--van-theme-primary), var(--van-theme-primary-light))`"
:track-color="`var(--van-theme-primary-light)`"
class="!rounded-full" />
:color="`linear-gradient(to right, var(--van-theme-primary), var(--van-theme-primary-light))`"
:track-color="`var(--van-theme-primary-light)`" class="!rounded-full" />
<!-- 辅助文案 -->
<div class="text-xs space-y-1.5" style="color: var(--van-text-color-2);">
<template v-if="status === 2">
<p>预计24小时内到账</p>
<p>可在支付宝账单中查看详情</p>
<p>预计1-3个工作日到账需人工审核</p>
<p v-if="withdrawalType === 1">可在支付宝账单中查看详情</p>
<p v-else>请留意银行卡到账通知</p>
</template>
<template v-if="status === 1">
<p>您的申请已进入处理队列</p>
@@ -237,8 +324,7 @@
</div>
<!-- 操作按钮 -->
<van-button block round size="small"
class="mt-4 h-11 font-medium shadow-sm"
<van-button block round size="small" class="mt-4 h-11 font-medium shadow-sm"
:style="{ backgroundColor: statusButtonColor[status], color: status === 1 ? 'var(--van-theme-primary)' : 'white' }"
@click="handlePopupAction">
{{
@@ -289,6 +375,12 @@ const estimatedActualAmount = computed(() => {
return Number(amount.value) - estimatedTaxAmount.value;
});
// 银行卡号尾号(成功弹窗展示用)
const displayBankCardTail = computed(() => {
const no = (bankCardNo.value || "").replace(/\s/g, "");
return no.length >= 4 ? no.slice(-4) : no || "****";
});
// 样式配置
const statusIcon = {
1: "clock",
@@ -325,11 +417,35 @@ const statusMessages = {
3: "提现失败",
};
// 表单数据
// 提现方式1=支付宝2=银行卡
const withdrawalType = ref(1);
// 表单数据 - 支付宝
const alipayAccount = ref("");
const amount = ref(0);
const availableAmount = ref(null);
const realName = ref("");
// 表单数据 - 银行卡
const bankCardNo = ref("");
const bankName = ref("");
const bankPayeeName = ref("");
// 切换提现方式时清空另一侧部分字段(金额保留)
const onWithdrawalTypeChange = () => {
if (withdrawalType.value === 1) {
bankCardNo.value = "";
bankName.value = "";
bankPayeeName.value = "";
} else {
alipayAccount.value = "";
realName.value = "";
}
};
const switchTab = (type) => {
if (withdrawalType.value === type) return;
withdrawalType.value = type;
onWithdrawalTypeChange();
};
const getData = async () => {
const { data, error } = await getRevenueInfo();
@@ -350,35 +466,58 @@ const validateAmount = (val) => {
};
const validateForm = () => {
if (!realName.value.trim()) {
showToast("请输入账户实名姓名");
return false;
}
if (!/^[\u4e00-\u9fa5]{2,4}$/.test(realName.value)) {
showToast("请输入2-4位中文姓名");
return false;
}
if (!alipayAccount.value.trim()) {
showToast("请输入支付宝账号");
return false;
}
const amountNum = Number(amount.value);
if (!amount.value || isNaN(amountNum)) {
showToast("请输入有效金额");
return false;
}
if (amountNum < 50) {
showToast("提现金额不能低于50元");
return false;
}
if (amountNum > availableAmount.value) {
showToast("超过可提现金额");
return false;
}
if (withdrawalType.value === 1) {
// 支付宝
if (!realName.value.trim()) {
showToast("请输入账户实名姓名");
return false;
}
if (!/^[\u4e00-\u9fa5]{2,4}$/.test(realName.value)) {
showToast("请输入2-4位中文姓名");
return false;
}
if (!alipayAccount.value.trim()) {
showToast("请输入支付宝账号");
return false;
}
} else {
// 银行卡
if (!bankCardNo.value.trim()) {
showToast("请输入银行卡号");
return false;
}
if (!/^\d{16,19}$/.test(bankCardNo.value.replace(/\s/g, ""))) {
showToast("请输入正确的银行卡号");
return false;
}
if (!bankName.value.trim()) {
showToast("请输入开户行名称");
return false;
}
if (!bankPayeeName.value.trim()) {
showToast("请输入收款人姓名");
return false;
}
if (!/^[\u4e00-\u9fa5]{2,4}$/.test(bankPayeeName.value)) {
showToast("请输入2-4位中文姓名");
return false;
}
}
return true;
};
@@ -409,11 +548,19 @@ const confirmWithdraw = async () => {
isSubmitting.value = true;
try {
const { applyWithdrawal } = await import('@/api/agent');
const { data, error } = await applyWithdrawal({
payee_account: alipayAccount.value,
const payload = {
withdrawal_type: withdrawalType.value,
amount: Number(amount.value),
payee_name: realName.value,
});
payee_name: withdrawalType.value === 1 ? realName.value : bankPayeeName.value,
};
if (withdrawalType.value === 1) {
payload.payee_account = alipayAccount.value;
} else {
payload.payee_account = bankCardNo.value.replace(/\s/g, "");
payload.bank_card_no = bankCardNo.value.replace(/\s/g, "");
payload.bank_name = bankName.value.trim();
}
const { data, error } = await applyWithdrawal(payload);
if (data.value?.code === 200 && !error.value) {
status.value = 1; // 新系统申请后状态为1待审核
showTaxConfirmPopup.value = false;
@@ -450,6 +597,9 @@ const resetForm = () => {
alipayAccount.value = "";
amount.value = "";
realName.value = "";
bankCardNo.value = "";
bankName.value = "";
bankPayeeName.value = "";
};
</script>
@@ -467,6 +617,78 @@ const resetForm = () => {
@apply opacity-60 cursor-not-allowed;
}
/* 提现方式 Tab 样式 */
.withdraw-tabs {
display: flex;
}
.tab-container {
position: relative;
display: flex;
width: 100%;
background: rgba(0, 0, 0, 0.04);
border-radius: 10px;
padding: 3px;
gap: 3px;
}
.tab-slider {
position: absolute;
top: 3px;
left: 3px;
width: calc(50% - 4.5px);
height: calc(100% - 6px);
background: #fff;
border-radius: 8px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
transition: transform 0.2s ease;
z-index: 0;
}
.tab-slider--right {
transform: translateX(calc(100% + 3px));
}
.tab-item {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
flex: 1;
padding: 7px 16px;
border-radius: 8px;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
user-select: none;
}
.tab-item--active .tab-label {
color: var(--van-theme-primary);
font-weight: 600;
}
.tab-item:not(.tab-item--active) .tab-label {
color: var(--van-text-color-2);
}
.tab-item:not(.tab-item--active) .tab-vant-icon {
color: var(--van-text-color-3);
}
.tab-item--active .tab-vant-icon {
color: var(--van-theme-primary);
}
.tab-label {
font-size: 14px;
}
.tab-vant-icon {
font-size: 16px;
}
/* 弹窗入场动画 */
.van-popup {
transition: transform 0.4s cubic-bezier(0.22, 0.61, 0.36, 1),

View File

@@ -1,7 +1,7 @@
<template>
<div class="min-h-screen bg-gray-50">
<!-- 提现记录列表 -->
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
<van-list v-model:loading="loading" :finished="finished" :finished-text="data.list.length > 0 ? '没有更多了' : ''" @load="onLoad">
<div v-for="(item, index) in data.list" :key="index" class="mx-4 my-2 bg-white rounded-lg p-4 shadow-sm">
<div class="flex justify-between items-center mb-2">
<span class="text-gray-500 text-sm">{{
@@ -36,11 +36,15 @@
</div>
</div>
</van-list>
<!-- 空状态 -->
<EmptyState v-if="!loading && !data.list.length" text="暂无提现记录" />
</div>
</template>
<script setup>
import { getWithdrawalList } from '@/api/agent'
import EmptyState from '@/components/EmptyState.vue'
// 新系统状态映射配置1=待审核2=审核通过3=审核拒绝4=提现中5=提现成功6=提现失败
const statusConfig = {
@@ -140,15 +144,11 @@ const getAmountColor = (status) => {
// 加载更多数据
const onLoad = async () => {
if (!finished.value) {
await getData();
}
await getData();
};
// 获取数据
const getData = async () => {
if (loading.value || finished.value) return
try {
loading.value = true;
const { data: res, error } = await getWithdrawalList({
@@ -157,45 +157,35 @@ const getData = async () => {
});
if (res.value?.code === 200 && !error.value) {
// 首次加载
if (page.value === 1) {
data.value = res.value.data;
} else {
// 分页加载
data.value.list.push(...res.value.data.list);
}
const list = res.value.data.list || []
const total = res.value.data.total || 0
// 更新分页状态
page.value++;
if (page.value === 1) {
data.value = { total, list }
} else {
data.value.list.push(...list)
}
// 判断是否加载完成
if (
data.value.list.length >= res.value.data.total ||
res.value.data.list.length < pageSize.value
data.value.list.length >= total ||
list.length < pageSize.value
) {
finished.value = true;
} else {
page.value++;
}
} else {
// 接口返回错误或请求失败,停止翻页
finished.value = true;
console.error('获取提现列表失败:', res.value?.msg || error.value || '未知错误');
}
} catch (err) {
// 捕获异常,停止翻页
finished.value = true;
console.error('获取提现列表失败:', err);
} finally {
loading.value = false;
}
};
// 初始化加载
onMounted(async () => {
// 重置分页状态
page.value = 1;
finished.value = false;
await getData();
});
</script>
<style scoped>

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