Compare commits
20 Commits
c85b46c18e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| df78375d0a | |||
| 14d282b366 | |||
| be1b1faf86 | |||
| 24b068574c | |||
| c7d23975b3 | |||
| e37c4e446f | |||
| 777f2e2a7b | |||
| 14d7131447 | |||
| 1652ee1e55 | |||
| 030ed260c1 | |||
| 576ab0c362 | |||
| 3e2d828f4d | |||
| f48052ff10 | |||
| 3eb3636b22 | |||
| 9e0b38a6d5 | |||
| 0e210505f5 | |||
| db4f287bf5 | |||
| c96def406e | |||
| bceca129be | |||
| 430b8f12ba |
13
.env
@@ -1,16 +1,23 @@
|
|||||||
VITE_API_URL=
|
VITE_API_URL=
|
||||||
VITE_API_PREFIX=/api/v1
|
VITE_API_PREFIX=/api/v1
|
||||||
|
|
||||||
VITE_COMPANY_NAME=海南省学宇思网络科技有限公司
|
VITE_COMPANY_NAME=海南海宇大数据有限公司
|
||||||
|
|
||||||
VITE_INQUIRE_AES_KEY=ff83609b2b24fc73196aac3d3dfb874f
|
VITE_INQUIRE_AES_KEY=ff83609b2b24fc73196aac3d3dfb874f
|
||||||
|
|
||||||
VITE_WECHAT_APP_ID=wx442ee1ac1ee75917
|
VITE_WECHAT_APP_ID=wx442ee1ac1ee75917
|
||||||
|
|
||||||
|
# 阿里云滑块验证码配置
|
||||||
|
# 从阿里云验证码控制台获取 SceneId
|
||||||
|
VITE_ALIYUN_CAPTCHA_SCENE_ID=wynt39to
|
||||||
|
# 是否启用加密模式(true/false),需要在阿里云控制台开启加密模式
|
||||||
|
# 注意:根据代码逻辑,设置为 true 表示禁用加密,设置为 false 表示启用加密
|
||||||
|
VITE_ALIYUN_CAPTCHA_ENCRYPTED=true
|
||||||
|
|
||||||
VITE_CHAT_AES_KEY=qw5w6SFE2D1jmxyd
|
VITE_CHAT_AES_KEY=qw5w6SFE2D1jmxyd
|
||||||
VITE_CHAT_AES_IV=345GDFED433223DF
|
VITE_CHAT_AES_IV=345GDFED433223DF
|
||||||
|
|
||||||
VITE_SHARE_TITLE=一查查|大数据风险报告查询与代理平台,支持个人和企业多场景风控应用
|
VITE_SHARE_TITLE=一查查|大数据风险报告查询与代理平台,支持个人和企业多场景风控应用
|
||||||
VITE_SHARE_DESC=提供个人信用评估、入职背调、信贷风控、企业风险监测等服务
|
VITE_SHARE_DESC=提供个人风险评估、入职背调、信贷风控、企业风险监测等服务
|
||||||
VITE_SHARE_IMG=https://www.tianyuandb.com/logo.png
|
VITE_SHARE_IMG=https://www.onecha.cn/logo.png
|
||||||
|
VITE_TOKEN_VERSION=1.5
|
||||||
112
index.html
@@ -3,22 +3,36 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
<link
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
rel="apple-touch-icon"
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
sizes="180x180"
|
||||||
<link rel="manifest" href="/site.webmanifest">
|
href="/apple-touch-icon.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="32x32"
|
||||||
|
href="/favicon-32x32.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="16x16"
|
||||||
|
href="/favicon-16x16.png"
|
||||||
|
/>
|
||||||
|
<link rel="manifest" href="/site.webmanifest" />
|
||||||
<meta
|
<meta
|
||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1, maximum-scale=3, user-scalable=no"
|
content="width=device-width, initial-scale=1, maximum-scale=3, user-scalable=no"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 基础SEO信息 -->
|
<!-- 基础SEO信息 -->
|
||||||
<title>
|
<title>
|
||||||
一查查|大数据风险报告查询与代理平台,支持个人和企业多场景风控应用
|
一查查|大数据风险报告查询与代理平台,支持个人和企业多场景风控应用
|
||||||
</title>
|
</title>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="一查查,专业大数据风险报告查询与代理平台,支持个人信用查询、小微企业风控、贷前风险背调等多场景报告应用,免费开通代理权限,助力高效识别信用与风险。"
|
content="一查查,专业大数据风险报告查询与代理平台,支持个人风险查询、小微企业风控、贷前风险背调等多场景报告应用,免费开通代理权限,助力高效识别信用与风险。"
|
||||||
/>
|
/>
|
||||||
<meta
|
<meta
|
||||||
name="keywords"
|
name="keywords"
|
||||||
@@ -28,21 +42,33 @@
|
|||||||
<meta name="robots" content="index, follow" />
|
<meta name="robots" content="index, follow" />
|
||||||
<meta name="googlebot" content="index, follow" />
|
<meta name="googlebot" content="index, follow" />
|
||||||
<meta name="baidu-site-verification" content="" />
|
<meta name="baidu-site-verification" content="" />
|
||||||
|
|
||||||
<!-- Open Graph / Facebook -->
|
<!-- Open Graph / Facebook -->
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:url" content="https://www.zhinengcha.cn/" />
|
<meta property="og:url" content="https://www.zhinengcha.cn/" />
|
||||||
<meta property="og:title" content="一查查|大数据风险报告查询与代理平台,支持个人和企业多场景风控应用" />
|
<meta
|
||||||
<meta property="og:description" content="一查查,专业大数据风险报告查询与代理平台,支持个人信用查询、小微企业风控、贷前风险背调等多场景报告应用,免费开通代理权限,助力高效识别信用与风险。" />
|
property="og:title"
|
||||||
|
content="一查查|大数据风险报告查询与代理平台,支持个人和企业多场景风控应用"
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
property="og:description"
|
||||||
|
content="一查查,专业大数据风险报告查询与代理平台,支持个人风险查询、小微企业风控、贷前风险背调等多场景报告应用,免费开通代理权限,助力高效识别信用与风险。"
|
||||||
|
/>
|
||||||
<meta property="og:site_name" content="一查查" />
|
<meta property="og:site_name" content="一查查" />
|
||||||
<meta property="og:locale" content="zh_CN" />
|
<meta property="og:locale" content="zh_CN" />
|
||||||
|
|
||||||
<!-- Twitter -->
|
<!-- Twitter -->
|
||||||
<meta property="twitter:card" content="summary" />
|
<meta property="twitter:card" content="summary" />
|
||||||
<meta property="twitter:url" content="https://www.zhinengcha.cn/" />
|
<meta property="twitter:url" content="https://www.zhinengcha.cn/" />
|
||||||
<meta property="twitter:title" content="一查查|大数据风险报告查询与代理平台,支持个人和企业多场景风控应用" />
|
<meta
|
||||||
<meta property="twitter:description" content="一查查,专业大数据风险报告查询与代理平台,支持个人信用查询、小微企业风控、贷前风险背调等多场景报告应用,免费开通代理权限,助力高效识别信用与风险。" />
|
property="twitter:title"
|
||||||
|
content="一查查|大数据风险报告查询与代理平台,支持个人和企业多场景风控应用"
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
property="twitter:description"
|
||||||
|
content="一查查,专业大数据风险报告查询与代理平台,支持个人风险查询、小微企业风控、贷前风险背调等多场景报告应用,免费开通代理权限,助力高效识别信用与风险。"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 其他重要meta标签 -->
|
<!-- 其他重要meta标签 -->
|
||||||
<meta name="theme-color" content="#3498db" />
|
<meta name="theme-color" content="#3498db" />
|
||||||
<meta name="msapplication-TileColor" content="#3498db" />
|
<meta name="msapplication-TileColor" content="#3498db" />
|
||||||
@@ -50,31 +76,31 @@
|
|||||||
<meta name="apple-mobile-web-app-title" content="一查查" />
|
<meta name="apple-mobile-web-app-title" content="一查查" />
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||||
|
|
||||||
<!-- 结构化数据 -->
|
<!-- 结构化数据 -->
|
||||||
<script type="application/ld+json">
|
<script type="application/ld+json">
|
||||||
{
|
{
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "WebSite",
|
"@type": "WebSite",
|
||||||
"name": "一查查",
|
"name": "一查查",
|
||||||
"url": "https://www.zhinengcha.cn/",
|
"url": "https://www.zhinengcha.cn/",
|
||||||
"description": "专业大数据风险报告查询与代理平台,支持个人信用查询、小微企业风控、贷前风险背调等多场景报告应用",
|
"description": "专业大数据风险报告查询与代理平台,支持个人风险查询、小微企业风控、贷前风险背调等多场景报告应用",
|
||||||
"potentialAction": {
|
"potentialAction": {
|
||||||
"@type": "SearchAction",
|
"@type": "SearchAction",
|
||||||
"target": "https://www.zhinengcha.cn/search?q={search_term_string}",
|
"target": "https://www.zhinengcha.cn/search?q={search_term_string}",
|
||||||
"query-input": "required name=search_term_string"
|
"query-input": "required name=search_term_string"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script type="application/ld+json">
|
<script type="application/ld+json">
|
||||||
{
|
{
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "Organization",
|
"@type": "Organization",
|
||||||
"name": "一查查",
|
"name": "一查查",
|
||||||
"url": "https://www.zhinengcha.cn/",
|
"url": "https://www.zhinengcha.cn/",
|
||||||
"description": "专业大数据风险报告查询与代理平台,支持个人和企业多场景风控应用"
|
"description": "专业大数据风险报告查询与代理平台,支持个人和企业多场景风控应用"
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
|
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
|
||||||
@@ -82,13 +108,19 @@
|
|||||||
window.jWeixin = window.wx;
|
window.jWeixin = window.wx;
|
||||||
delete window.wx;
|
delete window.wx;
|
||||||
</script>
|
</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.zhinengcha.cn" />
|
||||||
<link rel="preconnect" href="https://res.wx.qq.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.zhinengcha.cn" />
|
||||||
<link rel="dns-prefetch" href="https://res.wx.qq.com">
|
<link rel="dns-prefetch" href="https://res.wx.qq.com" />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* 基础样式 */
|
/* 基础样式 */
|
||||||
html {
|
html {
|
||||||
@@ -181,6 +213,8 @@
|
|||||||
<div class="loading-text">加载中</div>
|
<div class="loading-text">加载中</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
<!-- 阿里云验证码容器 -->
|
||||||
|
<div id="captcha-element"></div>
|
||||||
|
|
||||||
<script type="module" src="/src/main.js"></script>
|
<script type="module" src="/src/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
1
lEbH141J7d.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
f07fc7c629885231180fb79885dba876
|
||||||
BIN
public/image/poster/backgroundcheck/backgroundcheck_01.jpg
Normal file
|
After Width: | Height: | Size: 179 KiB |
BIN
public/image/poster/backgroundcheck/backgroundcheck_02.jpg
Normal file
|
After Width: | Height: | Size: 181 KiB |
BIN
public/image/poster/backgroundcheck/backgroundcheck_03.jpg
Normal file
|
After Width: | Height: | Size: 210 KiB |
BIN
public/image/poster/backgroundcheck/backgroundcheck_04.jpg
Normal file
|
After Width: | Height: | Size: 163 KiB |
BIN
public/image/poster/companyinfo/companyinfo_01.jpg
Normal file
|
After Width: | Height: | Size: 234 KiB |
BIN
public/image/poster/companyinfo/companyinfo_02.jpg
Normal file
|
After Width: | Height: | Size: 241 KiB |
BIN
public/image/poster/companyinfo/companyinfo_03.jpg
Normal file
|
After Width: | Height: | Size: 203 KiB |
BIN
public/image/poster/companyinfo/companyinfo_04.jpg
Normal file
|
After Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 218 KiB |
|
After Width: | Height: | Size: 209 KiB |
|
After Width: | Height: | Size: 192 KiB |
|
After Width: | Height: | Size: 231 KiB |
BIN
public/image/poster/homeservice/homeservice_01.jpg
Normal file
|
After Width: | Height: | Size: 247 KiB |
BIN
public/image/poster/homeservice/homeservice_02.jpg
Normal file
|
After Width: | Height: | Size: 226 KiB |
BIN
public/image/poster/homeservice/homeservice_03.jpg
Normal file
|
After Width: | Height: | Size: 176 KiB |
BIN
public/image/poster/homeservice/homeservice_04.jpg
Normal file
|
After Width: | Height: | Size: 193 KiB |
BIN
public/image/poster/invitation/invitation_01.jpg
Normal file
|
After Width: | Height: | Size: 319 KiB |
BIN
public/image/poster/invitation/invitation_02.jpg
Normal file
|
After Width: | Height: | Size: 252 KiB |
BIN
public/image/poster/invitation/invitation_03.jpg
Normal file
|
After Width: | Height: | Size: 304 KiB |
BIN
public/image/poster/invitation/invitation_04.jpg
Normal file
|
After Width: | Height: | Size: 287 KiB |
BIN
public/image/poster/marriage/marriage_01.jpg
Normal file
|
After Width: | Height: | Size: 219 KiB |
BIN
public/image/poster/marriage/marriage_02.jpg
Normal file
|
After Width: | Height: | Size: 205 KiB |
BIN
public/image/poster/marriage/marriage_03.jpg
Normal file
|
After Width: | Height: | Size: 201 KiB |
BIN
public/image/poster/marriage/marriage_04.jpg
Normal file
|
After Width: | Height: | Size: 182 KiB |
BIN
public/image/poster/riskassessment/riskassessment_01.jpg
Normal file
|
After Width: | Height: | Size: 250 KiB |
BIN
public/image/poster/riskassessment/riskassessment_02.jpg
Normal file
|
After Width: | Height: | Size: 280 KiB |
BIN
public/image/poster/riskassessment/riskassessment_03.jpg
Normal file
|
After Width: | Height: | Size: 246 KiB |
BIN
public/image/poster/riskassessment/riskassessment_04.jpg
Normal file
|
After Width: | Height: | Size: 249 KiB |
BIN
public/logo.jpg
|
Before Width: | Height: | Size: 124 KiB |
BIN
public/logo.png
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 1.5 MiB |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "一查查|大数据风险报告查询与代理平台,支持个人和企业多场景风控应用",
|
"name": "一查查|大数据风险报告查询与代理平台,支持个人和企业多场景风控应用",
|
||||||
"short_name": "一查查",
|
"short_name": "一查查",
|
||||||
"description": "专业大数据风险报告查询与代理平台,支持个人信用查询、小微企业风控、贷前风险背调等多场景报告应用",
|
"description": "专业大数据风险报告查询与代理平台,支持个人风险查询、小微企业风控、贷前风险背调等多场景报告应用",
|
||||||
"start_url": "/",
|
"start_url": "/",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"background_color": "#ffffff",
|
"background_color": "#ffffff",
|
||||||
@@ -34,7 +34,11 @@
|
|||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"categories": ["business", "finance", "utilities"],
|
"categories": [
|
||||||
|
"business",
|
||||||
|
"finance",
|
||||||
|
"utilities"
|
||||||
|
],
|
||||||
"lang": "zh-CN",
|
"lang": "zh-CN",
|
||||||
"dir": "ltr"
|
"dir": "ltr"
|
||||||
}
|
}
|
||||||
429
src/App.vue
@@ -1,211 +1,322 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { RouterLink, RouterView, useRouter } from "vue-router";
|
import { RouterView, useRouter, useRoute } from "vue-router";
|
||||||
const { isWeChat } = useEnv();
|
const { isWeChat } = useEnv();
|
||||||
import { useAgentStore } from "@/stores/agentStore";
|
import { useAgentStore } from "@/stores/agentStore";
|
||||||
import { useUserStore } from "@/stores/userStore";
|
import { useUserStore } from "@/stores/userStore";
|
||||||
import { useDialogStore } from "@/stores/dialogStore";
|
import { useDialogStore } from "@/stores/dialogStore";
|
||||||
import { useAuthStore } from "@/stores/authStore";
|
import { useAuthStore } from "@/stores/authStore";
|
||||||
|
import { useAppStore } from "@/stores/appStore";
|
||||||
import { useWeixinShare } from "@/composables/useWeixinShare";
|
import { useWeixinShare } from "@/composables/useWeixinShare";
|
||||||
|
import BindPhoneDialog from "@/components/BindPhoneDialog.vue";
|
||||||
|
import BindPhoneOnlyDialog from "@/components/BindPhoneOnlyDialog.vue";
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
const agentStore = useAgentStore();
|
const agentStore = useAgentStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const dialogStore = useDialogStore();
|
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
const { configWeixinShare, setDynamicShare } = useWeixinShare();
|
const appStore = useAppStore();
|
||||||
|
const { setDynamicShare } = useWeixinShare();
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
// 检查token版本,如果版本不匹配则清除旧token
|
// 初始化应用配置
|
||||||
checkTokenVersion()
|
await appStore.fetchAppConfig();
|
||||||
|
|
||||||
// 恢复微信授权状态(页面刷新后)
|
// 恢复微信授权状态(页面刷新或回调时)
|
||||||
authStore.restoreFromStorage();
|
authStore.restoreFromStorage();
|
||||||
|
|
||||||
// 检查是否是微信授权回调
|
// 检查是否是微信授权回调
|
||||||
const url = new URL(window.location.href);
|
const url = new URL(window.location.href);
|
||||||
const hasWeixinCode = url.searchParams.has('code') && url.searchParams.has('state');
|
const code = url.searchParams.get("code");
|
||||||
|
const state = url.searchParams.get("state");
|
||||||
|
|
||||||
if (hasWeixinCode) {
|
if (code && state) {
|
||||||
// 如果是授权回调,标记为正在授权
|
// 这是微信授权回调,处理授权结果
|
||||||
authStore.startWeixinAuth();
|
console.log("Handling WeChat auth callback");
|
||||||
|
await handleWeixinAuthCallback(code);
|
||||||
|
} else {
|
||||||
|
// 正常初始化:加载用户信息等
|
||||||
|
await initializeApp();
|
||||||
}
|
}
|
||||||
|
getWeixinAuthUrl();
|
||||||
|
|
||||||
RefreshToken();
|
// 延迟配置微信分享
|
||||||
const token = localStorage.getItem("token");
|
|
||||||
if (token) {
|
|
||||||
agentStore.fetchAgentStatus();
|
|
||||||
userStore.fetchUserInfo();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 配置微信分享
|
|
||||||
// 延迟执行,确保微信SDK已加载
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
if (isWeChat.value && window.jWeixin) {
|
if (isWeChat.value && window.jWeixin) {
|
||||||
await setDynamicShare();
|
await setDynamicShare();
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 1000);
|
||||||
|
|
||||||
// 监听路由变化,更新微信分享配置
|
// 监听路由变化更新分享配置
|
||||||
router.afterEach(() => {
|
router.afterEach(async () => {
|
||||||
setTimeout(async () => {
|
if (isWeChat.value && window.jWeixin && !authStore.isWeixinAuthing) {
|
||||||
if (isWeChat.value && window.jWeixin) {
|
await setDynamicShare();
|
||||||
await setDynamicShare();
|
}
|
||||||
}
|
|
||||||
}, 300);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const checkTokenVersion = () => {
|
/**
|
||||||
// 设置新的token版本号(当后端token格式改变时,修改这个版本号)
|
* 处理微信授权回调
|
||||||
const CURRENT_TOKEN_VERSION = '2.1'
|
*/
|
||||||
const storedTokenVersion = localStorage.getItem('tokenVersion')
|
const handleWeixinAuthCallback = async (code) => {
|
||||||
|
try {
|
||||||
|
console.log("🔄 WeChat auth callback: code=", code);
|
||||||
|
|
||||||
if (!storedTokenVersion || storedTokenVersion !== CURRENT_TOKEN_VERSION) {
|
// 调用后端接口交换 token
|
||||||
// 清除所有旧的认证信息
|
|
||||||
clearAuthData()
|
|
||||||
|
|
||||||
// 设置新的token版本
|
|
||||||
localStorage.setItem('tokenVersion', CURRENT_TOKEN_VERSION)
|
|
||||||
|
|
||||||
console.log('Token version updated, cleared old authentication data')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 统一清除认证数据的工具函数
|
|
||||||
const clearAuthData = () => {
|
|
||||||
localStorage.removeItem('token')
|
|
||||||
localStorage.removeItem('refreshAfter')
|
|
||||||
localStorage.removeItem('accessExpire')
|
|
||||||
localStorage.removeItem('userInfo')
|
|
||||||
localStorage.removeItem('agentInfo')
|
|
||||||
}
|
|
||||||
|
|
||||||
const RefreshToken = async () => {
|
|
||||||
if (isWeChat.value) {
|
|
||||||
h5WeixinLogin();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const token = localStorage.getItem("token");
|
|
||||||
const refreshAfter = localStorage.getItem("refreshAfter");
|
|
||||||
const accessExpire = localStorage.getItem("accessExpire");
|
|
||||||
const currentTime = new Date().getTime();
|
|
||||||
|
|
||||||
if (accessExpire) {
|
|
||||||
const accessExpireInMilliseconds = parseInt(accessExpire) * 1000; // 转换为毫秒级
|
|
||||||
if (currentTime > accessExpireInMilliseconds) {
|
|
||||||
if (isWeChat.value) {
|
|
||||||
h5WeixinLogin();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 1. 如果没有 token,直接返回
|
|
||||||
if (!token) {
|
|
||||||
if (isWeChat.value) {
|
|
||||||
h5WeixinLogin();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 如果有 refreshAfter,检查当前时间是否超过 refreshAfter(refreshAfter 是秒级,需要转换为毫秒级)
|
|
||||||
if (refreshAfter) {
|
|
||||||
const refreshAfterInMilliseconds = parseInt(refreshAfter) * 1000; // 转换为毫秒级
|
|
||||||
if (currentTime < refreshAfterInMilliseconds) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 3. 如果没有 refreshAfter 或者时间超过 refreshAfter,执行刷新 token 的请求
|
|
||||||
refreshToken();
|
|
||||||
};
|
|
||||||
|
|
||||||
const mpWeixinLogin = () => { };
|
|
||||||
|
|
||||||
const refreshToken = async () => {
|
|
||||||
const { data, error } = await useApiFetch("/user/getToken").post().json();
|
|
||||||
|
|
||||||
if (data.value && !error.value) {
|
|
||||||
if (data.value.code === 200) {
|
|
||||||
localStorage.setItem("token", data.value.data.accessToken);
|
|
||||||
localStorage.setItem("refreshAfter", data.value.data.refreshAfter);
|
|
||||||
localStorage.setItem("accessExpire", data.value.data.accessExpire);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const h5WeixinLogin = async () => {
|
|
||||||
// 获取当前URL
|
|
||||||
const url = new URL(window.location.href);
|
|
||||||
// 获取参数
|
|
||||||
const params = new URLSearchParams(url.search);
|
|
||||||
// 获取特定参数值
|
|
||||||
const code = params.get("code");
|
|
||||||
const state = params.get("state");
|
|
||||||
|
|
||||||
if (code && state) {
|
|
||||||
// 这是微信授权回调,处理授权结果
|
|
||||||
const { data, error } = await useApiFetch("/user/wxh5Auth")
|
const { data, error } = await useApiFetch("/user/wxh5Auth")
|
||||||
.post({ code })
|
.post({ code })
|
||||||
.json();
|
.json();
|
||||||
|
|
||||||
if (data.value && !error.value) {
|
console.log("📡 wxh5Auth response:", {
|
||||||
if (data.value.code === 200) {
|
code: data.value?.code,
|
||||||
localStorage.setItem("token", data.value.data.accessToken);
|
hasToken: !!data.value?.data?.accessToken,
|
||||||
localStorage.setItem(
|
error: error.value
|
||||||
"refreshAfter",
|
});
|
||||||
data.value.data.refreshAfter
|
|
||||||
);
|
|
||||||
localStorage.setItem(
|
|
||||||
"accessExpire",
|
|
||||||
data.value.data.accessExpire
|
|
||||||
);
|
|
||||||
|
|
||||||
params.delete("code");
|
if (data.value && !error.value && data.value.code === 200) {
|
||||||
params.delete("state");
|
// 保存 token
|
||||||
|
const token = data.value.data.accessToken;
|
||||||
|
localStorage.setItem("token", token);
|
||||||
|
localStorage.setItem("refreshAfter", data.value.data.refreshAfter);
|
||||||
|
localStorage.setItem("accessExpire", data.value.data.accessExpire);
|
||||||
|
// ⚠️ 重要:保存 token 后立即设置 tokenVersion,防止被 checkTokenVersion 清除
|
||||||
|
const tokenVersion = import.meta.env.VITE_TOKEN_VERSION || "1.1";
|
||||||
|
localStorage.setItem("tokenVersion", tokenVersion);
|
||||||
|
|
||||||
// 更新 URL(不刷新页面)
|
console.log("✅ Token saved successfully, token:", token.substring(0, 20) + "...");
|
||||||
const newUrl = `${url.origin}${url.pathname
|
console.log("✅ Token saved to localStorage, userId:", data.value.data.userId || "unknown");
|
||||||
}?${params.toString()}`;
|
console.log(`✅ TokenVersion set to ${tokenVersion}`);
|
||||||
window.history.replaceState({}, "", newUrl);
|
|
||||||
|
|
||||||
// 标记微信授权完成
|
// 验证 token 是否真的保存成功
|
||||||
authStore.completeWeixinAuth();
|
const savedToken = localStorage.getItem("token");
|
||||||
|
if (savedToken === token) {
|
||||||
|
console.log("✅ Token verification: localStorage中的token与保存的token一致");
|
||||||
|
} else {
|
||||||
|
console.error("❌ Token verification failed: localStorage中的token与保存的token不一致");
|
||||||
|
}
|
||||||
|
|
||||||
// 获取用户和代理信息
|
// 清除 URL 中的 code 和 state 参数
|
||||||
await Promise.all([
|
const url = new URL(window.location.href);
|
||||||
agentStore.fetchAgentStatus(),
|
const params = new URLSearchParams(url.search);
|
||||||
userStore.fetchUserInfo()
|
params.delete("code");
|
||||||
]);
|
params.delete("state");
|
||||||
|
const newUrl = `${url.origin}${url.pathname}${params.toString() ? "?" + params.toString() : ""}`;
|
||||||
|
window.history.replaceState({}, "", newUrl);
|
||||||
|
console.log("✅ URL cleaned, removed code and state parameters");
|
||||||
|
|
||||||
// 如果有待处理的路由,跳转到该路由
|
// 获取用户信息
|
||||||
if (authStore.pendingRoute) {
|
console.log("👤 Fetching user info...");
|
||||||
router.replace(authStore.pendingRoute);
|
try {
|
||||||
authStore.clearPendingRoute();
|
await userStore.fetchUserInfo();
|
||||||
|
console.log("✅ User info fetched:", {
|
||||||
|
mobile: userStore.mobile,
|
||||||
|
isLoggedIn: userStore.isLoggedIn
|
||||||
|
});
|
||||||
|
} catch (userInfoErr) {
|
||||||
|
console.error("❌ Failed to fetch user info:", userInfoErr);
|
||||||
|
// 用户信息获取失败,清除 token,跳转到登录
|
||||||
|
authStore.resetAuthState();
|
||||||
|
await router.replace("/login");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取代理信息(如果是有手机号的用户)
|
||||||
|
if (userStore.mobile) {
|
||||||
|
try {
|
||||||
|
await agentStore.fetchAgentStatus();
|
||||||
|
} catch (agentErr) {
|
||||||
|
console.warn("Warning: Failed to fetch agent status:", agentErr);
|
||||||
|
// 不中断流程,只是警告
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 标记授权完成
|
||||||
|
authStore.completeWeixinAuth();
|
||||||
|
console.log("✅ WeChat auth marked as complete");
|
||||||
|
|
||||||
|
// 获取 pendingRoute 并跳转
|
||||||
|
const pendingRoute = authStore.pendingRoute
|
||||||
|
console.log("🎯 pendingRoute:", pendingRoute);
|
||||||
|
|
||||||
|
// if (pendingRoute) {
|
||||||
|
// // ⚠️ 重要:必须先跳转再清除,否则清除后 pendingRoute 为 null
|
||||||
|
// console.log("🚀 Navigating to pendingRoute:", pendingRoute);
|
||||||
|
// await router.replace(pendingRoute);
|
||||||
|
// authStore.clearPendingRoute();
|
||||||
|
// console.log("✅ Navigated to pendingRoute and cleared it");
|
||||||
|
// } else {
|
||||||
|
// // 默认跳转到首页
|
||||||
|
// console.log("📍 No pendingRoute found, navigating to home");
|
||||||
|
// await router.replace("/");
|
||||||
|
// }
|
||||||
|
} else {
|
||||||
|
console.error("❌ WeChat auth failed:", {
|
||||||
|
code: data.value?.code,
|
||||||
|
message: data.value?.msg || data.value?.message,
|
||||||
|
error: error.value
|
||||||
|
});
|
||||||
|
|
||||||
|
// 授权失败,重置状态
|
||||||
|
authStore.resetAuthState();
|
||||||
|
// 跳转到登录页
|
||||||
|
await router.replace("/login");
|
||||||
}
|
}
|
||||||
} else {
|
} catch (err) {
|
||||||
// 没有授权参数,需要开始微信授权
|
console.error("❌ Error handling WeChat auth callback:", err);
|
||||||
// 保存当前路由作为授权完成后的目标路由
|
authStore.resetAuthState();
|
||||||
const currentRoute = router.currentRoute.value;
|
await router.replace("/login");
|
||||||
authStore.startWeixinAuth(currentRoute);
|
|
||||||
h5WeixinGetCode();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const h5WeixinGetCode = () => {
|
|
||||||
const currentUrl = window.location.href;
|
/**
|
||||||
let redirectUri = encodeURIComponent(currentUrl);
|
* 初始化应用:检查 token,加载用户信息
|
||||||
let appId = import.meta.env.VITE_WECHAT_APP_ID;
|
*/
|
||||||
let state = "snsapi_base";
|
const initializeApp = async () => {
|
||||||
let scope = "snsapi_base";
|
// 检查 token 版本
|
||||||
let authUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}&redirect_uri=${redirectUri}&response_type=code&scope=${scope}&state=${state}#wechat_redirect`;
|
checkTokenVersion();
|
||||||
// 跳转到授权URL
|
|
||||||
window.location.href = authUrl;
|
// 尝试刷新 token
|
||||||
|
await refreshTokenIfNeeded();
|
||||||
|
|
||||||
|
// 如果有 token,加载用户信息
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
if (token) {
|
||||||
|
try {
|
||||||
|
await userStore.fetchUserInfo();
|
||||||
|
if (userStore.mobile) {
|
||||||
|
await agentStore.fetchAgentStatus();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error loading user info:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查 token 版本,清除不兼容的旧 token
|
||||||
|
* ⚠️ 注意:只有在确实需要清除旧 token 时才清除,避免误清除新保存的 token
|
||||||
|
*/
|
||||||
|
const checkTokenVersion = () => {
|
||||||
|
const CURRENT_TOKEN_VERSION = import.meta.env.VITE_TOKEN_VERSION || "1.1";
|
||||||
|
const storedVersion = localStorage.getItem("tokenVersion");
|
||||||
|
const hasToken = !!localStorage.getItem("token");
|
||||||
|
|
||||||
|
// 如果 tokenVersion 不存在或版本不匹配
|
||||||
|
if (!storedVersion || storedVersion !== CURRENT_TOKEN_VERSION) {
|
||||||
|
// 只有在有 token 的情况下才清除(避免清除刚保存的新 token)
|
||||||
|
if (hasToken) {
|
||||||
|
console.log(`Token version mismatch: storedVersion=${storedVersion}, currentVersion=${CURRENT_TOKEN_VERSION}, clearing old auth data`);
|
||||||
|
// Token 版本不匹配,清除旧数据
|
||||||
|
localStorage.removeItem("token");
|
||||||
|
localStorage.removeItem("refreshAfter");
|
||||||
|
localStorage.removeItem("accessExpire");
|
||||||
|
localStorage.removeItem("userInfo");
|
||||||
|
localStorage.removeItem("agentInfo");
|
||||||
|
}
|
||||||
|
// 无论是否有 token,都设置新的 tokenVersion
|
||||||
|
localStorage.setItem("tokenVersion", CURRENT_TOKEN_VERSION);
|
||||||
|
} else {
|
||||||
|
console.log(`Token version check passed: storedVersion=${storedVersion}, currentVersion=${CURRENT_TOKEN_VERSION}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在需要时刷新 token
|
||||||
|
*/
|
||||||
|
const refreshTokenIfNeeded = async () => {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
if (!token) return;
|
||||||
|
|
||||||
|
const accessExpire = localStorage.getItem("accessExpire");
|
||||||
|
const refreshAfter = localStorage.getItem("refreshAfter");
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// 检查 token 是否已过期
|
||||||
|
if (accessExpire) {
|
||||||
|
const expireTime = parseInt(accessExpire) * 1000;
|
||||||
|
if (now > expireTime) {
|
||||||
|
console.log("Token expired");
|
||||||
|
return; // Token 已过期,不刷新,由路由守卫处理
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否需要刷新
|
||||||
|
if (refreshAfter) {
|
||||||
|
const refreshTime = parseInt(refreshAfter) * 1000;
|
||||||
|
if (now < refreshTime) {
|
||||||
|
return; // 还不需要刷新
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行 token 刷新
|
||||||
|
try {
|
||||||
|
const { data, error } = await useApiFetch("/user/getToken").post().json();
|
||||||
|
if (data.value && !error.value && data.value.code === 200) {
|
||||||
|
localStorage.setItem("token", data.value.data.accessToken);
|
||||||
|
localStorage.setItem("refreshAfter", data.value.data.refreshAfter);
|
||||||
|
localStorage.setItem("accessExpire", data.value.data.accessExpire);
|
||||||
|
console.log("Token refreshed successfully");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error refreshing token:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发起微信授权 URL
|
||||||
|
* 这个逻辑已经在路由守卫中实现了
|
||||||
|
* 这里保留这个函数的备份,以防需要其他地方调用
|
||||||
|
*/
|
||||||
|
const getWeixinAuthUrl = () => {
|
||||||
|
const isAuthenticated = localStorage.getItem("token");
|
||||||
|
|
||||||
|
// 检查 token 是否过期
|
||||||
|
const accessExpire = localStorage.getItem("accessExpire");
|
||||||
|
const now = Date.now();
|
||||||
|
let isTokenExpired = false;
|
||||||
|
if (accessExpire) {
|
||||||
|
isTokenExpired = now > parseInt(accessExpire) * 1000;
|
||||||
|
}
|
||||||
|
console.log("WeChat auth check:", {
|
||||||
|
isWeChat: isWeChat.value,
|
||||||
|
isAuthenticated,
|
||||||
|
isTokenExpired
|
||||||
|
});
|
||||||
|
if (isWeChat.value && !isAuthenticated && !isTokenExpired) {
|
||||||
|
console.log("🔄 Initiating WeChat auth flow");
|
||||||
|
// 如果正在授权中或已完成授权,则阻止重复授权
|
||||||
|
console.log("Auth store state:", {
|
||||||
|
isWeixinAuthing: authStore.isWeixinAuthing,
|
||||||
|
weixinAuthComplete: authStore.weixinAuthComplete
|
||||||
|
});
|
||||||
|
if (authStore.isWeixinAuthing || authStore.weixinAuthComplete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 保存目标路由
|
||||||
|
authStore.startWeixinAuth(route);
|
||||||
|
console.log("🔖 Saved pendingRoute for WeChat auth:", route.fullPath);
|
||||||
|
const appId = import.meta.env.VITE_WECHAT_APP_ID;
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
const params = new URLSearchParams(url.search);
|
||||||
|
params.delete("code");
|
||||||
|
params.delete("state");
|
||||||
|
const cleanUrl = `${url.origin}${url.pathname}${params.toString() ? "?" + params.toString() : ""
|
||||||
|
}`;
|
||||||
|
const redirectUri = encodeURIComponent(cleanUrl);
|
||||||
|
const weixinAuthUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}&redirect_uri=${redirectUri}&response_type=code&scope=snsapi_base&state=snsapi_base#wechat_redirect`;
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"🔄 Triggering WeChat auth from route guard, pendingRoute:",
|
||||||
|
route.fullPath
|
||||||
|
);
|
||||||
|
window.location.href = weixinAuthUrl;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<RouterView />
|
<RouterView />
|
||||||
<BindPhoneDialog />
|
<BindPhoneDialog />
|
||||||
|
<BindPhoneOnlyDialog />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
359
src/api/agent.js
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
import useApiFetch from "@/composables/useApiFetch";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 代理相关API调用统一管理
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建查询字符串的辅助函数
|
||||||
|
* @param {object} params - 查询参数对象
|
||||||
|
* @returns {string} 查询字符串
|
||||||
|
*/
|
||||||
|
function buildQueryString(params) {
|
||||||
|
const queryParams = new URLSearchParams();
|
||||||
|
Object.keys(params).forEach((key) => {
|
||||||
|
if (
|
||||||
|
params[key] !== undefined &&
|
||||||
|
params[key] !== null &&
|
||||||
|
params[key] !== ""
|
||||||
|
) {
|
||||||
|
queryParams.append(key, params[key]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const queryString = queryParams.toString();
|
||||||
|
return queryString ? `?${queryString}` : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 公开接口(无需登录) ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取推广链接数据
|
||||||
|
* @param {string} linkIdentifier - 推广链接标识
|
||||||
|
*/
|
||||||
|
export function getLinkData(linkIdentifier) {
|
||||||
|
return useApiFetch(
|
||||||
|
`/agent/link?link_identifier=${encodeURIComponent(linkIdentifier)}`
|
||||||
|
)
|
||||||
|
.get()
|
||||||
|
.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过邀请码申请成为代理(已注册用户)
|
||||||
|
* @param {object} params - 申请参数
|
||||||
|
* @param {string} params.mobile - 手机号
|
||||||
|
* @param {string} params.code - 验证码
|
||||||
|
* @param {string} params.invite_code - 邀请码(必填)
|
||||||
|
* @param {string} params.region - 区域(可选)
|
||||||
|
*/
|
||||||
|
export function applyForAgent(params) {
|
||||||
|
return useApiFetch("/agent/apply").post(params).json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过邀请码注册(同时注册用户和代理)
|
||||||
|
* @param {object} params - 注册参数
|
||||||
|
* @param {string} params.mobile - 手机号
|
||||||
|
* @param {string} params.code - 验证码
|
||||||
|
* @param {string} params.invite_code - 邀请码(必填)
|
||||||
|
* @param {string} params.region - 区域(可选)
|
||||||
|
* @param {string} params.wechat_id - 微信号(可选)
|
||||||
|
*/
|
||||||
|
export function registerByInviteCode(params) {
|
||||||
|
return useApiFetch("/agent/register/invite").post(params).json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 需要登录的接口 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取代理信息
|
||||||
|
*/
|
||||||
|
export function getAgentInfo() {
|
||||||
|
return useApiFetch("/agent/info").get().json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取代理等级特权信息
|
||||||
|
*/
|
||||||
|
export function getLevelPrivilege() {
|
||||||
|
return useApiFetch("/agent/level/privilege").get().json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成推广链接
|
||||||
|
* @param {object} params - 生成参数
|
||||||
|
* @param {number} params.product_id - 产品ID
|
||||||
|
* @param {number} params.set_price - 设定价格
|
||||||
|
*/
|
||||||
|
export function generateLink(params) {
|
||||||
|
return useApiFetch("/agent/generating_link").post(params).json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取产品配置
|
||||||
|
*/
|
||||||
|
export function getProductConfig() {
|
||||||
|
return useApiFetch("/agent/product_config").get().json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取团队统计
|
||||||
|
*/
|
||||||
|
export function getTeamStatistics() {
|
||||||
|
return useApiFetch("/agent/team/statistics").get().json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取团队列表
|
||||||
|
* @param {object} params - 查询参数
|
||||||
|
* @param {number} params.page - 页码
|
||||||
|
* @param {number} params.page_size - 每页数量
|
||||||
|
*/
|
||||||
|
export function getTeamList(params) {
|
||||||
|
const queryString = buildQueryString(params || {});
|
||||||
|
return useApiFetch(`/agent/team/list${queryString}`).get().json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取下级列表
|
||||||
|
* @param {object} params - 查询参数
|
||||||
|
* @param {number} params.page - 页码
|
||||||
|
* @param {number} params.page_size - 每页数量
|
||||||
|
*/
|
||||||
|
export function getSubordinateList(params) {
|
||||||
|
const queryString = buildQueryString(params || {});
|
||||||
|
return useApiFetch(`/agent/subordinate/list${queryString}`).get().json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取收益信息
|
||||||
|
*/
|
||||||
|
export function getRevenueInfo() {
|
||||||
|
return useApiFetch("/agent/revenue").get().json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取转化率统计
|
||||||
|
*/
|
||||||
|
export function getConversionRate() {
|
||||||
|
return useApiFetch("/agent/conversion/rate").get().json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取佣金记录
|
||||||
|
* @param {object} params - 查询参数
|
||||||
|
* @param {number} params.page - 页码
|
||||||
|
* @param {number} params.page_size - 每页数量
|
||||||
|
*/
|
||||||
|
export function getCommissionList(params) {
|
||||||
|
const queryString = buildQueryString(params || {});
|
||||||
|
return useApiFetch(`/agent/commission/list${queryString}`).get().json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取返佣记录(推广返佣)
|
||||||
|
* @param {object} params - 查询参数
|
||||||
|
* @param {number} params.page - 页码
|
||||||
|
* @param {number} params.page_size - 每页数量
|
||||||
|
* @param {number} params.rebate_type - 返佣类型(可选):1=直接上级返佣,2=钻石上级返佣,3=黄金上级返佣
|
||||||
|
*/
|
||||||
|
export function getRebateList(params) {
|
||||||
|
const queryString = buildQueryString(params || {});
|
||||||
|
return useApiFetch(`/agent/rebate/list${queryString}`).get().json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取升级返佣记录
|
||||||
|
* @param {object} params - 查询参数
|
||||||
|
* @param {number} params.page - 页码
|
||||||
|
* @param {number} params.page_size - 每页数量
|
||||||
|
*/
|
||||||
|
export function getUpgradeRebateList(params) {
|
||||||
|
const queryString = buildQueryString(params || {});
|
||||||
|
return useApiFetch(`/agent/rebate/upgrade/list${queryString}`).get().json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取升级记录
|
||||||
|
* @param {object} params - 查询参数
|
||||||
|
* @param {number} params.page - 页码
|
||||||
|
* @param {number} params.page_size - 每页数量
|
||||||
|
*/
|
||||||
|
export function getUpgradeList(params) {
|
||||||
|
const queryString = buildQueryString(params || {});
|
||||||
|
return useApiFetch(`/agent/upgrade/list${queryString}`).get().json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 申请升级
|
||||||
|
* @param {object} params - 升级参数
|
||||||
|
* @param {number} params.to_level - 目标等级:2=黄金,3=钻石
|
||||||
|
*/
|
||||||
|
export function applyUpgrade(params) {
|
||||||
|
return useApiFetch("/agent/upgrade/apply").post(params).json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 钻石代理升级下级
|
||||||
|
* @param {object} params - 升级参数
|
||||||
|
* @param {number} params.subordinate_id - 下级代理ID
|
||||||
|
* @param {number} params.to_level - 目标等级(只能是2=黄金)
|
||||||
|
*/
|
||||||
|
export function upgradeSubordinate(params) {
|
||||||
|
return useApiFetch("/agent/upgrade/subordinate").post(params).json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取提现列表
|
||||||
|
* @param {object} params - 查询参数
|
||||||
|
* @param {number} params.page - 页码
|
||||||
|
* @param {number} params.page_size - 每页数量
|
||||||
|
*/
|
||||||
|
export function getWithdrawalList(params) {
|
||||||
|
const queryString = buildQueryString(params || {});
|
||||||
|
return useApiFetch(`/agent/withdrawal/list${queryString}`).get().json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 申请提现
|
||||||
|
* @param {object} params - 提现参数
|
||||||
|
* @param {number} params.withdrawal_type - 提现方式:1=支付宝,2=银行卡
|
||||||
|
* @param {number} params.amount - 提现金额
|
||||||
|
* @param {string} params.payee_account - 收款账户(支付宝账号)
|
||||||
|
* @param {string} params.payee_name - 收款人姓名
|
||||||
|
* @param {string} params.bank_card_no - 银行卡号(银行卡提现时必填)
|
||||||
|
* @param {string} params.bank_name - 开户行名称(银行卡提现时必填)
|
||||||
|
*/
|
||||||
|
export function applyWithdrawal(params) {
|
||||||
|
return useApiFetch("/agent/withdrawal/apply").post(params).json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取上次提现信息(用于前端预填)
|
||||||
|
* @param {object} params - 查询参数
|
||||||
|
* @param {number} params.withdrawal_type - 提现方式:1=支付宝,2=银行卡
|
||||||
|
*/
|
||||||
|
export function getLastWithdrawalInfo(params) {
|
||||||
|
const queryString = buildQueryString(params || {});
|
||||||
|
return useApiFetch(`/agent/withdrawal/last_info${queryString}`)
|
||||||
|
.get()
|
||||||
|
.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 实名认证
|
||||||
|
* @param {object} params - 实名认证参数
|
||||||
|
* @param {string} params.name - 姓名
|
||||||
|
* @param {string} params.id_card - 身份证号
|
||||||
|
* @param {string} params.mobile - 手机号
|
||||||
|
* @param {string} params.code - 验证码
|
||||||
|
*/
|
||||||
|
export function realNameAuth(params) {
|
||||||
|
return useApiFetch("/agent/real_name").post(params).json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成邀请码
|
||||||
|
* @param {object} params - 生成参数
|
||||||
|
* @param {number} params.count - 生成数量
|
||||||
|
* @param {number} params.expire_days - 过期天数(可选,0表示不过期)
|
||||||
|
* @param {string} params.remark - 备注(可选)
|
||||||
|
*/
|
||||||
|
export function generateInviteCode(params) {
|
||||||
|
return useApiFetch("/agent/invite_code/generate").post(params).json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取邀请码列表
|
||||||
|
* @param {object} params - 查询参数
|
||||||
|
* @param {number} params.page - 页码
|
||||||
|
* @param {number} params.page_size - 每页数量
|
||||||
|
* @param {number} params.status - 状态(可选):0=未使用,1=已使用,2=已失效
|
||||||
|
*/
|
||||||
|
export function getInviteCodeList(params) {
|
||||||
|
const queryString = buildQueryString(params || {});
|
||||||
|
return useApiFetch(`/agent/invite_code/list${queryString}`).get().json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除邀请码
|
||||||
|
* @param {object} params - 删除参数
|
||||||
|
* @param {number} params.id - 邀请码ID
|
||||||
|
*/
|
||||||
|
export function deleteInviteCode(params) {
|
||||||
|
return useApiFetch("/agent/invite_code/delete").post(params).json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取邀请链接
|
||||||
|
* @param {object} params - 请求参数
|
||||||
|
* @param {string} params.invite_code - 邀请码
|
||||||
|
*/
|
||||||
|
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列表
|
||||||
|
* @param {string} params.order_id - 关联的查询订单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标识
|
||||||
|
*/
|
||||||
|
export function checkFeatureWhitelistStatus(params) {
|
||||||
|
const queryString = buildQueryString(params || {});
|
||||||
|
return useApiFetch(`/agent/whitelist/check${queryString}`).get().json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下架单个模块(创建订单并支付)
|
||||||
|
* @param {object} params - 下架参数
|
||||||
|
* @param {string} params.id_card - 身份证号
|
||||||
|
* @param {string} params.feature_api_id - Feature的API标识
|
||||||
|
* @param {string} params.order_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();
|
||||||
|
}
|
||||||
@@ -1,8 +1,24 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import useApiFetch from "@/composables/useApiFetch";
|
||||||
|
|
||||||
// 获取API基础URL(与生产规则一致:VITE_API_URL)
|
// 获取API基础URL(与生产规则一致:VITE_API_URL)
|
||||||
const baseURL = import.meta.env.VITE_API_URL;
|
const baseURL = import.meta.env.VITE_API_URL;
|
||||||
|
|
||||||
|
// 手机号验证码登录
|
||||||
|
export function mobileCodeLogin(params) {
|
||||||
|
return useApiFetch("/user/mobileCodeLogin").post(params).json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一认证
|
||||||
|
export function unifiedAuth(params) {
|
||||||
|
return useApiFetch("/user/auth").post(params).json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定手机号
|
||||||
|
export function bindMobile(params) {
|
||||||
|
return useApiFetch("/user/bindMobile").post(params).json();
|
||||||
|
}
|
||||||
|
|
||||||
// 注销账号API
|
// 注销账号API
|
||||||
export function cancelAccount() {
|
export function cancelAccount() {
|
||||||
return axios({
|
return axios({
|
||||||
|
|||||||
BIN
src/assets/images/index/n/01.png
Normal file
|
After Width: | Height: | Size: 461 KiB |
BIN
src/assets/images/index/n/02.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
src/assets/images/index/n/03.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
src/assets/images/index/n/04.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
src/assets/images/index/n/05.png
Normal file
|
After Width: | Height: | Size: 333 KiB |
BIN
src/assets/images/index/n/06.png
Normal file
|
After Width: | Height: | Size: 178 KiB |
BIN
src/assets/images/index/n/07.png
Normal file
|
After Width: | Height: | Size: 129 KiB |
BIN
src/assets/images/index/n/08.png
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
src/assets/images/index/n/09.png
Normal file
|
After Width: | Height: | Size: 103 KiB |
BIN
src/assets/images/index/n/10.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
src/assets/images/index/n/11.png
Normal file
|
After Width: | Height: | Size: 160 KiB |
BIN
src/assets/images/index/n/样板.png
Normal file
|
After Width: | Height: | Size: 380 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 1.5 MiB |
1
src/assets/images/me/lxkf.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1764650175158" class="icon" viewBox="0 0 1052 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="14882" xmlns:xlink="http://www.w3.org/1999/xlink" width="205.46875" height="200"><path d="M516.920889 741.745778c4.010667 0 8.106667-0.227556 12.231111-0.711111 39.395556-4.551111 72.874667-31.260444 85.248-68.010667a5.575111 5.575111 0 0 0-3.128889-6.912 5.973333 5.973333 0 0 0-7.480889 2.275556c-0.142222 0.284444-15.815111 24.746667-76.8 32.711111-9.102222 1.194667-17.976889 1.820444-26.396444 1.820444-42.723556 0-61.781333-15.616-61.952-15.758222a5.973333 5.973333 0 0 0-7.623111-0.085333 5.575111 5.575111 0 0 0-1.223111 7.338666 103.964444 103.964444 0 0 0 87.125333 47.331556zM844.8 375.808c-0.540444 0-1.024 0.113778-1.536 0.113778C807.537778 226.133333 671.004444 114.346667 507.335111 114.346667c-168.163556 0-307.797333 117.930667-338.773333 273.92-30.094222 5.233778-52.992 30.833778-52.992 61.809777v124.017778c0 34.702222 28.785778 62.919111 64.284444 62.919111 20.053333 0 37.745778-9.187556 49.521778-23.239111 28.444444 72.817778 86.328889 131.527111 159.573333 162.503111a32.995556 32.995556 0 0 1 3.242667-5.034666c1.137778-1.450667 2.446222-2.645333 3.555556-2.645334 1.137778 0 2.190222 0.398222 3.100444 1.024-16.952889-12.231111-78.222222-75.064889-91.477333-162.759111-5.802667-38.570667 24.035556-76.458667 58.823111-82.744889 55.808-10.069333 111.331556-21.532444 167.139555-31.402666 35.470222-6.257778 59.733333-25.116444 74.524445-56.490667 3.498667-7.338667 8.533333-22.186667 10.808889-43.548444a6.826667 6.826667 0 0 1 6.769777-5.632c2.275556 0 4.266667 1.137778 5.546667 2.759111l1.536-0.910223c21.987556 30.919111 65.621333 99.413333 71.879111 171.633778 7.196444 82.545778 3.185778 139.093333-62.065778 196.949334l-0.284444 0.227555a5.12 5.12 0 0 0-1.422222 3.555556c0 1.763556 0.967111 3.299556 2.389333 4.209777 0.540444 0.227556 1.080889 0.512 1.621333 0.711112 0.426667 0.085333 0.853333 0.227556 1.28 0.227555 0.455111 0 0.853333-0.142222 1.223112-0.227555 0.938667-0.483556 1.820444-1.024 2.730666-1.479112 65.991111-35.214222 116.622222-94.008889 140.003556-164.721777 9.472 13.624889 24.291556 23.409778 41.528889 26.851555-27.619556 121.656889-139.690667 197.831111-278.528 209.379556-8.305778-19.996444-28.785778-34.190222-52.849778-34.190222-31.431111 0-56.888889 24.092444-56.888889 53.788444s25.457778 53.76 56.888889 53.76c25.315556 0 46.535111-15.758222 53.902222-37.404444 160.711111-12.629333 289.536-105.159111 316.416-248.888889 23.779556-9.614222 40.448-32.170667 40.448-58.538667v-125.44c0-35.072-29.553778-63.516444-65.991111-63.516444z m-59.278222 36.039111c-41.927111-109.966222-150.528-188.501333-278.300445-188.501333-127.232 0-235.463111 77.880889-277.76 187.136-2.104889-2.503111-4.579556-4.636444-6.997333-6.798222 23.950222-133.12 142.250667-234.353778 284.899556-234.353778 141.966222 0 259.896889 100.238222 284.643555 232.419555a62.094222 62.094222 0 0 0-6.485333 10.097778zM389.091556 776.334222h-0.028445c-0.170667 0.341333-0.170667 0.398222 0.028445 0z" fill="#1daaf4" p-id="14883"></path></svg>
|
||||||
|
After Width: | Height: | Size: 3.1 KiB |
1
src/assets/images/me/sjdl.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1764650020736" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10374" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M700.02368 966.144H324.85568a140.8 140.8 0 0 1-130.24-90.368L57.46368 516.928a70.784 70.784 0 0 1 12.8-140.288 70.4 70.4 0 0 1 69.632 65.792 605.696 605.696 0 0 0 108.096 13.184 168.448 168.448 0 0 0 79.936-16.64c64-33.792 105.472-121.984 135.936-186.368l2.368-5.056 0.384-0.896a94.784 94.784 0 0 1-47.296-82.112 93.12 93.12 0 1 1 139.072 81.92v0.512a410.816 410.816 0 0 0 138.496 182.208 199.168 199.168 0 0 0 94.208 19.2 724.992 724.992 0 0 0 94.016-8.064 69.888 69.888 0 1 1 82.24 76.8l-137.152 358.784a140.8 140.8 0 0 1-130.176 90.24z m-212.8-312.192v178.24a33.28 33.28 0 1 0 66.56 0v-178.304l29.568 29.952a38.4 38.4 0 0 0 55.104 0 39.936 39.936 0 0 0 0-55.808L548.02368 536.448a38.4 38.4 0 0 0-24.704-11.392h-5.696a38.976 38.976 0 0 0-24.768 11.456l-90.304 91.52a39.936 39.936 0 0 0 0 55.808 38.4 38.4 0 0 0 55.168 0l29.44-29.824z" p-id="10375" fill="#1daaf4"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
1
src/assets/images/me/sjxj.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1764650035745" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11504" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M733.6448 947.2c-118.1184 0-213.6064-101.504-213.6064-226.5344 0-125.2608 95.6928-226.5856 213.6064-226.5856C851.456 494.08 947.2 595.584 947.2 720.6656 947.2 845.8752 851.6352 947.2 733.6448 947.2z m51.0976-316.8l-81.8432 101.6576v-101.6576h-66.3296v17.536h16.5376v193.3824h49.7152l164.7616-210.7392-82.7648-0.2048h-0.0768z m-184.5504-146.9952l-6.4512 4.224a217.216 217.216 0 0 1-115.9424 33.536c-124.4416 0-225.152-105.216-225.152-235.008C252.6464 156.3392 353.408 51.2 477.7472 51.2c124.4416 0 225.2032 105.216 225.2032 234.9312a238.2848 238.2848 0 0 1-26.9056 111.616 235.8784 235.8784 0 0 1-75.8528 85.6576z m-75.52 72.2944a288.3072 288.3072 0 0 0-51.456 164.9664c0 37.1968 6.9376 73.3952 20.5312 107.4688A274.4832 274.4832 0 0 0 584.4736 947.2h-191.1296C232.704 947.2 102.4 947.2 102.4 878.6688v-19.456c0-167.552 130.304-303.5136 290.944-303.5136h131.328z" p-id="11505" fill="#1daaf4"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
1
src/assets/images/me/smrz.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1764649808201" class="icon" viewBox="0 0 1169 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3433" xmlns:xlink="http://www.w3.org/1999/xlink" width="228.3203125" height="200"><path d="M72.227662 0h996.553234c17.168557 1.082587 33.980498 7.794627 46.780497 19.359204 16.162388 14.239204 25.7401 35.547065 25.6 57.084179 0.012736 167.915622 0.025473 335.831244 0 503.734129-25.803781-31.751642-58.192239-58.141294-94.605373-76.812736-37.686766-19.435622-79.627463-30.47801-121.988458-32.261094-49.620697-2.228856-99.776318 8.227662-144.238806 30.401592-37.393831 18.531343-70.737512 45.11204-97.216318 77.360398-29.497313 35.827264-50.384876 78.697711-60.255523 124.039005-14.277413 64.751443-6.075224 134.240796 23.294727 193.719403-189.911244 0.025473-379.809751 0.012736-569.708259 0.012736-21.549851 0.127363-42.883184-9.450348-57.096915-25.625473C7.8201 858.249552 1.146269 841.501294 0 824.409154V72.40597C1.120796 54.345871 8.54607 36.680597 21.167761 23.638607 34.38806 9.628657 53.046766 1.235423 72.227662 0m240.487164 132.190249c-28.64398 3.362388-56.07801 16.162388-77.054726 35.954627-22.530547 20.96398-37.623085 49.735323-41.927961 80.213333-4.444975 30.274229 1.630249 61.974925 17.104876 88.39005 16.862886 29.153433 44.857313 51.671244 77.029254 61.707462 31.369552 9.99801 66.330746 8.138507 96.41393-5.22189 30.121393-13.169353 55.071841-37.635821 68.890746-67.451542 14.442985-30.681791 16.71005-66.827463 6.393632-99.126767-9.488557-30.363383-30.108657-56.995025-56.918607-74.061691-26.415124-16.926567-58.790846-24.275423-89.931144-20.403582m333.475025 31.471443c-6.457313 2.088756-11.984876 6.852139-14.965174 12.940099-3.960995 7.845572-3.413333 17.741692 1.426467 25.077811 4.63602 7.374328 13.271244 11.895721 21.97015 11.679204 109.978109 0.025473 219.968955 0.025473 329.947064 0.012736 8.698905 0.216517 17.346866-4.304876 22.021095-11.666467 5.196418-7.883781 5.412935-18.684179 0.522189-26.771742-4.534129-7.832836-13.475025-12.774527-22.517811-12.507064-107.851144 0.012736-215.689552 0-323.52796 0-4.9799 0.038209-10.0999-0.38209-14.87602 1.235423m0.063681 110.793233c-6.775721 2.152438-12.545274 7.29791-15.436418 13.80617-3.642587 7.947463-2.763781 17.754428 2.279801 24.899502 4.725174 6.966766 13.080199 11.195224 21.498906 11.004179 110.436617 0 220.873234 0.076418 331.309851-0.038209 14.035423 0.012736 26.134925-13.334925 24.670248-27.319403-0.598607-9.832438-7.501692-18.837015-16.773731-22.084776-5.005373-1.897711-10.443781-1.464677-15.678408-1.490149H661.027662c-4.941692 0.038209-10.010746-0.38209-14.77413 1.222686M214.199403 476.605771c-30.516219 23.460299-53.925572 55.976119-66.636418 92.3001-12.507065 35.648955-14.557612 74.851343-6.024279 111.63383 130.101493 0.025473 260.190249-0.025473 390.291742 0.025473 9.552239-40.794428 5.896915-84.581891-10.418309-123.172935-16.95204-40.450547-47.646567-74.940498-85.804577-96.515821-32.286567-18.505871-69.858706-27.53592-107.010547-25.931144-41.252935 1.464677-81.843582 16.315224-114.397612 41.660497z" p-id="3434" fill="#1daaf4"></path><path d="M882.028259 511.694328c50.066468-5.84597 101.788657 3.247761 146.658706 26.300498 39.75005 20.3399 74.061692 51.2 98.413533 88.619303 21.231443 32.464876 34.922985 69.795025 39.60995 108.296916 5.514826 44.296915-0.611343 90.045771-17.970946 131.196816-21.129552 50.512239-58.943682 93.85393-106.144477 121.63184-32.65592 19.410149-69.74408 31.433234-107.609154 34.668259-44.742687 4.126567-90.542488-3.782687-131.273234-22.772537-37.228259-17.346866-70.202587-43.711045-95.254926-76.277811-27.29393-35.317811-45.137512-77.882587-50.983482-122.141294-6.355423-47.022488 0.458507-95.789851 19.906866-139.093333 18.505871-41.58408 48.359801-77.971741 85.384278-104.399602 35.012139-25.090547 76.456119-41.125572 119.262886-46.029055m144.633632 157.051543c-5.056318 1.69393-8.877214 5.553035-12.481592 9.297512-41.813333 41.813333-83.61393 83.626667-125.44 125.414527-26.020299-25.994826-52.027861-52.015124-78.035423-78.022686-3.222289-3.209552-6.317214-6.724776-10.469254-8.737115-7.705473-3.973731-17.512438-3.324179-24.606567 1.642986-5.553035 4.024677-10.341891 9.666866-11.704677 16.557213-1.872239 8.100299 0.904279 16.977512 6.915821 22.708856 33.52199 33.509254 67.018507 67.056716 100.578706 100.540498 6.406368 6.457313 16.56995 8.660697 25.103284 5.553035 6.266269-2.037811 10.405572-7.412537 15.385473-11.386269 47.009751-46.882388 93.904876-93.904876 140.889154-140.812736 3.515224-3.375124 6.32995-7.578109 7.463483-12.366966 5.234627-18.467662-15.652935-37.686766-33.598408-30.388855z" p-id="3435" fill="#1daaf4"></path></svg>
|
||||||
|
After Width: | Height: | Size: 4.6 KiB |
1
src/assets/images/me/tgcxjl.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1765182453848" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2537" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M0 0h1024v1024H0z" fill="#1daaf4" fill-opacity="0" p-id="2538"></path><path d="M668 484c121.503 0 220 98.497 220 220 0 42.679-12.153 82.52-33.187 116.25l54.531 54.532c19.527 19.526 19.527 51.184 0 70.71-19.33 19.331-50.552 19.525-70.12 0.58l-0.59-0.58-54.567-54.565C750.376 911.89 710.602 924 668 924c-121.503 0-220-98.497-220-220s98.497-220 220-220zM768 64c17.673 0 32.004 14.327 32.004 32v356.477C760.566 431.737 715.654 420 668 420c-156.849 0-284 127.151-284 284 0 112.742 65.694 210.14 160.891 256.003H192c-17.673 0-32-14.33-32-32.003V428h268c53.02 0 96-42.98 96-96V64h244zM668 584c-66.274 0-120 53.726-120 120s53.726 120 120 120 120-53.726 120-120-53.726-120-120-120zM460 64v268c0 17.673-14.327 32-32 32H160L460 64z" fill="#1daaf4" p-id="2539"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
1
src/assets/images/me/tx.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1764649823438" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4596" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M922.122705 347.051227c0-19.764136-16.021905-35.785018-35.785018-35.785018L99.064226 311.26621c-19.764136 0-35.785018 16.020882-35.785018 35.785018l0 533.198811c0 19.763113 16.020882 35.785018 35.785018 35.785018l787.273461 0c19.763113 0 35.785018-16.021905 35.785018-35.785018L922.122705 744.266971 696.67607 744.266971l0-0.022513c-0.596588 0.008186-1.191128 0.022513-1.788739 0.022513-72.136947 0-130.615826-58.478879-130.615826-130.615826s58.478879-130.615826 130.615826-130.615826c0.598634 0 1.193175 0.014326 1.788739 0.022513l0-0.022513 225.446635 0L922.122705 347.051227zM617.949031 613.651145c0 42.491767 34.446533 76.9383 76.9383 76.9383s76.9383-34.446533 76.9383-76.9383-34.446533-76.9383-76.9383-76.9383S617.949031 571.159378 617.949031 613.651145zM836.231499 277.968849l-44.421722-155.503657c-4.469801-15.647375-20.828374-24.724101-36.537148-20.273742L134.930085 277.968849 836.231499 277.968849z" fill="#1daaf4" p-id="4597"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
1
src/assets/images/me/yhxy.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1764650051703" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12625" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M775.912727 105.192727H234.356364a69.818182 69.818182 0 0 0-68.654546 68.654546V837.818182a69.818182 69.818182 0 0 0 68.654546 69.818182h384a226.210909 226.210909 0 0 0 226.210909-226.210909V173.847273a69.818182 69.818182 0 0 0-68.654546-68.654546zM541.789091 577.396364H302.545455a18.385455 18.385455 0 0 1 0-36.770909h239.243636a18.385455 18.385455 0 1 1 0 36.770909z m165.236364-128.465455H302.545455a18.385455 18.385455 0 0 1 0-36.770909h404.48a18.385455 18.385455 0 1 1 0 36.770909z m0-128.465454H302.545455a18.385455 18.385455 0 0 1 0-36.77091h404.48a18.385455 18.385455 0 1 1 0 36.77091z" fill="#1daaf4" p-id="12626"></path></svg>
|
||||||
|
After Width: | Height: | Size: 971 B |
1
src/assets/images/me/yqmgl.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1764649988299" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6624" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M714.5 501.2c0 19.5-16 35.4-35.4 35.4H345.7c-19.4 0-35.4-16-35.4-35.4 0-19.5 16-35.4 35.4-35.4h333.4c19.5 0 35.4 15.8 35.4 35.4z m244.7-133.4V261.7c0-29.7-11.7-58.2-32.7-79.2-21-21-49.5-32.9-79.2-32.7H176.8c-29.7 0-58.2 11.8-79.1 32.7-21 21-32.7 49.5-32.7 79.2v106.1c82.3 0 149 65.1 149 145.3S147.3 658.4 65 658.4v106c0 29.7 11.7 58.2 32.7 79.2 21 21 49.5 32.9 79.2 32.7h670.6c29.7 0 58.2-11.8 79.1-32.7 21-21 32.7-49.5 32.7-79.2v-106c-82.3 0-149-65.1-149-145.3-0.1-80.4 66.7-145.3 148.9-145.3z" fill="#1daaf4" p-id="6625"></path></svg>
|
||||||
|
After Width: | Height: | Size: 869 B |
1
src/assets/images/me/yszc.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1764650162537" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="13739" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 384m-85.333333 0a85.333333 85.333333 0 1 0 170.666666 0 85.333333 85.333333 0 1 0-170.666666 0Z" p-id="13740" fill="#1daaf4"></path><path d="M768 56.888889H256a113.777778 113.777778 0 0 0-113.777778 113.777778v682.666666a113.777778 113.777778 0 0 0 113.777778 113.777778h369.777778a256 256 0 0 0 256-256V170.666667a113.777778 113.777778 0 0 0-113.777778-113.777778z m-142.222222 554.666667a42.666667 42.666667 0 0 1 0 85.333333h-71.111111V768a42.666667 42.666667 0 0 1-85.333334 0v-219.022222a170.666667 170.666667 0 1 1 85.333334 0v62.577778z" p-id="13741" fill="#1daaf4"></path></svg>
|
||||||
|
After Width: | Height: | Size: 925 B |
BIN
src/assets/images/ysjjt.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
5
src/auto-imports.d.ts
vendored
@@ -93,7 +93,9 @@ declare global {
|
|||||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||||
const shallowRef: typeof import('vue')['shallowRef']
|
const shallowRef: typeof import('vue')['shallowRef']
|
||||||
const showConfirmDialog: typeof import('vant/es')['showConfirmDialog']
|
const showConfirmDialog: typeof import('vant/es')['showConfirmDialog']
|
||||||
|
const showFailToast: typeof import('vant/es')['showFailToast']
|
||||||
const showLoadingToast: typeof import('vant/es')['showLoadingToast']
|
const showLoadingToast: typeof import('vant/es')['showLoadingToast']
|
||||||
|
const showSuccessToast: typeof import('vant/es')['showSuccessToast']
|
||||||
const showToast: typeof import('vant/es')['showToast']
|
const showToast: typeof import('vant/es')['showToast']
|
||||||
const syncRef: typeof import('@vueuse/core')['syncRef']
|
const syncRef: typeof import('@vueuse/core')['syncRef']
|
||||||
const syncRefs: typeof import('@vueuse/core')['syncRefs']
|
const syncRefs: typeof import('@vueuse/core')['syncRefs']
|
||||||
@@ -117,8 +119,10 @@ declare global {
|
|||||||
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
|
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
|
||||||
const useAgent: typeof import('./composables/useAgent.js')['useAgent']
|
const useAgent: typeof import('./composables/useAgent.js')['useAgent']
|
||||||
const useAgentStore: typeof import('./stores/agentStore.js')['useAgentStore']
|
const useAgentStore: typeof import('./stores/agentStore.js')['useAgentStore']
|
||||||
|
const useAliyunCaptcha: typeof import('./composables/useAliyunCaptcha.js')['default']
|
||||||
const useAnimate: typeof import('@vueuse/core')['useAnimate']
|
const useAnimate: typeof import('@vueuse/core')['useAnimate']
|
||||||
const useApiFetch: typeof import('./composables/useApiFetch.js')['default']
|
const useApiFetch: typeof import('./composables/useApiFetch.js')['default']
|
||||||
|
const useAppStore: typeof import('./stores/appStore.js')['useAppStore']
|
||||||
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
|
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
|
||||||
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
|
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
|
||||||
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
|
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
|
||||||
@@ -248,6 +252,7 @@ declare global {
|
|||||||
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
|
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
|
||||||
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
|
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
|
||||||
const useShare: typeof import('@vueuse/core')['useShare']
|
const useShare: typeof import('@vueuse/core')['useShare']
|
||||||
|
const useShareReport: typeof import('./composables/useShareReport.js')['useShareReport']
|
||||||
const useSlots: typeof import('vue')['useSlots']
|
const useSlots: typeof import('vue')['useSlots']
|
||||||
const useSorted: typeof import('@vueuse/core')['useSorted']
|
const useSorted: typeof import('@vueuse/core')['useSorted']
|
||||||
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
|
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
|
||||||
|
|||||||
@@ -6,17 +6,22 @@
|
|||||||
>
|
>
|
||||||
成为代理
|
成为代理
|
||||||
</div>
|
</div>
|
||||||
<div v-if="ancestor" class="text-center text-xs my-2" style="color: var(--van-text-color-2);">
|
|
||||||
{{ maskName(ancestor) }}邀您成为一查查代理方
|
|
||||||
</div>
|
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
|
<van-field
|
||||||
|
label-width="56"
|
||||||
|
v-model="form.referrer"
|
||||||
|
label="邀请信息"
|
||||||
|
name="referrer"
|
||||||
|
placeholder="请输入邀请码/代理编码/代理手机号"
|
||||||
|
required
|
||||||
|
/>
|
||||||
<van-field
|
<van-field
|
||||||
label-width="56"
|
label-width="56"
|
||||||
v-model="form.region"
|
v-model="form.region"
|
||||||
is-link
|
is-link
|
||||||
readonly
|
readonly
|
||||||
label="地区"
|
label="地区"
|
||||||
placeholder="请选择地区"
|
placeholder="请选择地区(可选)"
|
||||||
@click="showCascader = true"
|
@click="showCascader = true"
|
||||||
/>
|
/>
|
||||||
<van-popup v-model:show="showCascader" round position="bottom">
|
<van-popup v-model:show="showCascader" round position="bottom">
|
||||||
@@ -121,13 +126,11 @@
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const show = defineModel("show");
|
const show = defineModel("show");
|
||||||
import { useCascaderAreaData } from "@vant/area-data";
|
import { useCascaderAreaData } from "@vant/area-data";
|
||||||
import { showToast } from "vant"; // 引入 showToast 方法
|
import { showToast } from "vant";
|
||||||
|
import useApiFetch from "@/composables/useApiFetch";
|
||||||
|
import { useAliyunCaptcha } from "@/composables/useAliyunCaptcha";
|
||||||
const emit = defineEmits(); // 确保 emit 可以正确使用
|
const emit = defineEmits(); // 确保 emit 可以正确使用
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
ancestor: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
isSelf: {
|
isSelf: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
@@ -137,15 +140,17 @@ const props = defineProps({
|
|||||||
default: "",
|
default: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const { ancestor, isSelf, userName } = toRefs(props);
|
const { isSelf, userName } = toRefs(props);
|
||||||
const form = ref({
|
const form = ref({
|
||||||
|
referrer: "",
|
||||||
region: "",
|
region: "",
|
||||||
mobile: "",
|
mobile: "",
|
||||||
code: "", // 增加验证码字段
|
code: "", // 验证码字段
|
||||||
});
|
});
|
||||||
const showCascader = ref(false);
|
const showCascader = ref(false);
|
||||||
const cascaderValue = ref("");
|
const cascaderValue = ref("");
|
||||||
const options = useCascaderAreaData();
|
const options = useCascaderAreaData();
|
||||||
|
const { runWithCaptcha } = useAliyunCaptcha();
|
||||||
const loadingSms = ref(false); // 控制验证码按钮的loading状态
|
const loadingSms = ref(false); // 控制验证码按钮的loading状态
|
||||||
const isCountingDown = ref(false);
|
const isCountingDown = ref(false);
|
||||||
const isAgreed = ref(false);
|
const isAgreed = ref(false);
|
||||||
@@ -170,21 +175,20 @@ const getSmsCode = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadingSms.value = true;
|
loadingSms.value = true;
|
||||||
|
runWithCaptcha(
|
||||||
const { data, error } = await useApiFetch("auth/sendSms")
|
(captchaVerifyParam) => useApiFetch("auth/sendSms")
|
||||||
.post({ mobile: form.value.mobile, actionType: "agentApply" })
|
.post({ mobile: form.value.mobile, actionType: "agentApply", captchaVerifyParam })
|
||||||
.json();
|
.json(),
|
||||||
|
(result) => {
|
||||||
loadingSms.value = false;
|
loadingSms.value = false;
|
||||||
|
if (result && result.code === 200) {
|
||||||
if (data.value && !error.value) {
|
showToast({ message: "获取成功" });
|
||||||
if (data.value.code === 200) {
|
startCountdown();
|
||||||
showToast({ message: "获取成功" });
|
} else if (result) {
|
||||||
startCountdown(); // 启动倒计时
|
showToast(result.msg || "发送失败");
|
||||||
} else {
|
}
|
||||||
showToast(data.value.msg);
|
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
};
|
};
|
||||||
let timer = null;
|
let timer = null;
|
||||||
|
|
||||||
@@ -207,10 +211,11 @@ onUnmounted(() => {
|
|||||||
});
|
});
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
// 校验表单字段
|
// 校验表单字段
|
||||||
if (!form.value.region) {
|
if (!form.value.referrer || !form.value.referrer.trim()) {
|
||||||
showToast({ message: "请选择地区" });
|
showToast({ message: "请输入邀请信息" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!form.value.mobile) {
|
if (!form.value.mobile) {
|
||||||
showToast({ message: "请输入手机号" });
|
showToast({ message: "请输入手机号" });
|
||||||
return;
|
return;
|
||||||
@@ -231,15 +236,13 @@ const submit = () => {
|
|||||||
showToast({ message: "请先阅读并同意用户协议及相关条款" });
|
showToast({ message: "请先阅读并同意用户协议及相关条款" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log("form", form.value);
|
|
||||||
// 触发父组件提交申请
|
// 触发父组件提交申请
|
||||||
emit("submit", form.value);
|
emit("submit", {
|
||||||
|
...form.value,
|
||||||
|
referrer: form.value.referrer.trim()
|
||||||
|
});
|
||||||
};
|
};
|
||||||
const maskName = computed(() => {
|
|
||||||
return (name) => {
|
|
||||||
return name.substring(0, 3) + "****" + name.substring(7);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
const closePopup = () => {
|
const closePopup = () => {
|
||||||
emit("close");
|
emit("close");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
本人在授权签署前,已通过实名认证及动态验证码验证(或其他身份验证手段),确认本授权行为为本人真实意思表示,平台已履行身份验证义务。
|
本人在授权签署前,已通过实名认证及动态验证码验证(或其他身份验证手段),确认本授权行为为本人真实意思表示,平台已履行身份验证义务。
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
本人在此声明已充分理解上述授权条款含义,知晓并自愿承担因授权数据使用可能带来的后果,包括但不限于影响个人信用评分、生活行为等。本人确认授权范围内的相关信息由本人提供并真实有效。
|
本人在此声明已充分理解上述授权条款含义,知晓并自愿承担因授权数据使用可能带来的后果,包括但不限于影响个人风险评分、生活行为等。本人确认授权范围内的相关信息由本人提供并真实有效。
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
若用户冒名签署或提供虚假信息,由用户自行承担全部法律责任,平台不承担任何后果。
|
若用户冒名签署或提供虚假信息,由用户自行承担全部法律责任,平台不承担任何后果。
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
<p class="mt-4 font-bold">
|
<p class="mt-4 font-bold">
|
||||||
签署人:<span class="underline">{{
|
签署人:<span class="underline">{{
|
||||||
signature ? props.name : "____________"
|
signature ? props.name : "____________"
|
||||||
}}</span>
|
}}</span>
|
||||||
<br />
|
<br />
|
||||||
手机号码:<span class="underline">
|
手机号码:<span class="underline">
|
||||||
{{ signature ? props.mobile : "____________" }}
|
{{ signature ? props.mobile : "____________" }}
|
||||||
|
|||||||
@@ -3,11 +3,18 @@ import ShareReportButton from "./ShareReportButton.vue";
|
|||||||
import TitleBanner from "./TitleBanner.vue";
|
import TitleBanner from "./TitleBanner.vue";
|
||||||
import VerificationCard from "./VerificationCard.vue";
|
import VerificationCard from "./VerificationCard.vue";
|
||||||
import StyledTabs from "./StyledTabs.vue";
|
import StyledTabs from "./StyledTabs.vue";
|
||||||
|
import WhitelistModuleDialog from "./WhitelistModuleDialog.vue";
|
||||||
|
import Payment from "./Payment.vue";
|
||||||
import { splitDWBG8B4DForTabs } from '@/ui/CDWBG8B4D/utils/simpleSplitter.js';
|
import { splitDWBG8B4DForTabs } from '@/ui/CDWBG8B4D/utils/simpleSplitter.js';
|
||||||
import { splitDWBG6A2CForTabs } from '@/ui/DWBG6A2C/utils/simpleSplitter.js';
|
import { splitDWBG6A2CForTabs } from '@/ui/DWBG6A2C/utils/simpleSplitter.js';
|
||||||
import { splitJRZQ7F1AForTabs } from '@/ui/JRZQ7F1A/utils/simpleSplitter.js';
|
import { splitJRZQ7F1AForTabs } from '@/ui/JRZQ7F1A/utils/simpleSplitter.js';
|
||||||
import { splitCJRZQ5E9FForTabs } from '@/ui/CJRZQ5E9F/utils/simpleSplitter.js';
|
import { splitCJRZQ5E9FForTabs } from '@/ui/CJRZQ5E9F/utils/simpleSplitter.js';
|
||||||
import { splitCQYGL3F8EForTabs } from '@/ui/CQYGL3F8E/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 { showSuccessToast, showFailToast, showConfirmDialog } from 'vant';
|
||||||
|
import { checkFeatureWhitelistStatus, offlineFeature, checkOrderAgent } from '@/api/agent';
|
||||||
|
|
||||||
// 动态导入产品背景图片的函数
|
// 动态导入产品背景图片的函数
|
||||||
const loadProductBackground = async (productType) => {
|
const loadProductBackground = async (productType) => {
|
||||||
@@ -17,7 +24,7 @@ const loadProductBackground = async (productType) => {
|
|||||||
return (await import("@/assets/images/report/xwqy_inquire_bg.png")).default;
|
return (await import("@/assets/images/report/xwqy_inquire_bg.png")).default;
|
||||||
case 'preloanbackgroundcheck':
|
case 'preloanbackgroundcheck':
|
||||||
return (await import("@/assets/images/report/dqfx_inquire_bg.png")).default;
|
return (await import("@/assets/images/report/dqfx_inquire_bg.png")).default;
|
||||||
case 'personalData':
|
case 'riskassessment':
|
||||||
return (await import("@/assets/images/report/grdsj_inquire_bg.png")).default;
|
return (await import("@/assets/images/report/grdsj_inquire_bg.png")).default;
|
||||||
case 'marriage':
|
case 'marriage':
|
||||||
return (await import("@/assets/images/report/marriage_inquire_bg.png")).default;
|
return (await import("@/assets/images/report/marriage_inquire_bg.png")).default;
|
||||||
@@ -36,6 +43,8 @@ const loadProductBackground = async (productType) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const appStore = useAppStore();
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
isShare: {
|
isShare: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -50,6 +59,11 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: "",
|
default: "",
|
||||||
},
|
},
|
||||||
|
queryId: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
feature: {
|
feature: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
@@ -95,8 +109,328 @@ const {
|
|||||||
isEmpty,
|
isEmpty,
|
||||||
isDone,
|
isDone,
|
||||||
isExample,
|
isExample,
|
||||||
|
orderId,
|
||||||
|
orderNo,
|
||||||
|
queryId,
|
||||||
} = toRefs(props);
|
} = toRefs(props);
|
||||||
|
|
||||||
|
// 代理信息
|
||||||
|
const agentStore = useAgentStore()
|
||||||
|
const { isDiamond } = storeToRefs(agentStore)
|
||||||
|
|
||||||
|
// 屏蔽模块弹窗(已废弃,保留用于兼容)
|
||||||
|
const showWhitelistDialog = ref(false)
|
||||||
|
|
||||||
|
// 订单是否属于当前代理推广
|
||||||
|
const isAgentOrder = ref(false)
|
||||||
|
|
||||||
|
// 获取身份证号(从 reportParams 中,用于展示与接口)
|
||||||
|
const idCard = computed(() => {
|
||||||
|
return reportParams.value?.id_card || ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 评分用身份证键:保证所有模式(自己报告/代理推广/示例/分享)都有稳定键,±10 按此计算
|
||||||
|
const scoreIdCard = computed(() => {
|
||||||
|
const raw = reportParams.value?.id_card
|
||||||
|
if (raw != null && String(raw).trim() !== '') return String(raw).trim()
|
||||||
|
return `${feature.value || 'report'}_${queryId.value || orderId.value || orderNo.value || 'unknown'}`
|
||||||
|
})
|
||||||
|
|
||||||
|
// 提取主模块ID(去掉下划线后的部分)
|
||||||
|
// 例如:JRZQ7F1A_BigDataReport -> JRZQ7F1A
|
||||||
|
const getMainApiId = (apiId) => {
|
||||||
|
if (!apiId) return ''
|
||||||
|
const index = apiId.indexOf('_')
|
||||||
|
return index > 0 ? apiId.substring(0, index) : apiId
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模块下架状态映射:主模块ID -> { isOfflined, whitelistPrice, isSubmitting }
|
||||||
|
// 使用主模块ID作为key,这样同一组模块(如JRZQ7F1A_*)会共享状态
|
||||||
|
const featureOfflineStatus = ref(new Map())
|
||||||
|
|
||||||
|
// 当前正在下架的模块信息(用于支付确认弹窗)
|
||||||
|
const currentOfflineFeature = ref(null)
|
||||||
|
const showOfflineConfirmDialog = ref(false)
|
||||||
|
|
||||||
|
// 检查模块下架状态(使用主模块ID)
|
||||||
|
const checkFeatureStatus = async (featureApiId, forceRefresh = false) => {
|
||||||
|
if (!idCard.value || !featureApiId) return
|
||||||
|
|
||||||
|
// 提取主模块ID
|
||||||
|
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, // 使用主模块ID调用接口
|
||||||
|
query_id: queryId.value || '', // 传递查询记录ID,用于检查报告数据是否已删除
|
||||||
|
})
|
||||||
|
if (data.value && !error.value && data.value.code === 200) {
|
||||||
|
// 使用主模块ID作为key存储状态
|
||||||
|
// 只有当白名单存在且报告数据已删除时,才认为已下架
|
||||||
|
const isWhitelisted = data.value.data.is_whitelisted || false
|
||||||
|
const dataDeleted = data.value.data.data_deleted !== undefined ? data.value.data.data_deleted : true // 默认认为已删除(保守处理)
|
||||||
|
const status = {
|
||||||
|
isOfflined: isWhitelisted && dataDeleted, // 白名单存在且数据已删除
|
||||||
|
whitelistPrice: data.value.data.whitelist_price || 0,
|
||||||
|
isSubmitting: false,
|
||||||
|
}
|
||||||
|
featureOfflineStatus.value.set(mainApiId, status)
|
||||||
|
// 调试信息
|
||||||
|
console.log(`[白名单状态] 主模块ID: ${mainApiId}, 白名单: ${isWhitelisted}, 数据已删除: ${dataDeleted}, 已下架: ${status.isOfflined}`, status)
|
||||||
|
} else {
|
||||||
|
console.warn(`[白名单状态] 检查失败:`, data.value?.msg || error.value)
|
||||||
|
}
|
||||||
|
} 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)
|
||||||
|
// 提取所有主模块ID并去重
|
||||||
|
const mainApiIds = [...new Set(featureApiIds.map(id => getMainApiId(id)))]
|
||||||
|
|
||||||
|
for (const mainApiId of mainApiIds) {
|
||||||
|
if (mainApiId) {
|
||||||
|
await checkFeatureStatus(mainApiId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取模块下架状态(使用主模块ID)
|
||||||
|
const getFeatureStatus = (featureApiId) => {
|
||||||
|
const mainApiId = getMainApiId(featureApiId)
|
||||||
|
const status = featureOfflineStatus.value.get(mainApiId) || {
|
||||||
|
isOfflined: false,
|
||||||
|
whitelistPrice: 0,
|
||||||
|
isSubmitting: false,
|
||||||
|
}
|
||||||
|
// 调试信息(仅在开发环境)
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
console.log(`[getFeatureStatus] featureApiId: ${featureApiId}, mainApiId: ${mainApiId}, status:`, status)
|
||||||
|
}
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理下架按钮点击
|
||||||
|
const handleOfflineClick = (featureApiId, featureName) => {
|
||||||
|
const mainApiId = getMainApiId(featureApiId)
|
||||||
|
const status = getFeatureStatus(mainApiId)
|
||||||
|
|
||||||
|
// 如果已下架,不允许再次点击
|
||||||
|
if (status.isOfflined) {
|
||||||
|
showFailToast('该模块已下架')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果 whitelistPrice = 0,直接下架(免费),不需要支付确认
|
||||||
|
if (status.whitelistPrice <= 0) {
|
||||||
|
confirmOfflineDirectly(mainApiId, featureName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果 whitelistPrice > 0,需要支付确认
|
||||||
|
currentOfflineFeature.value = {
|
||||||
|
featureApiId: mainApiId, // 使用主模块ID
|
||||||
|
featureName,
|
||||||
|
whitelistPrice: status.whitelistPrice,
|
||||||
|
}
|
||||||
|
showOfflineConfirmDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接下架(免费,不需要支付)
|
||||||
|
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 currentStatus = getFeatureStatus(mainApiId)
|
||||||
|
currentStatus.isSubmitting = false
|
||||||
|
featureOfflineStatus.value.set(mainApiId, { ...currentStatus })
|
||||||
|
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 currentStatus = getFeatureStatus(mainApiId)
|
||||||
|
currentStatus.isSubmitting = false
|
||||||
|
featureOfflineStatus.value.set(mainApiId, { ...currentStatus })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const resp = data.value.data || {}
|
||||||
|
// 如果返回 need_pay=true,说明需要支付,弹出支付弹窗
|
||||||
|
if (resp.need_pay) {
|
||||||
|
// 更新状态,保存实际的价格信息
|
||||||
|
const currentStatus = getFeatureStatus(mainApiId)
|
||||||
|
currentStatus.isSubmitting = false
|
||||||
|
currentStatus.whitelistPrice = resp.amount || 0 // 更新实际价格
|
||||||
|
featureOfflineStatus.value.set(mainApiId, { ...currentStatus })
|
||||||
|
|
||||||
|
// 弹出支付弹窗
|
||||||
|
whitelistPaymentData.value = {
|
||||||
|
product_name: `${featureName || '模块'} 下架`,
|
||||||
|
sell_price: resp.amount || 0,
|
||||||
|
}
|
||||||
|
// PaymentReq.Id 约定格式:"{idCard}|{featureApiId}"
|
||||||
|
whitelistPaymentId.value = `${idCard.value}|${mainApiId}`
|
||||||
|
whitelistPaymentType.value = 'whitelist'
|
||||||
|
showWhitelistPayment.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
showSuccessToast('下架成功')
|
||||||
|
|
||||||
|
// 更新状态:标记为已下架
|
||||||
|
const updatedStatus = getFeatureStatus(mainApiId)
|
||||||
|
updatedStatus.isSubmitting = false
|
||||||
|
updatedStatus.isOfflined = true // 标记为已下架
|
||||||
|
featureOfflineStatus.value.set(mainApiId, { ...updatedStatus })
|
||||||
|
|
||||||
|
// 刷新报告数据(重新加载,获取后端处理后的数据)
|
||||||
|
if (queryId.value || orderId.value) {
|
||||||
|
// 触发父组件刷新报告数据
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('下架模块失败:', err)
|
||||||
|
showFailToast('下架模块失败')
|
||||||
|
const currentStatus = getFeatureStatus(mainApiId)
|
||||||
|
currentStatus.isSubmitting = false
|
||||||
|
featureOfflineStatus.value.set(mainApiId, { ...currentStatus })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认下架
|
||||||
|
const confirmOffline = async () => {
|
||||||
|
if (!currentOfflineFeature.value) return
|
||||||
|
|
||||||
|
const { featureApiId } = currentOfflineFeature.value // 这里已经是主模块ID了
|
||||||
|
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, // 使用主模块ID调用接口
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!data.value || error.value || data.value.code !== 200) {
|
||||||
|
showFailToast(data.value?.msg || '下架失败')
|
||||||
|
const currentStatus = getFeatureStatus(mainApiId)
|
||||||
|
currentStatus.isSubmitting = false
|
||||||
|
featureOfflineStatus.value.set(mainApiId, { ...currentStatus })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const resp = data.value.data || {}
|
||||||
|
|
||||||
|
// 价格 > 0 的情况:
|
||||||
|
// - need_pay = true:提示前端发起支付(这里只做占位,真正支付流程复用现有支付系统)
|
||||||
|
// - success = true:表示已经有支付成功订单且后端已补写白名单,前端直接标记为已下架
|
||||||
|
if (resp.need_pay) {
|
||||||
|
// 关闭下架确认弹窗
|
||||||
|
showOfflineConfirmDialog.value = false
|
||||||
|
|
||||||
|
// 使用统一支付组件 Payment.vue 发起支付
|
||||||
|
whitelistPaymentData.value = {
|
||||||
|
product_name: `${currentOfflineFeature.value?.featureName || '模块'} 下架`,
|
||||||
|
sell_price: resp.amount || 0,
|
||||||
|
}
|
||||||
|
// PaymentReq.Id 约定格式:"{idCard}|{featureApiId}"
|
||||||
|
whitelistPaymentId.value = `${idCard.value}|${mainApiId}`
|
||||||
|
whitelistPaymentType.value = 'whitelist'
|
||||||
|
showWhitelistPayment.value = true
|
||||||
|
|
||||||
|
const currentStatus = getFeatureStatus(mainApiId)
|
||||||
|
currentStatus.isSubmitting = false
|
||||||
|
featureOfflineStatus.value.set(mainApiId, { ...currentStatus })
|
||||||
|
|
||||||
|
// 不立刻清空 currentOfflineFeature,方便支付完成后可根据需要刷新状态
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
showSuccessToast('下架成功')
|
||||||
|
showOfflineConfirmDialog.value = false
|
||||||
|
currentOfflineFeature.value = null
|
||||||
|
|
||||||
|
// 更新状态(不再标记为已下架,因为后端已处理数据,前端正常渲染即可)
|
||||||
|
const updatedStatus = getFeatureStatus(mainApiId)
|
||||||
|
updatedStatus.isSubmitting = false
|
||||||
|
featureOfflineStatus.value.set(mainApiId, { ...updatedStatus })
|
||||||
|
|
||||||
|
// 刷新报告数据(重新加载,获取后端处理后的数据)
|
||||||
|
if (queryId.value || orderId.value) {
|
||||||
|
// 触发父组件刷新报告数据
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('下架模块失败:', err)
|
||||||
|
showFailToast('下架模块失败')
|
||||||
|
const currentStatus = getFeatureStatus(mainApiId)
|
||||||
|
currentStatus.isSubmitting = false
|
||||||
|
featureOfflineStatus.value.set(mainApiId, { ...currentStatus })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开屏蔽模块弹窗(已废弃,保留用于兼容)
|
||||||
|
const openWhitelistDialog = () => {
|
||||||
|
if (!idCard.value) {
|
||||||
|
console.error('无法获取身份证号')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
showWhitelistDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 屏蔽成功后的回调(已废弃,保留用于兼容)
|
||||||
|
const onWhitelistSuccess = () => {
|
||||||
|
console.log('模块已屏蔽')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 白名单下架支付弹窗相关状态
|
||||||
|
const showWhitelistPayment = ref(false)
|
||||||
|
const whitelistPaymentData = ref({ product_name: '', sell_price: 0 })
|
||||||
|
const whitelistPaymentId = ref('')
|
||||||
|
const whitelistPaymentType = ref('whitelist')
|
||||||
|
|
||||||
|
// 获取当前报告页面的 URL(用于支付成功后返回)
|
||||||
|
const getCurrentReportUrl = () => {
|
||||||
|
if (orderNo.value) {
|
||||||
|
return `/report?orderNo=${orderNo.value}`
|
||||||
|
} else if (orderId.value) {
|
||||||
|
return `/report?orderId=${orderId.value}`
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
const active = ref(null);
|
const active = ref(null);
|
||||||
const backgroundContainerRef = ref(null); // 背景容器的引用
|
const backgroundContainerRef = ref(null); // 背景容器的引用
|
||||||
|
|
||||||
@@ -150,6 +484,23 @@ onMounted(async () => {
|
|||||||
|
|
||||||
// 监听窗口大小变化,重新计算高度
|
// 监听窗口大小变化,重新计算高度
|
||||||
window.addEventListener('resize', handleResize);
|
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()
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理窗口大小变化(带防抖)
|
// 处理窗口大小变化(带防抖)
|
||||||
@@ -352,6 +703,7 @@ const featureMap = {
|
|||||||
name: "谛听多维报告",
|
name: "谛听多维报告",
|
||||||
component: defineAsyncComponent(() => import("@/ui/CDWBG8B4D/index.vue")),
|
component: defineAsyncComponent(() => import("@/ui/CDWBG8B4D/index.vue")),
|
||||||
},
|
},
|
||||||
|
|
||||||
// 谛听多维报告拆分模块
|
// 谛听多维报告拆分模块
|
||||||
DWBG8B4D_Overview: {
|
DWBG8B4D_Overview: {
|
||||||
name: "报告概览",
|
name: "报告概览",
|
||||||
@@ -373,10 +725,10 @@ const featureMap = {
|
|||||||
name: "逾期风险综述",
|
name: "逾期风险综述",
|
||||||
component: defineAsyncComponent(() => import("@/ui/CDWBG8B4D/components/OverdueRiskSection.vue")),
|
component: defineAsyncComponent(() => import("@/ui/CDWBG8B4D/components/OverdueRiskSection.vue")),
|
||||||
},
|
},
|
||||||
// DWBG8B4D_CourtInfo: {
|
DWBG8B4D_CourtInfo: {
|
||||||
// name: "法院曝光台信息",
|
name: "法院曝光台信息",
|
||||||
// component: defineAsyncComponent(() => import("@/ui/CDWBG8B4D/components/MultCourtInfoSection.vue")),
|
component: defineAsyncComponent(() => import("@/ui/CDWBG8B4D/components/MultCourtInfoSection.vue")),
|
||||||
// },
|
},
|
||||||
DWBG8B4D_LoanEvaluation: {
|
DWBG8B4D_LoanEvaluation: {
|
||||||
name: "借贷评估",
|
name: "借贷评估",
|
||||||
component: defineAsyncComponent(() => import("@/ui/CDWBG8B4D/components/LoanEvaluationSection.vue")),
|
component: defineAsyncComponent(() => import("@/ui/CDWBG8B4D/components/LoanEvaluationSection.vue")),
|
||||||
@@ -396,7 +748,7 @@ const featureMap = {
|
|||||||
JRZQ4B6C: {
|
JRZQ4B6C: {
|
||||||
name: "信贷表现",
|
name: "信贷表现",
|
||||||
component: defineAsyncComponent(() => import("@/ui/JRZQ4B6C/index.vue")),
|
component: defineAsyncComponent(() => import("@/ui/JRZQ4B6C/index.vue")),
|
||||||
remark: '信贷表现主要为企业在背景调查过程中探查用户近期信贷表现时提供参考,帮助企业对其内部员工、外部业务进行个人信用过滤。数据来源于多个征信机构,可能存在数据延迟或不完整的情况。'
|
remark: '信贷表现主要为企业在背景调查过程中探查用户近期信贷表现时提供参考,帮助企业对其内部员工、外部业务进行个人风险过滤。数据来源于多个征信机构,可能存在数据延迟或不完整的情况。'
|
||||||
},
|
},
|
||||||
JRZQ09J8: {
|
JRZQ09J8: {
|
||||||
name: "收入评估",
|
name: "收入评估",
|
||||||
@@ -407,7 +759,7 @@ const featureMap = {
|
|||||||
DWBG6A2C: {
|
DWBG6A2C: {
|
||||||
name: "司南报告",
|
name: "司南报告",
|
||||||
component: defineAsyncComponent(() => import("@/ui/DWBG6A2C/index.vue")),
|
component: defineAsyncComponent(() => import("@/ui/DWBG6A2C/index.vue")),
|
||||||
remark: '司南报告提供全面的个人信用风险评估,包括身份核验、风险名单、借贷行为、履约情况等多维度分析。'
|
remark: '司南报告提供全面的个人风险风险评估,包括身份核验、风险名单、借贷行为、履约情况等多维度分析。'
|
||||||
},
|
},
|
||||||
// 司南报告拆分模块
|
// 司南报告拆分模块
|
||||||
// DWBG6A2C_BaseInfo: {
|
// DWBG6A2C_BaseInfo: {
|
||||||
@@ -462,10 +814,10 @@ const featureMap = {
|
|||||||
name: "关联风险监督",
|
name: "关联风险监督",
|
||||||
component: defineAsyncComponent(() => import("@/ui/DWBG6A2C/components/RiskSupervisionSection.vue")),
|
component: defineAsyncComponent(() => import("@/ui/DWBG6A2C/components/RiskSupervisionSection.vue")),
|
||||||
},
|
},
|
||||||
// DWBG6A2C_CourtRiskInfo: {
|
DWBG6A2C_CourtRiskInfo: {
|
||||||
// name: "法院风险信息",
|
name: "法院风险信息",
|
||||||
// component: defineAsyncComponent(() => import("@/ui/DWBG6A2C/components/CourtRiskInfoSection.vue")),
|
component: defineAsyncComponent(() => import("@/ui/DWBG6A2C/components/CourtRiskInfoSection.vue")),
|
||||||
// },
|
},
|
||||||
// 贷款风险报告
|
// 贷款风险报告
|
||||||
JRZQ5E9F: {
|
JRZQ5E9F: {
|
||||||
name: "贷款风险评估",
|
name: "贷款风险评估",
|
||||||
@@ -549,13 +901,26 @@ const featureMap = {
|
|||||||
component: defineAsyncComponent(() => import("@/ui/YYSY7D3E/index.vue")),
|
component: defineAsyncComponent(() => import("@/ui/YYSY7D3E/index.vue")),
|
||||||
remark: '手机携号转网查询用于检测用户手机号码是否发生过携号转网操作,以及转网前后的运营商信息。携号转网可能影响用户身份验证和信用评估。'
|
remark: '手机携号转网查询用于检测用户手机号码是否发生过携号转网操作,以及转网前后的运营商信息。携号转网可能影响用户身份验证和信用评估。'
|
||||||
},
|
},
|
||||||
|
// 借贷意向A
|
||||||
|
JRZQ6F2A: {
|
||||||
|
name: "借贷申请",
|
||||||
|
component: defineAsyncComponent(() => import("@/ui/JRZQ6F2A/index.vue")),
|
||||||
|
remark: '【非银行类贷款】:主要有持牌网络小贷、持牌小贷、持牌消费金融、持牌融资租赁、持牌汽车金融、其他。\n【持牌网络小贷】:小额贷款公司拥有相关牌照和许可证,可以通过互联网为用户提供贷款服务。贷款服务包括抵押贷款、消费贷款等。\n【持牌小贷机构】:是指经国家金融管理部门批准设立并颁发许可证,具备合法资质,可依法在注册地所在省市开展小额贷款业务的非银行类金融机构。\n【持牌消费金融】:是指经银监会批准,在中华人民共和国境内设立的,不吸收公众存款,以小额、分散为原则,为中国境内居民个人提供以消费为目的的贷款的非银行金融机构。\n【持牌融资租赁】:是指持有金融牌照和许可证进行的融资租赁业务的融资租赁机构。\n【持牌汽车金融机构】:是经过国家金融监督管理总局批准设立的、专门提供汽车金融服务的非银行金融机构。主要职责是为中国境内的汽车购买者及销售者提供金融服务,包括但不限于购车贷款、经销商贷款、汽车融资租赁等。\n【其他】:指除以上分类的其他非银行类贷款机构。'
|
||||||
|
},
|
||||||
// 手机在网时长
|
// 手机在网时长
|
||||||
YYSY8B1C: {
|
YYSY8B1C: {
|
||||||
name: "手机在网时长",
|
name: "手机在网时长",
|
||||||
component: defineAsyncComponent(() => import("@/ui/YYSY8B1C/index.vue")),
|
component: defineAsyncComponent(() => import("@/ui/YYSY8B1C/index.vue")),
|
||||||
remark: '手机在网时长查询用于检测用户手机号码的在网使用时长。在网时长越长,通常表示用户身份越稳定,信用风险越低。需要注意的是,如果手机号码存在携号转网的情况,那么在网时长会从转网的时候重新计算,转网前的在网时长不计入当前在网时长。建议结合手机携号转网查询结果进行综合评估。'
|
remark: '手机在网时长查询用于检测用户手机号码的在网使用时长。在网时长越长,通常表示用户身份越稳定,信用风险越低。需要注意的是,如果手机号码存在携号转网的情况,那么在网时长会从转网的时候重新计算,转网前的在网时长不计入当前在网时长。建议结合手机携号转网查询结果进行综合评估。'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 支付行为指数
|
||||||
|
JRZQ3C9R: {
|
||||||
|
name: "支付表现",
|
||||||
|
component: defineAsyncComponent(() => import("@/ui/JRZQ3C9R/index.vue")),
|
||||||
|
remark: '支付表现是指数基于近两年的查验记录、还款成功与失败表现以及余额不足情况,对用户支付与还款习惯进行量化评分,用于评估其支付稳定性与违约风险。'
|
||||||
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const maskValue = computed(() => {
|
const maskValue = computed(() => {
|
||||||
@@ -627,86 +992,89 @@ const maskValue = computed(() => {
|
|||||||
// ==================== 新评分系统 ====================
|
// ==================== 新评分系统 ====================
|
||||||
// Feature 风险等级配置(权重越高表示风险越大,最终分数越高越安全)
|
// Feature 风险等级配置(权重越高表示风险越大,最终分数越高越安全)
|
||||||
const featureRiskLevels = {
|
const featureRiskLevels = {
|
||||||
// 🔴 高风险类 - 权重 10
|
// 🔴 极高风险类 - 权重 60(有高风险直接扣约70-80分)
|
||||||
'FLXG0V4B': 20, // 司法涉诉
|
'FLXG0V4B': 50, // 司法涉诉
|
||||||
'FLXG7E8F': 20, // 个人涉诉
|
'FLXG3D56': 50, // 违约失信
|
||||||
'FLXG3D56': 10, // 违约失信
|
'JRZQ8A2D': 50, // 特殊名单验证
|
||||||
'FLXGDEA9': 18, // 本人不良
|
'FLXG7E8F': 55, // 个人涉诉
|
||||||
'JRZQ4AA8': 10, // 还款压力
|
'FLXGDEA9': 55, // 本人不良
|
||||||
|
|
||||||
// 🟠 中高风险类 - 权重 7
|
// 🟡 中高风险类 - 权重 20-30
|
||||||
'JRZQ0A03': 7, // 借贷申请记录
|
'JRZQ4AA8': 30, // 还款压力
|
||||||
'JRZQ8203': 7, // 借贷行为记录
|
'JRZQ0A03': 20, // 借贷申请记录
|
||||||
'JRZQ4B6C': 7, // 信贷表现
|
'JRZQ8203': 35, // 借贷行为记录
|
||||||
'BehaviorRiskScan': 7, // 风险行为扫描
|
'JRZQ4B6C': 35, // 信贷表现
|
||||||
'IVYZ8I9J': 7, // 网络社交异常
|
'JRZQ6F2A': 20, // 借贷申请
|
||||||
'JRZQ8A2D': 9, // 特殊名单验证
|
'BehaviorRiskScan': 60, // 风险行为扫描
|
||||||
'JRZQ7F1A': 8, // 全景雷达
|
'IVYZ8I9J': 45, // 网络社交异常
|
||||||
'JRZQ7F1A_ApplyReport': 3,
|
'JRZQ7F1A': 40, // 全景雷达
|
||||||
'JRZQ7F1A_BehaviorReport': 3,
|
'JRZQ7F1A_ApplyReport': 20,
|
||||||
'JRZQ7F1A_BigDataReport': 2,
|
'JRZQ7F1A_BehaviorReport': 20,
|
||||||
'YYSY7D3E': 5, // 手机携号转网
|
'JRZQ7F1A_BigDataReport': 15,
|
||||||
'YYSY8B1C': 5, // 手机在网时长
|
'DWBG7F3A': 45, // 多头借贷
|
||||||
'DWBG7F3A': 8, // 多头借贷
|
'YYSY7D3E': 15, // 手机携号转网
|
||||||
|
'YYSY8B1C': 15, // 手机在网时长
|
||||||
|
|
||||||
|
// 🟢 中风险类 - 权重 8-12
|
||||||
|
'QYGL3F8E': 10, // 人企关系加强版
|
||||||
|
'QCXG7A2B': 10, // 名下车辆
|
||||||
|
'JRZQ09J8': 10, // 收入评估
|
||||||
|
'JRZQ3C9R': 10, // 支付行为指数
|
||||||
|
|
||||||
// 🟡 中风险类 - 权重 5
|
// 🔵 低风险类 - 权重 3-5
|
||||||
'QYGL3F8E': 5, // 人企关系加强版
|
'IVYZ5733': 4, // 婚姻状态
|
||||||
'QCXG7A2B': 5, // 名下车辆
|
'IVYZ9A2B': 4, // 学历信息
|
||||||
'JRZQ09J8': 5, // 收入评估
|
'IVYZ3P9M': 4, // 学历信息查询(实时版)
|
||||||
|
|
||||||
// 🔵 低风险类 - 权重 3
|
|
||||||
'IVYZ5733': 3, // 婚姻状态
|
|
||||||
'IVYZ9A2B': 3, // 学历信息
|
|
||||||
'IVYZ3P9M': 3, // 学历信息查询(实时版)
|
|
||||||
|
|
||||||
// 📊 复合报告类 - 按子模块动态计算
|
// 📊 复合报告类 - 按子模块动态计算
|
||||||
'DWBG8B4D': 0, // 谛听多维报告(由子模块计算)
|
'DWBG8B4D': 0, // 谛听多维报告(由子模块计算)
|
||||||
'DWBG6A2C': 0, // 司南报告(由子模块计算)
|
'DWBG6A2C': 0, // 司南报告(由子模块计算)
|
||||||
'JRZQ5E9F': 0, // 贷款风险评估(由子模块计算)
|
'JRZQ5E9F': 0, // 贷款风险评估(由子模块计算)
|
||||||
// 谛听多维报告子模块
|
// 谛听多维报告子模块
|
||||||
'DWBG8B4D_Overview': 10,
|
'DWBG8B4D_Overview': 30,
|
||||||
'DWBG8B4D_ElementVerification': 4,
|
'DWBG8B4D_ElementVerification': 10,
|
||||||
'DWBG8B4D_Identity': 4,
|
'DWBG8B4D_Identity': 10,
|
||||||
'DWBG8B4D_RiskWarning': 10,
|
'DWBG8B4D_RiskWarning': 35,
|
||||||
'DWBG8B4D_OverdueRisk': 9,
|
'DWBG8B4D_OverdueRisk': 30,
|
||||||
'DWBG8B4D_LoanEvaluation': 7,
|
'DWBG8B4D_LoanEvaluation': 40,
|
||||||
'DWBG8B4D_LeasingRisk': 6,
|
'DWBG8B4D_LeasingRisk': 18,
|
||||||
'DWBG8B4D_RiskSupervision': 8,
|
'DWBG8B4D_RiskSupervision': 25,
|
||||||
'DWBG8B4D_RiskWarningTab': 9,
|
'DWBG8B4D_RiskWarningTab': 30,
|
||||||
|
'DWBG8B4D_CourtInfo':33,
|
||||||
|
|
||||||
// 司南报告子模块
|
// 司南报告子模块
|
||||||
'DWBG6A2C_StandLiveInfo': 4,
|
'DWBG6A2C_StandLiveInfo': 10,
|
||||||
'DWBG6A2C_RiskPoint': 9,
|
'DWBG6A2C_RiskPoint': 28,
|
||||||
'DWBG6A2C_SecurityInfo': 15,
|
'DWBG6A2C_SecurityInfo': 45,
|
||||||
'DWBG6A2C_AntiFraudInfo': 15,
|
'DWBG6A2C_AntiFraudInfo': 45,
|
||||||
'DWBG6A2C_RiskList': 12,
|
'DWBG6A2C_RiskList': 38,
|
||||||
'DWBG6A2C_ApplicationStatistics': 7,
|
'DWBG6A2C_ApplicationStatistics': 40,
|
||||||
'DWBG6A2C_LendingStatistics': 6,
|
'DWBG6A2C_LendingStatistics': 35,
|
||||||
'DWBG6A2C_PerformanceStatistics': 7,
|
'DWBG6A2C_PerformanceStatistics': 22,
|
||||||
'DWBG6A2C_OverdueRecord': 9,
|
'DWBG6A2C_OverdueRecord': 28,
|
||||||
'DWBG6A2C_CreditDetail': 5,
|
'DWBG6A2C_CreditDetail': 15,
|
||||||
'DWBG6A2C_RentalBehavior': 5,
|
'DWBG6A2C_RentalBehavior': 15,
|
||||||
'DWBG6A2C_RiskSupervision': 8,
|
'DWBG6A2C_RiskSupervision': 25,
|
||||||
|
'DWBG6A2C_CourtRiskInfo':39,
|
||||||
|
|
||||||
// 贷款风险评估子模块
|
// 贷款风险评估子模块
|
||||||
'CJRZQ5E9F_RiskOverview': 8,
|
'CJRZQ5E9F_RiskOverview': 25,
|
||||||
'CJRZQ5E9F_CreditScores': 7,
|
'CJRZQ5E9F_CreditScores': 22,
|
||||||
'CJRZQ5E9F_LoanBehaviorAnalysis': 7,
|
'CJRZQ5E9F_LoanBehaviorAnalysis': 40,
|
||||||
'CJRZQ5E9F_InstitutionAnalysis': 5,
|
'CJRZQ5E9F_InstitutionAnalysis': 15,
|
||||||
'CJRZQ5E9F_TimeTrendAnalysis': 6,
|
'CJRZQ5E9F_TimeTrendAnalysis': 18,
|
||||||
'CJRZQ5E9F_RiskIndicators': 8,
|
'CJRZQ5E9F_RiskIndicators': 25,
|
||||||
'CJRZQ5E9F_RiskAdvice': 2,
|
'CJRZQ5E9F_RiskAdvice': 6,
|
||||||
|
|
||||||
// 人企关系加强版子模块
|
// 人企关系加强版子模块
|
||||||
'CQYGL3F8E_Investment': 4,
|
'CQYGL3F8E_Investment': 12,
|
||||||
'CQYGL3F8E_SeniorExecutive': 4,
|
'CQYGL3F8E_SeniorExecutive': 12,
|
||||||
'CQYGL3F8E_Lawsuit': 8,
|
'CQYGL3F8E_Lawsuit': 25,
|
||||||
'CQYGL3F8E_InvestHistory': 3,
|
'CQYGL3F8E_InvestHistory': 8,
|
||||||
'CQYGL3F8E_FinancingHistory': 3,
|
'CQYGL3F8E_FinancingHistory': 8,
|
||||||
'CQYGL3F8E_Punishment': 7,
|
'CQYGL3F8E_Punishment': 22,
|
||||||
'CQYGL3F8E_Abnormal': 6,
|
'CQYGL3F8E_Abnormal': 18,
|
||||||
'CQYGL3F8E_TaxRisk': 7,
|
'CQYGL3F8E_TaxRisk': 22,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 存储每个组件的 ref 引用
|
// 存储每个组件的 ref 引用
|
||||||
@@ -726,6 +1094,19 @@ defineExpose({
|
|||||||
notifyRiskStatus
|
notifyRiskStatus
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 根据身份证号生成确定性偏移(-10 ~ +10),同一人每次结果一致
|
||||||
|
const getDeterministicOffset = (idCardStr) => {
|
||||||
|
if (!idCardStr || typeof idCardStr !== 'string') return 0;
|
||||||
|
let h = 0;
|
||||||
|
for (let i = 0; i < idCardStr.length; i++) {
|
||||||
|
h = ((h << 5) - h) + idCardStr.charCodeAt(i);
|
||||||
|
h = h & h;
|
||||||
|
}
|
||||||
|
const abs = Math.abs(h);
|
||||||
|
// 0~20 映射为 -10~10
|
||||||
|
return (abs % 21) - 10;
|
||||||
|
};
|
||||||
|
|
||||||
// 计算综合评分的函数(分数越高越安全)
|
// 计算综合评分的函数(分数越高越安全)
|
||||||
const calculateScore = () => {
|
const calculateScore = () => {
|
||||||
// 收集实际存在的 features 及其风险权重
|
// 收集实际存在的 features 及其风险权重
|
||||||
@@ -748,7 +1129,12 @@ const calculateScore = () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (presentFeatures.length === 0) return 100; // 无有效特征时返回满分(最安全)
|
if (presentFeatures.length === 0) {
|
||||||
|
// 无有效特征时给 20-80 范围的基础分 50,再 ±10 得到最终分(10-90)
|
||||||
|
const base = 50; // 已在 20-80 内
|
||||||
|
const offset = getDeterministicOffset(scoreIdCard.value);
|
||||||
|
return Math.max(10, Math.min(90, Math.round(base + offset)));
|
||||||
|
}
|
||||||
|
|
||||||
// 累计总风险分数
|
// 累计总风险分数
|
||||||
let totalRiskScore = 0;
|
let totalRiskScore = 0;
|
||||||
@@ -763,11 +1149,7 @@ const calculateScore = () => {
|
|||||||
const componentRisk = 100 - componentScore;
|
const componentRisk = 100 - componentScore;
|
||||||
|
|
||||||
// 计算该模块的风险贡献(固定分值,不按占比)
|
// 计算该模块的风险贡献(固定分值,不按占比)
|
||||||
// 使用权重系数放大高风险模块的影响
|
const weightMultiplier = 1.2;
|
||||||
// 高风险模块(权重10)如果风险分数是0,扣20分(权重10 × 系数2)
|
|
||||||
// 中风险模块(权重7)如果风险分数是0,扣14分(权重7 × 系数2)
|
|
||||||
// 低风险模块(权重3)如果风险分数是0,扣6分(权重3 × 系数2)
|
|
||||||
const weightMultiplier = 1.5; // 权重系数,可以调整这个值来控制影响程度
|
|
||||||
const riskContribution = (componentRisk / 100) * weight * weightMultiplier;
|
const riskContribution = (componentRisk / 100) * weight * weightMultiplier;
|
||||||
|
|
||||||
riskDetails.push({
|
riskDetails.push({
|
||||||
@@ -780,22 +1162,44 @@ const calculateScore = () => {
|
|||||||
hasStatus: key in componentRiskScores.value
|
hasStatus: key in componentRiskScores.value
|
||||||
});
|
});
|
||||||
|
|
||||||
// 累加风险分数
|
|
||||||
totalRiskScore += riskContribution;
|
totalRiskScore += riskContribution;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 将总风险分数限制在 0-90 范围内(确保最低分为10分)
|
// 总风险分数限制在 0-86,得到安全分数 14-100
|
||||||
const finalRiskScore = Math.max(0, Math.min(90, Math.round(totalRiskScore)));
|
const finalRiskScore = Math.max(0, Math.min(86, Math.round(totalRiskScore)));
|
||||||
|
|
||||||
// 转换为安全分数:分数越高越安全(100 - 风险分数)
|
|
||||||
// 最终分数范围:10-100分
|
|
||||||
const safetyScore = 100 - finalRiskScore;
|
const safetyScore = 100 - finalRiskScore;
|
||||||
|
|
||||||
return safetyScore;
|
// 先得到 20-80 范围分(基础分)
|
||||||
|
const baseScore = 20 + (safetyScore - 10) * (60 / 90);
|
||||||
|
const rangeScore = Math.max(20, Math.min(80, Math.round(baseScore)));
|
||||||
|
|
||||||
|
// 再按身份证号(或报告唯一键)确定性 ±10,最终 10-90,同一人/同一报告每次相同
|
||||||
|
const offset = getDeterministicOffset(scoreIdCard.value);
|
||||||
|
const finalScore = Math.max(10, Math.min(90, rangeScore + offset));
|
||||||
|
return finalScore;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 监听 reportData 和 componentRiskScores 变化并计算评分
|
// 监听报告数据变化,重新检查模块状态
|
||||||
watch([reportData, componentRiskScores], () => {
|
watch([reportData, isDone], async () => {
|
||||||
|
if (isDone.value && isAgentOrder.value && idCard.value && !isExample.value) {
|
||||||
|
// 强制刷新状态(清除缓存,重新检查)
|
||||||
|
featureOfflineStatus.value.clear()
|
||||||
|
await checkAllFeaturesStatus();
|
||||||
|
}
|
||||||
|
}, { immediate: false });
|
||||||
|
|
||||||
|
// 监听路由变化,如果从支付结果页返回,刷新状态
|
||||||
|
const route = useRoute()
|
||||||
|
watch(() => route.path, (newPath, oldPath) => {
|
||||||
|
// 如果从支付结果页返回到报告页,刷新下架状态
|
||||||
|
if (oldPath === '/payment/result' && newPath === '/report' && isDone.value && isAgentOrder.value && idCard.value && !isExample.value) {
|
||||||
|
featureOfflineStatus.value.clear()
|
||||||
|
checkAllFeaturesStatus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听 reportData、componentRiskScores、scoreIdCard 变化并计算评分(scoreIdCard 影响确定性偏移)
|
||||||
|
watch([reportData, componentRiskScores, scoreIdCard], () => {
|
||||||
reportScore.value = calculateScore();
|
reportScore.value = calculateScore();
|
||||||
|
|
||||||
// 将评分系统数据整理到一个对象中
|
// 将评分系统数据整理到一个对象中
|
||||||
@@ -852,7 +1256,12 @@ watch([reportData, componentRiskScores], () => {
|
|||||||
</van-tab>
|
</van-tab>
|
||||||
<van-tab v-for="(item, index) in processedReportData" :key="`${item.data.apiID}_${index}`"
|
<van-tab v-for="(item, index) in processedReportData" :key="`${item.data.apiID}_${index}`"
|
||||||
:title="featureMap[item.data.apiID]?.name">
|
:title="featureMap[item.data.apiID]?.name">
|
||||||
<TitleBanner :id="item.data.apiID" class="mb-4">
|
<TitleBanner :id="item.data.apiID" class="mb-4"
|
||||||
|
:show-offline-button="isAgentOrder && idCard && !isExample && getFeatureStatus(item.data.apiID).whitelistPrice >= 0"
|
||||||
|
:whitelist-price="getFeatureStatus(item.data.apiID).whitelistPrice"
|
||||||
|
:is-submitting="getFeatureStatus(item.data.apiID).isSubmitting"
|
||||||
|
:is-offlined="getFeatureStatus(item.data.apiID).isOfflined"
|
||||||
|
@offline-click="handleOfflineClick(item.data.apiID, featureMap[item.data.apiID]?.name)">
|
||||||
{{ featureMap[item.data.apiID]?.name }}
|
{{ featureMap[item.data.apiID]?.name }}
|
||||||
</TitleBanner>
|
</TitleBanner>
|
||||||
<component :is="featureMap[item.data.apiID]?.component" :ref="el => {
|
<component :is="featureMap[item.data.apiID]?.component" :ref="el => {
|
||||||
@@ -879,7 +1288,7 @@ watch([reportData, componentRiskScores], () => {
|
|||||||
1、本份报告是在取得您个人授权后,我们才向合法存有您以上个人信息的机构去调取相关内容,我们不会以任何形式对您的报告进行存储,除您和您授权的人外不会提供给任何人和机构进行查看。
|
1、本份报告是在取得您个人授权后,我们才向合法存有您以上个人信息的机构去调取相关内容,我们不会以任何形式对您的报告进行存储,除您和您授权的人外不会提供给任何人和机构进行查看。
|
||||||
</p>
|
</p>
|
||||||
<p class="text-[#999999]">
|
<p class="text-[#999999]">
|
||||||
2、本报告自生成之日起,有效期 30
|
2、本报告自生成之日起,有效期 {{ useAppStore().queryRetentionDays || 30 }}
|
||||||
天,过期自动删除。如果您对本份报告存有异议,可能是合作机构数据有延迟或未能获取到您的相关数据,出于合作平台数据隐私的保护,本平台将不做任何解释。
|
天,过期自动删除。如果您对本份报告存有异议,可能是合作机构数据有延迟或未能获取到您的相关数据,出于合作平台数据隐私的保护,本平台将不做任何解释。
|
||||||
</p>
|
</p>
|
||||||
<p class="text-[#999999]">
|
<p class="text-[#999999]">
|
||||||
@@ -894,19 +1303,57 @@ watch([reportData, componentRiskScores], () => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="disclaimer">
|
<div class="disclaimer">
|
||||||
<div class="flex flex-col items-center">
|
<div class="flex flex-col items-center">
|
||||||
<div class="flex items-center">
|
<!-- <div class="flex items-center">
|
||||||
<img class="w-4 h-4 mr-2" src="@/assets/images/public_security_record_icon.png" alt="公安备案" />
|
<img class="w-4 h-4 mr-2" src="@/assets/images/public_security_record_icon.png" alt="公安备案" />
|
||||||
<text>琼公网安备46010002000584号</text>
|
<text>琼公网安备46010002000584号</text>
|
||||||
</div>
|
</div> -->
|
||||||
<div>
|
<div>
|
||||||
<a class="text-blue-500" href="https://beian.miit.gov.cn">
|
<a class="text-blue-500" href="https://beian.miit.gov.cn">
|
||||||
琼ICP备2024048057号-2
|
琼ICP备2024038584号-10
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>海南省学宇思网络科技有限公司版权所有</div>
|
<div>海南海宇大数据有限公司版权所有</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 屏蔽模块弹窗(已废弃,保留用于兼容) -->
|
||||||
|
<WhitelistModuleDialog v-if="!isShare && isDiamond" v-model:show="showWhitelistDialog" :id-card="idCard"
|
||||||
|
:order-id="orderId" @success="onWhitelistSuccess" />
|
||||||
|
|
||||||
|
<!-- 下架确认弹窗 -->
|
||||||
|
<van-popup v-model:show="showOfflineConfirmDialog" round position="center"
|
||||||
|
:style="{ width: '85%', borderRadius: '20px' }" :overlay-style="{ backgroundColor: 'rgba(0,0,0,0.4)' }">
|
||||||
|
<div class="p-6 bg-white">
|
||||||
|
<div class="text-center space-y-4">
|
||||||
|
<h3 class="text-lg font-bold text-gray-800">确认下架模块</h3>
|
||||||
|
<div v-if="currentOfflineFeature" class="space-y-2">
|
||||||
|
<p class="text-gray-600">
|
||||||
|
模块名称:<span class="font-medium">{{ currentOfflineFeature.featureName }}</span>
|
||||||
|
</p>
|
||||||
|
<p class="text-gray-600">
|
||||||
|
下架费用:<span class="text-red-500 font-bold text-lg">¥{{
|
||||||
|
currentOfflineFeature.whitelistPrice.toFixed(2) }}</span>
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
下架后,该身份证号查询时将不显示此模块的数据
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3 mt-6">
|
||||||
|
<van-button block round @click="showOfflineConfirmDialog = false">取消</van-button>
|
||||||
|
<van-button type="primary" block round
|
||||||
|
:loading="currentOfflineFeature && getFeatureStatus(currentOfflineFeature.featureApiId).isSubmitting"
|
||||||
|
@click="confirmOffline">
|
||||||
|
确认下架
|
||||||
|
</van-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</van-popup>
|
||||||
|
|
||||||
|
<!-- 白名单下架支付弹窗(复用查询报告支付组件) -->
|
||||||
|
<Payment v-if="whitelistPaymentData.sell_price > 0" v-model="showWhitelistPayment" :data="whitelistPaymentData"
|
||||||
|
:id="whitelistPaymentId" :type="whitelistPaymentType" :return-url="getCurrentReportUrl()" />
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -1,33 +1,74 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, nextTick } from "vue";
|
import { ref, computed, nextTick, onMounted } from "vue";
|
||||||
|
import { useRouter, useRoute } from "vue-router";
|
||||||
import { useDialogStore } from "@/stores/dialogStore";
|
import { useDialogStore } from "@/stores/dialogStore";
|
||||||
|
import { useAgentStore } from "@/stores/agentStore";
|
||||||
|
import { useUserStore } from "@/stores/userStore";
|
||||||
|
import { showToast } from "vant";
|
||||||
|
import useApiFetch from "@/composables/useApiFetch";
|
||||||
|
import { useAliyunCaptcha } from "@/composables/useAliyunCaptcha";
|
||||||
|
import { registerByInviteCode } from "@/api/agent";
|
||||||
|
|
||||||
const emit = defineEmits(['bind-success'])
|
const emit = defineEmits(['register-success'])
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
const dialogStore = useDialogStore();
|
const dialogStore = useDialogStore();
|
||||||
const agentStore = useAgentStore();
|
const agentStore = useAgentStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
const { runWithCaptcha } = useAliyunCaptcha();
|
||||||
const phoneNumber = ref("");
|
const phoneNumber = ref("");
|
||||||
const verificationCode = ref("");
|
const verificationCode = ref("");
|
||||||
|
const inviteCode = ref("");
|
||||||
const isCountingDown = ref(false);
|
const isCountingDown = ref(false);
|
||||||
const countdown = ref(60);
|
const countdown = ref(60);
|
||||||
const isAgreed = ref(false);
|
const isAgreed = ref(false);
|
||||||
|
const hasAccount = ref(false); // 是否有平台账号
|
||||||
let timer = null;
|
let timer = null;
|
||||||
|
|
||||||
// 聚焦状态变量
|
// 聚焦状态变量
|
||||||
const phoneFocused = ref(false);
|
const phoneFocused = ref(false);
|
||||||
const codeFocused = ref(false);
|
const codeFocused = ref(false);
|
||||||
|
const inviteFocused = ref(false);
|
||||||
|
|
||||||
|
// 从URL参数中读取邀请码并自动填入
|
||||||
|
onMounted(() => {
|
||||||
|
const inviteCodeParam = route.query.invite_code;
|
||||||
|
if (inviteCodeParam) {
|
||||||
|
inviteCode.value = inviteCodeParam;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果用户已登录且有手机号,自动填充手机号
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
if (token && userStore.mobile) {
|
||||||
|
phoneNumber.value = userStore.mobile;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const isPhoneNumberValid = computed(() => {
|
const isPhoneNumberValid = computed(() => {
|
||||||
return /^1[3-9]\d{9}$/.test(phoneNumber.value);
|
return /^1[3-9]\d{9}$/.test(phoneNumber.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
const canBind = computed(() => {
|
const isInviteCodeValid = computed(() => {
|
||||||
return (
|
return inviteCode.value.trim().length > 0;
|
||||||
isPhoneNumberValid.value &&
|
});
|
||||||
verificationCode.value.length === 6 &&
|
|
||||||
isAgreed.value
|
const canRegister = computed(() => {
|
||||||
);
|
if (hasAccount.value) {
|
||||||
|
// 已有账号模式:只需要手机号和验证码
|
||||||
|
return (
|
||||||
|
isPhoneNumberValid.value &&
|
||||||
|
verificationCode.value.length === 6 &&
|
||||||
|
isAgreed.value
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// 新注册模式:需要手机号、验证码和邀请码
|
||||||
|
return (
|
||||||
|
isPhoneNumberValid.value &&
|
||||||
|
verificationCode.value.length === 6 &&
|
||||||
|
isInviteCodeValid.value &&
|
||||||
|
isAgreed.value
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function sendVerificationCode() {
|
async function sendVerificationCode() {
|
||||||
@@ -36,25 +77,30 @@ async function sendVerificationCode() {
|
|||||||
showToast({ message: "请输入有效的手机号" });
|
showToast({ message: "请输入有效的手机号" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { data, error } = await useApiFetch("auth/sendSms")
|
if (!hasAccount.value && !isInviteCodeValid.value) {
|
||||||
.post({ mobile: phoneNumber.value, actionType: "bindMobile" })
|
showToast({ message: "请先输入邀请码" });
|
||||||
.json();
|
return;
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
const actionType = hasAccount.value ? "bindMobile" : "agentApply";
|
||||||
|
runWithCaptcha(
|
||||||
|
(captchaVerifyParam) => useApiFetch("auth/sendSms")
|
||||||
|
.post({ mobile: phoneNumber.value, actionType, captchaVerifyParam })
|
||||||
|
.json(),
|
||||||
|
(result) => {
|
||||||
|
if (result && result.code === 200) {
|
||||||
|
showToast({ message: "获取成功" });
|
||||||
|
startCountdown();
|
||||||
|
nextTick(() => {
|
||||||
|
const verificationCodeInput = document.getElementById('registerVerificationCode');
|
||||||
|
if (verificationCodeInput) {
|
||||||
|
verificationCodeInput.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (result) {
|
||||||
|
showToast(result.msg || "发送失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function startCountdown() {
|
function startCountdown() {
|
||||||
@@ -70,7 +116,7 @@ function startCountdown() {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleBind() {
|
async function handleRegister() {
|
||||||
if (!isPhoneNumberValid.value) {
|
if (!isPhoneNumberValid.value) {
|
||||||
showToast({ message: "请输入有效的手机号" });
|
showToast({ message: "请输入有效的手机号" });
|
||||||
return;
|
return;
|
||||||
@@ -79,48 +125,117 @@ async function handleBind() {
|
|||||||
showToast({ message: "请输入有效的验证码" });
|
showToast({ message: "请输入有效的验证码" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!hasAccount.value && !isInviteCodeValid.value) {
|
||||||
|
showToast({ message: "请输入邀请码" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!isAgreed.value) {
|
if (!isAgreed.value) {
|
||||||
showToast({ message: "请先同意用户协议" });
|
showToast({ message: "请先同意用户协议" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data, error } = await useApiFetch("/user/bindMobile")
|
try {
|
||||||
.post({ mobile: phoneNumber.value, code: verificationCode.value })
|
if (hasAccount.value) {
|
||||||
.json();
|
// 已有账号模式:绑定手机号登录
|
||||||
|
const { data, error } = await useApiFetch("/user/bindMobile")
|
||||||
|
.post({ mobile: phoneNumber.value, code: verificationCode.value })
|
||||||
|
.json();
|
||||||
|
|
||||||
if (data.value && !error.value) {
|
if (data.value && !error.value) {
|
||||||
if (data.value.code === 200) {
|
if (data.value.code === 200) {
|
||||||
showToast({ message: "绑定成功" });
|
showToast({ message: "绑定成功!" });
|
||||||
localStorage.setItem('token', data.value.data.accessToken)
|
localStorage.setItem('token', data.value.data.accessToken)
|
||||||
localStorage.setItem('refreshAfter', data.value.data.refreshAfter)
|
localStorage.setItem('refreshAfter', data.value.data.refreshAfter)
|
||||||
localStorage.setItem('accessExpire', data.value.data.accessExpire)
|
localStorage.setItem('accessExpire', data.value.data.accessExpire)
|
||||||
closeDialog();
|
|
||||||
await Promise.all([
|
|
||||||
agentStore.fetchAgentStatus(),
|
|
||||||
userStore.fetchUserInfo()
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 发出绑定成功的事件
|
closeDialog();
|
||||||
emit('bind-success');
|
await Promise.all([
|
||||||
|
agentStore.fetchAgentStatus(),
|
||||||
|
userStore.fetchUserInfo()
|
||||||
|
]);
|
||||||
|
|
||||||
// 延迟执行路由检查,确保状态已更新
|
// 发出注册成功的事件
|
||||||
setTimeout(() => {
|
emit('register-success');
|
||||||
// 重新触发路由检查
|
|
||||||
const currentRoute = router.currentRoute.value;
|
// 检查是否是代理,如果是代理跳转到代理主页,否则跳转到首页
|
||||||
router.replace(currentRoute.path);
|
setTimeout(() => {
|
||||||
}, 100);
|
if (agentStore.isAgent) {
|
||||||
|
router.replace("/agent");
|
||||||
|
} else {
|
||||||
|
router.replace("/");
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
} else {
|
||||||
|
// 检查是否是手机号已绑定其他微信的错误
|
||||||
|
if (data.value.msg && data.value.msg.includes("已绑定其他微信号")) {
|
||||||
|
showToast({ message: "该手机号已绑定其他微信号,一个微信只能绑定一个手机号" });
|
||||||
|
} else {
|
||||||
|
showToast(data.value.msg || "绑定失败,请重试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
showToast(data.value.msg);
|
// 新注册模式:通过邀请码注册成为代理
|
||||||
|
const { data, error } = await registerByInviteCode({
|
||||||
|
mobile: phoneNumber.value,
|
||||||
|
code: verificationCode.value,
|
||||||
|
referrer: inviteCode.value.trim()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.value && !error.value) {
|
||||||
|
if (data.value.code === 200) {
|
||||||
|
showToast({ message: "注册成功!" });
|
||||||
|
localStorage.setItem('token', data.value.data.accessToken)
|
||||||
|
localStorage.setItem('refreshAfter', data.value.data.refreshAfter)
|
||||||
|
localStorage.setItem('accessExpire', data.value.data.accessExpire)
|
||||||
|
|
||||||
|
// 更新代理信息到store
|
||||||
|
if (data.value.data.agent_id) {
|
||||||
|
agentStore.updateAgentInfo({
|
||||||
|
isAgent: true,
|
||||||
|
agentID: data.value.data.agent_id,
|
||||||
|
level: data.value.data.level || 1,
|
||||||
|
levelName: data.value.data.level_name || '普通代理'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
closeDialog();
|
||||||
|
await Promise.all([
|
||||||
|
agentStore.fetchAgentStatus(),
|
||||||
|
userStore.fetchUserInfo()
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 发出注册成功的事件
|
||||||
|
emit('register-success');
|
||||||
|
|
||||||
|
// 跳转到代理主页
|
||||||
|
setTimeout(() => {
|
||||||
|
router.replace("/agent");
|
||||||
|
}, 300);
|
||||||
|
} else {
|
||||||
|
// 检查是否是手机号已绑定其他微信的错误
|
||||||
|
if (data.value.msg && data.value.msg.includes("已绑定其他微信号")) {
|
||||||
|
showToast({ message: "该手机号已绑定其他微信号,一个微信只能绑定一个手机号" });
|
||||||
|
} else {
|
||||||
|
showToast(data.value.msg || "注册失败,请重试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('操作失败:', err);
|
||||||
|
showToast({ message: "操作失败,请重试" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeDialog() {
|
function closeDialog() {
|
||||||
dialogStore.closeBindPhone();
|
dialogStore.closeRegisterAgent();
|
||||||
// 重置表单
|
// 重置表单
|
||||||
phoneNumber.value = "";
|
phoneNumber.value = "";
|
||||||
verificationCode.value = "";
|
verificationCode.value = "";
|
||||||
|
inviteCode.value = "";
|
||||||
isAgreed.value = false;
|
isAgreed.value = false;
|
||||||
|
hasAccount.value = false;
|
||||||
if (timer) {
|
if (timer) {
|
||||||
clearInterval(timer);
|
clearInterval(timer);
|
||||||
}
|
}
|
||||||
@@ -138,17 +253,14 @@ function toPrivacyPolicy() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="dialogStore.showBindPhone">
|
<div v-if="dialogStore.showRegisterAgent">
|
||||||
<van-popup v-model:show="dialogStore.showBindPhone" round position="bottom" :style="{ height: '80%' }"
|
<van-popup v-model:show="dialogStore.showRegisterAgent" round position="bottom" :style="{ height: '85%' }"
|
||||||
@close="closeDialog">
|
@close="closeDialog">
|
||||||
<div class="bind-phone-dialog">
|
<div class="register-agent-dialog">
|
||||||
<div class="title-bar">
|
<div class="title-bar">
|
||||||
<div class="font-bold">绑定手机号码</div>
|
<div class="font-bold">注册成为代理</div>
|
||||||
<div class="text-sm text-gray-500 mt-1">
|
<div class="text-sm text-gray-500 mt-1">
|
||||||
为使用完整功能请绑定手机号码
|
{{ hasAccount ? '绑定手机号登录已有账号' : '请输入手机号和邀请码完成代理注册' }}
|
||||||
</div>
|
|
||||||
<div class="text-sm text-gray-500 mt-1">
|
|
||||||
如该微信号之前已绑定过手机号,请输入已绑定的手机号
|
|
||||||
</div>
|
</div>
|
||||||
<van-icon name="cross" class="close-icon" @click="closeDialog" />
|
<van-icon name="cross" class="close-icon" @click="closeDialog" />
|
||||||
</div>
|
</div>
|
||||||
@@ -163,6 +275,48 @@ function toPrivacyPolicy() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-5">
|
<div class="space-y-5">
|
||||||
|
<!-- 账号类型选择 -->
|
||||||
|
<div class="flex items-center space-x-4 mb-4">
|
||||||
|
<button @click="hasAccount = false" :class="[
|
||||||
|
'flex-1 py-2 px-4 rounded-lg text-sm font-medium transition-all',
|
||||||
|
!hasAccount
|
||||||
|
? 'bg-blue-500 text-white shadow-md'
|
||||||
|
: 'bg-gray-100 text-gray-600'
|
||||||
|
]">
|
||||||
|
新注册
|
||||||
|
</button>
|
||||||
|
<button @click="hasAccount = true" :class="[
|
||||||
|
'flex-1 py-2 px-4 rounded-lg text-sm font-medium transition-all',
|
||||||
|
hasAccount
|
||||||
|
? 'bg-blue-500 text-white shadow-md'
|
||||||
|
: 'bg-gray-100 text-gray-600'
|
||||||
|
]">
|
||||||
|
已有平台账号
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 重要提示 -->
|
||||||
|
<div v-if="hasAccount" class="bg-yellow-50 border border-yellow-200 rounded-lg p-3 mb-4">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<van-icon name="warning-o" class="text-yellow-600 mr-2 mt-0.5 flex-shrink-0" />
|
||||||
|
<div class="text-xs text-yellow-800 leading-relaxed">
|
||||||
|
<p class="font-semibold mb-1">重要提示:</p>
|
||||||
|
<p>• 一个微信只能绑定一个手机号</p>
|
||||||
|
<p>• 如果该手机号已绑定其他微信号,将无法在此微信登录</p>
|
||||||
|
<p>• 请确保输入的是您已注册的手机号</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 邀请码输入(仅新注册模式显示) -->
|
||||||
|
<div v-if="!hasAccount" :class="[
|
||||||
|
'input-container bg-blue-300/20',
|
||||||
|
inviteFocused ? 'focused' : '',
|
||||||
|
]">
|
||||||
|
<input v-model="inviteCode" class="input-field" type="text" placeholder="请输入邀请码"
|
||||||
|
@focus="inviteFocused = true" @blur="inviteFocused = false" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 手机号输入 -->
|
<!-- 手机号输入 -->
|
||||||
<div :class="[
|
<div :class="[
|
||||||
'input-container bg-blue-300/20',
|
'input-container bg-blue-300/20',
|
||||||
@@ -178,20 +332,20 @@ function toPrivacyPolicy() {
|
|||||||
'input-container bg-blue-300/20',
|
'input-container bg-blue-300/20',
|
||||||
codeFocused ? 'focused' : '',
|
codeFocused ? 'focused' : '',
|
||||||
]">
|
]">
|
||||||
<input v-model="verificationCode" id="verificationCode" class="input-field"
|
<input v-model="verificationCode" id="registerVerificationCode" class="input-field"
|
||||||
placeholder="请输入验证码" maxlength="6" @focus="codeFocused = true"
|
placeholder="请输入验证码" maxlength="6" @focus="codeFocused = true"
|
||||||
@blur="codeFocused = false" />
|
@blur="codeFocused = false" />
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="ml-2 px-4 py-2 text-sm font-bold flex-shrink-0 rounded-lg transition duration-300"
|
class="ml-2 px-4 py-2 text-sm font-bold flex-shrink-0 rounded-lg transition duration-300"
|
||||||
:class="isCountingDown || !isPhoneNumberValid
|
:class="isCountingDown || !isPhoneNumberValid || (!hasAccount && !isInviteCodeValid)
|
||||||
? 'cursor-not-allowed bg-gray-300 text-gray-500'
|
? 'cursor-not-allowed bg-gray-300 text-gray-500'
|
||||||
: 'bg-blue-500 text-white hover:bg-blue-600'
|
: 'bg-blue-500 text-white hover:bg-blue-600'
|
||||||
" @click="sendVerificationCode">
|
" @click="sendVerificationCode">
|
||||||
{{
|
{{
|
||||||
isCountingDown
|
isCountingDown
|
||||||
? `${countdown}s重新获取`
|
? `${countdown}s重新获取`
|
||||||
: "获取验证码"
|
: "获取验证码"
|
||||||
}}
|
}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -200,7 +354,7 @@ function toPrivacyPolicy() {
|
|||||||
<div class="flex items-start space-x-2">
|
<div class="flex items-start space-x-2">
|
||||||
<input type="checkbox" v-model="isAgreed" class="mt-1" />
|
<input type="checkbox" v-model="isAgreed" class="mt-1" />
|
||||||
<span class="text-xs text-gray-400 leading-tight">
|
<span class="text-xs text-gray-400 leading-tight">
|
||||||
绑定手机号即代表您已阅读并同意
|
注册成为代理即代表您已阅读并同意
|
||||||
<a class="cursor-pointer text-blue-400" @click="toUserAgreement">
|
<a class="cursor-pointer text-blue-400" @click="toUserAgreement">
|
||||||
《用户协议》
|
《用户协议》
|
||||||
</a>
|
</a>
|
||||||
@@ -214,8 +368,8 @@ function toPrivacyPolicy() {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
class="mt-10 w-full py-3 text-lg font-bold text-white bg-blue-500 rounded-full transition duration-300"
|
class="mt-10 w-full py-3 text-lg font-bold text-white bg-blue-500 rounded-full transition duration-300"
|
||||||
:class="{ 'opacity-50 cursor-not-allowed': !canBind }" @click="handleBind">
|
:class="{ 'opacity-50 cursor-not-allowed': !canRegister }" @click="handleRegister">
|
||||||
确认绑定
|
{{ hasAccount ? '绑定手机号登录' : '注册成为代理' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -224,7 +378,7 @@ function toPrivacyPolicy() {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.bind-phone-dialog {
|
.register-agent-dialog {
|
||||||
background: url("@/assets/images/login_bg.png") no-repeat;
|
background: url("@/assets/images/login_bg.png") no-repeat;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
|
|||||||
271
src/components/BindPhoneOnlyDialog.vue
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed, nextTick } from "vue";
|
||||||
|
import { useDialogStore } from "@/stores/dialogStore";
|
||||||
|
import { showToast } from "vant";
|
||||||
|
import useApiFetch from "@/composables/useApiFetch";
|
||||||
|
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 phoneNumber = ref("");
|
||||||
|
const verificationCode = ref("");
|
||||||
|
const isCountingDown = ref(false);
|
||||||
|
const countdown = ref(60);
|
||||||
|
const isAgreed = ref(false);
|
||||||
|
let timer = null;
|
||||||
|
|
||||||
|
// 聚焦状态变量
|
||||||
|
const phoneFocused = ref(false);
|
||||||
|
const codeFocused = ref(false);
|
||||||
|
|
||||||
|
const isPhoneNumberValid = computed(() => {
|
||||||
|
return /^1[3-9]\d{9}$/.test(phoneNumber.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const canBind = computed(() => {
|
||||||
|
return (
|
||||||
|
isPhoneNumberValid.value &&
|
||||||
|
verificationCode.value.length === 6 &&
|
||||||
|
isAgreed.value
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function sendVerificationCode() {
|
||||||
|
if (isCountingDown.value || !isPhoneNumberValid.value) return;
|
||||||
|
if (!isPhoneNumberValid.value) {
|
||||||
|
showToast({ message: "请输入有效的手机号" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
runWithCaptcha(
|
||||||
|
(captchaVerifyParam) => useApiFetch("auth/sendSms")
|
||||||
|
.post({ mobile: phoneNumber.value, actionType: "bindMobile", captchaVerifyParam })
|
||||||
|
.json(),
|
||||||
|
(result) => {
|
||||||
|
if (result && result.code === 200) {
|
||||||
|
showToast({ message: "获取成功" });
|
||||||
|
startCountdown();
|
||||||
|
nextTick(() => {
|
||||||
|
const verificationCodeInput = document.getElementById('bindPhoneVerificationCode');
|
||||||
|
if (verificationCodeInput) {
|
||||||
|
verificationCodeInput.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (result) {
|
||||||
|
showToast(result.msg || "发送失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startCountdown() {
|
||||||
|
isCountingDown.value = true;
|
||||||
|
countdown.value = 60;
|
||||||
|
timer = setInterval(() => {
|
||||||
|
if (countdown.value > 0) {
|
||||||
|
countdown.value--;
|
||||||
|
} else {
|
||||||
|
clearInterval(timer);
|
||||||
|
isCountingDown.value = false;
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleBind() {
|
||||||
|
if (!isPhoneNumberValid.value) {
|
||||||
|
showToast({ message: "请输入有效的手机号" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (verificationCode.value.length !== 6) {
|
||||||
|
showToast({ message: "请输入有效的验证码" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isAgreed.value) {
|
||||||
|
showToast({ message: "请先同意用户协议" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, error } = await useApiFetch("/user/bindMobile")
|
||||||
|
.post({ mobile: phoneNumber.value, code: verificationCode.value })
|
||||||
|
.json();
|
||||||
|
|
||||||
|
if (data.value && !error.value) {
|
||||||
|
if (data.value.code === 200) {
|
||||||
|
showToast({ message: "绑定成功" });
|
||||||
|
localStorage.setItem('token', data.value.data.accessToken)
|
||||||
|
localStorage.setItem('refreshAfter', data.value.data.refreshAfter)
|
||||||
|
localStorage.setItem('accessExpire', data.value.data.accessExpire)
|
||||||
|
closeDialog();
|
||||||
|
await Promise.all([
|
||||||
|
agentStore.fetchAgentStatus(),
|
||||||
|
userStore.fetchUserInfo()
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 发出绑定成功的事件
|
||||||
|
emit('bind-success');
|
||||||
|
|
||||||
|
// 延迟执行路由检查,确保状态已更新
|
||||||
|
setTimeout(() => {
|
||||||
|
// 重新触发路由检查
|
||||||
|
const currentRoute = router.currentRoute.value;
|
||||||
|
router.replace(currentRoute.path);
|
||||||
|
}, 100);
|
||||||
|
} else {
|
||||||
|
showToast(data.value.msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDialog() {
|
||||||
|
dialogStore.closeBindPhone();
|
||||||
|
// 重置表单
|
||||||
|
phoneNumber.value = "";
|
||||||
|
verificationCode.value = "";
|
||||||
|
isAgreed.value = false;
|
||||||
|
if (timer) {
|
||||||
|
clearInterval(timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toUserAgreement() {
|
||||||
|
closeDialog();
|
||||||
|
router.push(`/userAgreement`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPrivacyPolicy() {
|
||||||
|
closeDialog();
|
||||||
|
router.push(`/privacyPolicy`);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="dialogStore.showBindPhone">
|
||||||
|
<van-popup v-model:show="dialogStore.showBindPhone" round position="bottom" :style="{ height: '80%' }"
|
||||||
|
@close="closeDialog">
|
||||||
|
<div class="bind-phone-dialog">
|
||||||
|
<div class="title-bar">
|
||||||
|
<div class="font-bold">绑定手机号码</div>
|
||||||
|
<div class="text-sm text-gray-500 mt-1">
|
||||||
|
为使用完整功能请绑定手机号码
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-500 mt-1">
|
||||||
|
如该微信号之前已绑定过手机号,请输入已绑定的手机号
|
||||||
|
</div>
|
||||||
|
<van-icon name="cross" class="close-icon" @click="closeDialog" />
|
||||||
|
</div>
|
||||||
|
<div class="px-8">
|
||||||
|
<div class="mb-8 pt-8 text-left">
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<img class="h-16 w-16 rounded-full shadow" src="/logo.png" alt="Logo" />
|
||||||
|
<div class="text-3xl mt-4 text-slate-700 font-bold">
|
||||||
|
一查查
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-5">
|
||||||
|
<!-- 手机号输入 -->
|
||||||
|
<div :class="[
|
||||||
|
'input-container bg-blue-300/20',
|
||||||
|
phoneFocused ? 'focused' : '',
|
||||||
|
]">
|
||||||
|
<input v-model="phoneNumber" class="input-field" type="tel" placeholder="请输入手机号"
|
||||||
|
maxlength="11" @focus="phoneFocused = true" @blur="phoneFocused = false" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 验证码输入 -->
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div :class="[
|
||||||
|
'input-container bg-blue-300/20',
|
||||||
|
codeFocused ? 'focused' : '',
|
||||||
|
]">
|
||||||
|
<input v-model="verificationCode" id="bindPhoneVerificationCode" class="input-field"
|
||||||
|
placeholder="请输入验证码" maxlength="6" @focus="codeFocused = true"
|
||||||
|
@blur="codeFocused = false" />
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="ml-2 px-4 py-2 text-sm font-bold flex-shrink-0 rounded-lg transition duration-300"
|
||||||
|
:class="isCountingDown || !isPhoneNumberValid
|
||||||
|
? 'cursor-not-allowed bg-gray-300 text-gray-500'
|
||||||
|
: 'bg-blue-500 text-white hover:bg-blue-600'
|
||||||
|
" @click="sendVerificationCode">
|
||||||
|
{{
|
||||||
|
isCountingDown
|
||||||
|
? `${countdown}s重新获取`
|
||||||
|
: "获取验证码"
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 协议同意框 -->
|
||||||
|
<div class="flex items-start space-x-2">
|
||||||
|
<input type="checkbox" v-model="isAgreed" class="mt-1" />
|
||||||
|
<span class="text-xs text-gray-400 leading-tight">
|
||||||
|
绑定手机号即代表您已阅读并同意
|
||||||
|
<a class="cursor-pointer text-blue-400" @click="toUserAgreement">
|
||||||
|
《用户协议》
|
||||||
|
</a>
|
||||||
|
和
|
||||||
|
<a class="cursor-pointer text-blue-400" @click="toPrivacyPolicy">
|
||||||
|
《隐私政策》
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="mt-10 w-full py-3 text-lg font-bold text-white bg-blue-500 rounded-full transition duration-300"
|
||||||
|
:class="{ 'opacity-50 cursor-not-allowed': !canBind }" @click="handleBind">
|
||||||
|
确认绑定
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</van-popup>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.bind-phone-dialog {
|
||||||
|
background: url("@/assets/images/login_bg.png") no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
background-size: cover;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #666;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-container {
|
||||||
|
border: 2px solid rgba(125, 211, 252, 0);
|
||||||
|
border-radius: 1rem;
|
||||||
|
transition: duration-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-container.focused {
|
||||||
|
border: 2px solid #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field {
|
||||||
|
width: 100%;
|
||||||
|
padding: 1rem;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.3s ease;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -47,8 +47,8 @@ const canvasWidth = 300
|
|||||||
const canvasHeight = 180
|
const canvasHeight = 180
|
||||||
const bgImgUrl = '/image/clickCaptcha.jpg' // 可替换为任意背景图
|
const bgImgUrl = '/image/clickCaptcha.jpg' // 可替换为任意背景图
|
||||||
|
|
||||||
const allChars = ['大', '数', '据', '全', '能', '查', '风', '险', '报', '告']
|
const allChars = ['大', '数', '据', '一', '查', '风', '险', '报', '告']
|
||||||
const targetChars = ref(['全', '能', '查']) // 目标点击顺序固定
|
const targetChars = ref(['一', '查', '查']) // 目标点击顺序固定
|
||||||
const charPositions = ref([]) // [{char, x, y, w, h}]
|
const charPositions = ref([]) // [{char, x, y, w, h}]
|
||||||
const clickedIndex = ref(0)
|
const clickedIndex = ref(0)
|
||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
@@ -134,7 +134,7 @@ function generateCaptcha() {
|
|||||||
;[chars[i], chars[j]] = [chars[j], chars[i]]
|
;[chars[i], chars[j]] = [chars[j], chars[i]]
|
||||||
}
|
}
|
||||||
currentChars = chars
|
currentChars = chars
|
||||||
targetChars.value = ['全', '能', '查']
|
targetChars.value = ['一', '查', '查']
|
||||||
clickedIndex.value = 0
|
clickedIndex.value = 0
|
||||||
errorMessage.value = ''
|
errorMessage.value = ''
|
||||||
successMessage.value = ''
|
successMessage.value = ''
|
||||||
@@ -238,284 +238,284 @@ watch(
|
|||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
backdrop-filter: blur(4px);
|
backdrop-filter: blur(4px);
|
||||||
animation: fadeIn 0.2s ease-out;
|
animation: fadeIn 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeIn {
|
to {
|
||||||
from {
|
opacity: 1;
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-modal {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 360px;
|
||||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05);
|
||||||
|
overflow: hidden;
|
||||||
|
animation: slideUp 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
transform: translateY(20px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1.25rem 1.5rem;
|
||||||
|
border-bottom: 1px solid var(--color-border-primary, #ebedf0);
|
||||||
|
background: linear-gradient(135deg, var(--color-primary-light, rgba(140, 198, 247, 0.1)), #ffffff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-title {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-primary, #323233);
|
||||||
|
margin: 0;
|
||||||
|
background: linear-gradient(135deg, var(--color-primary, #8CC6F7), var(--color-primary-600, #709ec6));
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
border: none;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: var(--color-text-secondary, #646566);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.375rem;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn:hover {
|
||||||
|
color: var(--color-text-primary, #323233);
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-content {
|
||||||
|
padding: 1.5rem 1.5rem 1rem 1.5rem;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-canvas {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
background: var(--color-bg-tertiary, #f8f8f8);
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
border: 2px solid var(--color-border-primary, #ebedf0);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-canvas:hover {
|
||||||
|
border-color: var(--color-primary, #8CC6F7);
|
||||||
|
box-shadow: 0 0 0 3px var(--color-primary-light, rgba(140, 198, 247, 0.1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-instruction {
|
||||||
|
margin: 1.25rem 0 0.75rem 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-instruction p {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: var(--color-text-secondary, #646566);
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.target-list {
|
||||||
|
color: var(--color-primary, #8CC6F7);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
background: var(--color-primary-light, rgba(140, 198, 247, 0.1));
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-status {
|
||||||
|
text-align: center;
|
||||||
|
min-height: 1.75rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
color: var(--color-danger, #ee0a24);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: rgba(238, 10, 36, 0.1);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
animation: shake 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shake {
|
||||||
|
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
25% {
|
||||||
|
transform: translateX(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
75% {
|
||||||
|
transform: translateX(5px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-message {
|
||||||
|
color: var(--color-success, #07c160);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: rgba(7, 193, 96, 0.1);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
animation: bounce 0.4s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
color: var(--color-text-tertiary, #969799);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-footer {
|
||||||
|
padding: 1.25rem 1.5rem;
|
||||||
|
border-top: 1px solid var(--color-border-primary, #ebedf0);
|
||||||
|
background: var(--color-bg-secondary, #fafafa);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.875rem;
|
||||||
|
background: var(--color-primary, #8CC6F7);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.625rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 2px 8px rgba(140, 198, 247, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn:hover:not(:disabled) {
|
||||||
|
background: var(--color-primary-dark, rgba(140, 198, 247, 0.8));
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(140, 198, 247, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn:active:not(:disabled) {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn:disabled {
|
||||||
|
background: var(--color-text-disabled, #c8c9cc);
|
||||||
|
cursor: not-allowed;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.875rem;
|
||||||
|
background: var(--color-success, #07c160);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.625rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 2px 8px rgba(7, 193, 96, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-btn:hover:not(:disabled) {
|
||||||
|
background: rgba(7, 193, 96, 0.9);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(7, 193, 96, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-btn:active:not(:disabled) {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-btn:disabled {
|
||||||
|
background: var(--color-text-disabled, #c8c9cc);
|
||||||
|
cursor: not-allowed;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.captcha-overlay {
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.captcha-modal {
|
.captcha-modal {
|
||||||
background: #fff;
|
max-width: 100%;
|
||||||
border-radius: 1rem;
|
border-radius: 0.875rem;
|
||||||
width: 100%;
|
|
||||||
max-width: 360px;
|
|
||||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05);
|
|
||||||
overflow: hidden;
|
|
||||||
animation: slideUp 0.3s ease-out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slideUp {
|
|
||||||
from {
|
|
||||||
transform: translateY(20px);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
transform: translateY(0);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.captcha-header {
|
.captcha-header {
|
||||||
display: flex;
|
padding: 1rem 1.25rem;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1.25rem 1.5rem;
|
|
||||||
border-bottom: 1px solid var(--color-border-primary, #ebedf0);
|
|
||||||
background: linear-gradient(135deg, var(--color-primary-light, rgba(140, 198, 247, 0.1)), #ffffff);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.captcha-title {
|
|
||||||
font-size: 1.125rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--color-text-primary, #323233);
|
|
||||||
margin: 0;
|
|
||||||
background: linear-gradient(135deg, var(--color-primary, #8CC6F7), var(--color-primary-600, #709ec6));
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-btn {
|
|
||||||
background: rgba(0, 0, 0, 0.05);
|
|
||||||
border: none;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
color: var(--color-text-secondary, #646566);
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0.375rem;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-btn:hover {
|
|
||||||
color: var(--color-text-primary, #323233);
|
|
||||||
background: rgba(0, 0, 0, 0.1);
|
|
||||||
transform: rotate(90deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.captcha-content {
|
.captcha-content {
|
||||||
padding: 1.5rem 1.5rem 1rem 1.5rem;
|
padding: 1.25rem 1.25rem 0.875rem 1.25rem;
|
||||||
background: #ffffff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.captcha-canvas {
|
.captcha-canvas {
|
||||||
width: 100%;
|
min-height: 140px;
|
||||||
border-radius: 0.75rem;
|
|
||||||
background: var(--color-bg-tertiary, #f8f8f8);
|
|
||||||
display: block;
|
|
||||||
margin: 0 auto;
|
|
||||||
border: 2px solid var(--color-border-primary, #ebedf0);
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.captcha-canvas:hover {
|
|
||||||
border-color: var(--color-primary, #8CC6F7);
|
|
||||||
box-shadow: 0 0 0 3px var(--color-primary-light, rgba(140, 198, 247, 0.1));
|
|
||||||
}
|
|
||||||
|
|
||||||
.captcha-instruction {
|
|
||||||
margin: 1.25rem 0 0.75rem 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.captcha-instruction p {
|
|
||||||
font-size: 0.95rem;
|
|
||||||
color: var(--color-text-secondary, #646566);
|
|
||||||
margin: 0;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.target-list {
|
|
||||||
color: var(--color-primary, #8CC6F7);
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 1.05rem;
|
|
||||||
padding: 0.25rem 0.5rem;
|
|
||||||
background: var(--color-primary-light, rgba(140, 198, 247, 0.1));
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.captcha-status {
|
|
||||||
text-align: center;
|
|
||||||
min-height: 1.75rem;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message {
|
|
||||||
color: var(--color-danger, #ee0a24);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0.5rem;
|
|
||||||
background: rgba(238, 10, 36, 0.1);
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
animation: shake 0.3s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes shake {
|
|
||||||
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
25% {
|
|
||||||
transform: translateX(-5px);
|
|
||||||
}
|
|
||||||
|
|
||||||
75% {
|
|
||||||
transform: translateX(5px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.success-message {
|
|
||||||
color: var(--color-success, #07c160);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0.5rem;
|
|
||||||
background: rgba(7, 193, 96, 0.1);
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
animation: bounce 0.4s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes bounce {
|
|
||||||
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
50% {
|
|
||||||
transform: translateY(-3px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-text {
|
|
||||||
color: var(--color-text-tertiary, #969799);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.captcha-footer {
|
.captcha-footer {
|
||||||
padding: 1.25rem 1.5rem;
|
padding: 1rem 1.25rem;
|
||||||
border-top: 1px solid var(--color-border-primary, #ebedf0);
|
|
||||||
background: var(--color-bg-secondary, #fafafa);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.refresh-btn {
|
.captcha-instruction p {
|
||||||
width: 100%;
|
font-size: 0.875rem;
|
||||||
padding: 0.875rem;
|
|
||||||
background: var(--color-primary, #8CC6F7);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 0.625rem;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
box-shadow: 0 2px 8px rgba(140, 198, 247, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.refresh-btn:hover:not(:disabled) {
|
|
||||||
background: var(--color-primary-dark, rgba(140, 198, 247, 0.8));
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 4px 12px rgba(140, 198, 247, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.refresh-btn:active:not(:disabled) {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.refresh-btn:disabled {
|
|
||||||
background: var(--color-text-disabled, #c8c9cc);
|
|
||||||
cursor: not-allowed;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirm-btn {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.875rem;
|
|
||||||
background: var(--color-success, #07c160);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 0.625rem;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
box-shadow: 0 2px 8px rgba(7, 193, 96, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirm-btn:hover:not(:disabled) {
|
|
||||||
background: rgba(7, 193, 96, 0.9);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 4px 12px rgba(7, 193, 96, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirm-btn:active:not(:disabled) {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirm-btn:disabled {
|
|
||||||
background: var(--color-text-disabled, #c8c9cc);
|
|
||||||
cursor: not-allowed;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.captcha-overlay {
|
|
||||||
padding: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.captcha-modal {
|
|
||||||
max-width: 100%;
|
|
||||||
border-radius: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.captcha-header {
|
|
||||||
padding: 1rem 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.captcha-content {
|
|
||||||
padding: 1.25rem 1.25rem 0.875rem 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.captcha-canvas {
|
|
||||||
min-height: 140px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.captcha-footer {
|
|
||||||
padding: 1rem 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.captcha-instruction p {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
分析指数是根据网络行为大数据出具的分析评估参考分数,分数越高越好。该指数仅对本报告有效,不代表对报告查询人的综合定性评价。
|
分析指数是根据网络行为大数据出具的分析评估参考分数,分数越高越好。该指数仅对本报告有效,不代表对报告查询人的综合定性评价。
|
||||||
</div>
|
</div>
|
||||||
<div ref="chartRef" :style="{ width: '100%', height: '200px' }"></div>
|
<div ref="chartRef" :style="{ width: '100%', height: '200px' }"></div>
|
||||||
<div class="risk-description">
|
<!-- <div class="risk-description">
|
||||||
{{ riskDescription }}
|
{{ riskDescription }}
|
||||||
</div>
|
</div> -->
|
||||||
<div class="risk-legend mt-6">
|
<div class="risk-legend mt-6">
|
||||||
<div v-for="item in legendItems" :key="item.level" class="risk-legend__item">
|
<div v-for="item in legendItems" :key="item.level" class="risk-legend__item">
|
||||||
<span class="risk-legend__pill" :style="{ backgroundColor: item.color, color: item.textColor }">
|
<span class="risk-legend__pill" :style="{ backgroundColor: item.color, color: item.textColor }">
|
||||||
@@ -29,10 +29,10 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 根据分数计算风险等级和颜色(分数越高越安全)
|
// 根据分数计算风险等级和颜色(分数范围 10-90:20-80 基础分 ±10,分数越高越安全)
|
||||||
const riskLevel = computed(() => {
|
const riskLevel = computed(() => {
|
||||||
const score = props.score;
|
const score = props.score;
|
||||||
if (score >= 75 && score <= 100) {
|
if (score >= 70 && score <= 90) {
|
||||||
return {
|
return {
|
||||||
level: "无任何风险",
|
level: "无任何风险",
|
||||||
color: "#52c41a",
|
color: "#52c41a",
|
||||||
@@ -41,7 +41,7 @@ const riskLevel = computed(() => {
|
|||||||
{ offset: 1, color: "#7fdb42" }
|
{ offset: 1, color: "#7fdb42" }
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
} else if (score >= 50 && score < 75) {
|
} else if (score >= 50 && score < 70) {
|
||||||
return {
|
return {
|
||||||
level: "风险指数较低",
|
level: "风险指数较低",
|
||||||
color: "#faad14",
|
color: "#faad14",
|
||||||
@@ -50,7 +50,7 @@ const riskLevel = computed(() => {
|
|||||||
{ offset: 1, color: "#ffc53d" }
|
{ offset: 1, color: "#ffc53d" }
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
} else if (score >= 25 && score < 50) {
|
} else if (score >= 30 && score < 50) {
|
||||||
return {
|
return {
|
||||||
level: "风险指数较高",
|
level: "风险指数较高",
|
||||||
color: "#fa8c16",
|
color: "#fa8c16",
|
||||||
@@ -71,14 +71,14 @@ const riskLevel = computed(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 评分解释文本(分数越高越安全)
|
// 评分解释文本(分数范围 10-90,分数越高越安全)
|
||||||
const riskDescription = computed(() => {
|
const riskDescription = computed(() => {
|
||||||
const score = props.score;
|
const score = props.score;
|
||||||
if (score >= 75 && score <= 100) {
|
if (score >= 70 && score <= 90) {
|
||||||
return "根据综合分析,当前报告未检测到明显风险因素,各项指标表现正常,总体状况良好。";
|
return "根据综合分析,当前报告未检测到明显风险因素,各项指标表现正常,总体状况良好。";
|
||||||
} else if (score >= 50 && score < 75) {
|
} else if (score >= 50 && score < 70) {
|
||||||
return "根据综合分析,当前报告存在少量风险信号,建议关注相关指标变化,保持警惕。";
|
return "根据综合分析,当前报告存在少量风险信号,建议关注相关指标变化,保持警惕。";
|
||||||
} else if (score >= 25 && score < 50) {
|
} else if (score >= 30 && score < 50) {
|
||||||
return "根据综合分析,当前报告风险指数较高,多项指标显示异常,建议进一步核实相关情况。";
|
return "根据综合分析,当前报告风险指数较高,多项指标显示异常,建议进一步核实相关情况。";
|
||||||
} else {
|
} else {
|
||||||
return "根据综合分析,当前报告显示高度风险状态,多项重要指标严重异常,请立即采取相应措施。";
|
return "根据综合分析,当前报告显示高度风险状态,多项重要指标严重异常,请立即采取相应措施。";
|
||||||
@@ -89,10 +89,10 @@ const chartRef = ref(null);
|
|||||||
let chartInstance = null;
|
let chartInstance = null;
|
||||||
|
|
||||||
const legendItems = [
|
const legendItems = [
|
||||||
{ level: "高风险", color: "#f5222d", range: "0-24", textColor: "#ffffff" },
|
{ level: "高风险", color: "#f5222d", range: "0-29", textColor: "#ffffff" },
|
||||||
{ level: "一般", color: "#fa8c16", range: "25-49", textColor: "#ffffff" },
|
{ level: "一般", color: "#fa8c16", range: "30-49", textColor: "#ffffff" },
|
||||||
{ level: "良好", color: "#faad14", range: "50-74", textColor: "#ffffff" },
|
{ level: "良好", color: "#faad14", range: "50-69", textColor: "#ffffff" },
|
||||||
{ level: "优秀", color: "#52c41a", range: "75-100", textColor: "#ffffff" }
|
{ level: "优秀", color: "#52c41a", range: "70-100", textColor: "#ffffff" }
|
||||||
];
|
];
|
||||||
|
|
||||||
const initChart = () => {
|
const initChart = () => {
|
||||||
@@ -116,8 +116,8 @@ const updateChart = () => {
|
|||||||
type: "gauge",
|
type: "gauge",
|
||||||
startAngle: 180,
|
startAngle: 180,
|
||||||
endAngle: 0,
|
endAngle: 0,
|
||||||
min: 0,
|
min: 10,
|
||||||
max: 100,
|
max: 90,
|
||||||
radius: "100%",
|
radius: "100%",
|
||||||
center: ["50%", "80%"],
|
center: ["50%", "80%"],
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
@@ -153,7 +153,7 @@ const updateChart = () => {
|
|||||||
show: true,
|
show: true,
|
||||||
distance: -30,
|
distance: -30,
|
||||||
length: 6,
|
length: 6,
|
||||||
splitNumber: 10, // 每1分一个小刻度
|
splitNumber: 8, // 刻度数量(10-90 共 80 分)
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
color: risk.color,
|
color: risk.color,
|
||||||
width: 1,
|
width: 1,
|
||||||
@@ -164,7 +164,7 @@ const updateChart = () => {
|
|||||||
show: true,
|
show: true,
|
||||||
distance: -36,
|
distance: -36,
|
||||||
length: 12,
|
length: 12,
|
||||||
splitNumber: 9, // 9个大刻度,100分分成9个区间
|
splitNumber: 8, // 8个大刻度,10-90 分成 8 个区间
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
color: risk.color,
|
color: risk.color,
|
||||||
width: 2,
|
width: 2,
|
||||||
@@ -195,7 +195,7 @@ const updateChart = () => {
|
|||||||
color: risk.color,
|
color: risk.color,
|
||||||
offsetCenter: [0, "-25%"],
|
offsetCenter: [0, "-25%"],
|
||||||
formatter: function (value) {
|
formatter: function (value) {
|
||||||
return `{value|${value}分}\n{level|${risk.level}}`;
|
return `{value|${value}分}`;
|
||||||
},
|
},
|
||||||
rich: {
|
rich: {
|
||||||
value: {
|
value: {
|
||||||
@@ -264,11 +264,12 @@ onUnmounted(() => {
|
|||||||
.risk-description {
|
.risk-description {
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
color: #666666;
|
color: #666666;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.risk-legend {
|
.risk-legend {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -66,19 +66,21 @@
|
|||||||
class="w-full bg-primary-second text-white py-4 rounded-[48px] text-lg font-medium mb-4 flex items-center justify-center mt-10"
|
class="w-full bg-primary-second text-white py-4 rounded-[48px] text-lg font-medium mb-4 flex items-center justify-center mt-10"
|
||||||
@click="handleSubmit">
|
@click="handleSubmit">
|
||||||
<span>{{ buttonText }}</span>
|
<span>{{ buttonText }}</span>
|
||||||
<span class="ml-4">¥{{ featureData.sell_price }}</span>
|
<span v-if="!isWeChat" class="ml-4">¥{{ featureData.sell_price }}</span>
|
||||||
</button>
|
</button>
|
||||||
<!-- <div class="text-gray-500 leading-relaxed mt-8" v-html="featureData.description">
|
<!-- <div class="text-gray-500 leading-relaxed mt-8" v-html="featureData.description">
|
||||||
</div> -->
|
</div> -->
|
||||||
<!-- 免责声明 -->
|
<!-- 免责声明 -->
|
||||||
<div class="text-xs text-center text-gray-500 leading-relaxed mt-2">
|
<div class="text-xs text-center text-gray-500 leading-relaxed mt-2">
|
||||||
为保证用户的隐私及数据安全,查询结果生成30天后将自动删除
|
为保证用户的隐私及数据安全,查询结果生成{{ appStore.queryRetentionDays || 30 }}天后将自动删除
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 报告包含内容 -->
|
<!-- 报告包含内容 -->
|
||||||
<div class="card mt-3" v-if="featureData.features && featureData.features.length > 0">
|
<div class="card mt-3" v-if="showReportFeatures">
|
||||||
<ReportFeatures :features="featureData.features" :title-style="{ color: 'var(--van-text-color)' }" />
|
<ReportFeatures :features="featureData.features || []" :inquire-product="inquireProductEn"
|
||||||
|
:inquire-feature-data="featureData"
|
||||||
|
:title-style="{ color: 'var(--van-text-color)' }" />
|
||||||
<div class="mt-3 text-center">
|
<div class="mt-3 text-center">
|
||||||
<div class="inline-flex items-center px-3 py-1.5 rounded-full border transition-all"
|
<div class="inline-flex items-center px-3 py-1.5 rounded-full border transition-all"
|
||||||
style="background: linear-gradient(135deg, var(--van-theme-primary-light), rgba(255,255,255,0.8)); border-color: var(--van-theme-primary);">
|
style="background: linear-gradient(135deg, var(--van-theme-primary-light), rgba(255,255,255,0.8)); border-color: var(--van-theme-primary);">
|
||||||
@@ -95,7 +97,7 @@
|
|||||||
<div class="mb-4 text-xl font-bold" style="color: var(--van-text-color);">
|
<div class="mb-4 text-xl font-bold" style="color: var(--van-text-color);">
|
||||||
{{ featureData.product_name }}
|
{{ featureData.product_name }}
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4 flex items-start justify-between">
|
<div v-if="!isWeChat" class="mb-4 flex items-start justify-between">
|
||||||
<div class="text-lg" style="color: var(--van-text-color-2);">价格:</div>
|
<div class="text-lg" style="color: var(--van-text-color-2);">价格:</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-2xl font-semibold text-danger">
|
<div class="text-2xl font-semibold text-danger">
|
||||||
@@ -108,14 +110,14 @@
|
|||||||
v-html="featureData.description">
|
v-html="featureData.description">
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2 text-xs italic text-danger">
|
<div class="mb-2 text-xs italic text-danger">
|
||||||
为保证用户的隐私以及数据安全,查询的结果生成30天之后将自动清除。
|
为保证用户的隐私以及数据安全,查询的结果生成{{ appStore.queryRetentionDays || 30 }}天之后将自动清除。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 支付组件 -->
|
<!-- 支付组件 -->
|
||||||
<Payment v-model="showPayment" :data="featureData" :id="queryId" type="query" @close="showPayment = false" />
|
<Payment v-model="showPayment" :data="featureData" :id="queryId" type="query" @close="showPayment = false" />
|
||||||
<BindPhoneDialog @bind-success="handleBindSuccess" />
|
<BindPhoneOnlyDialog @bind-success="handleBindSuccess" />
|
||||||
|
|
||||||
<!-- 历史查询按钮 - 仅推广查询显示 -->
|
<!-- 历史查询按钮 - 仅推广查询显示 -->
|
||||||
<div v-if="props.type === 'promotion'" @click="toHistory"
|
<div v-if="props.type === 'promotion'" @click="toHistory"
|
||||||
@@ -126,18 +128,24 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, computed, onMounted, onUnmounted, nextTick } from "vue";
|
import { ref, reactive, computed, onMounted, onUnmounted, nextTick, watch } from "vue";
|
||||||
import { aesEncrypt } from "@/utils/crypto";
|
import { aesEncrypt } from "@/utils/crypto";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { useUserStore } from "@/stores/userStore";
|
import { useUserStore } from "@/stores/userStore";
|
||||||
import { useDialogStore } from "@/stores/dialogStore";
|
import { useDialogStore } from "@/stores/dialogStore";
|
||||||
import { useEnv } from "@/composables/useEnv";
|
import { useEnv } from "@/composables/useEnv";
|
||||||
import { showConfirmDialog } from "vant";
|
import { showConfirmDialog } from "vant";
|
||||||
|
import { useAliyunCaptcha } from "@/composables/useAliyunCaptcha";
|
||||||
|
|
||||||
import Payment from "@/components/Payment.vue";
|
import Payment from "@/components/Payment.vue";
|
||||||
import BindPhoneDialog from "@/components/BindPhoneDialog.vue";
|
import BindPhoneOnlyDialog from "@/components/BindPhoneOnlyDialog.vue";
|
||||||
import SectionTitle from "@/components/SectionTitle.vue";
|
import SectionTitle from "@/components/SectionTitle.vue";
|
||||||
import ReportFeatures from "@/components/ReportFeatures.vue";
|
import ReportFeatures from "@/components/ReportFeatures.vue";
|
||||||
|
import { useAppStore } from "@/stores/appStore";
|
||||||
|
import {
|
||||||
|
isRiskAssessmentProduct,
|
||||||
|
resolveInquireProductEn,
|
||||||
|
} from "@/constants/riskAssessmentReportFeatures";
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -174,7 +182,7 @@ const loadProductBackground = async (productType) => {
|
|||||||
return (await import("@/assets/images/report/xwqy_inquire_bg.png")).default;
|
return (await import("@/assets/images/report/xwqy_inquire_bg.png")).default;
|
||||||
case 'preloanbackgroundcheck':
|
case 'preloanbackgroundcheck':
|
||||||
return (await import("@/assets/images/report/dqfx_inquire_bg.png")).default;
|
return (await import("@/assets/images/report/dqfx_inquire_bg.png")).default;
|
||||||
case 'personalData':
|
case 'riskassessment':
|
||||||
return (await import("@/assets/images/report/grdsj_inquire_bg.png")).default;
|
return (await import("@/assets/images/report/grdsj_inquire_bg.png")).default;
|
||||||
case 'marriage':
|
case 'marriage':
|
||||||
return (await import("@/assets/images/report/marriage_inquire_bg.png")).default;
|
return (await import("@/assets/images/report/marriage_inquire_bg.png")).default;
|
||||||
@@ -198,6 +206,8 @@ const router = useRouter();
|
|||||||
const dialogStore = useDialogStore();
|
const dialogStore = useDialogStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const { isWeChat } = useEnv();
|
const { isWeChat } = useEnv();
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const { runWithCaptcha } = useAliyunCaptcha();
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const showPayment = ref(false);
|
const showPayment = ref(false);
|
||||||
@@ -211,6 +221,17 @@ const countdown = ref(60);
|
|||||||
// 使用传入的featureData或创建响应式引用
|
// 使用传入的featureData或创建响应式引用
|
||||||
const featureData = computed(() => props.featureData || {});
|
const featureData = computed(() => props.featureData || {});
|
||||||
|
|
||||||
|
const inquireProductEn = computed(() =>
|
||||||
|
resolveInquireProductEn(props.feature, featureData.value)
|
||||||
|
);
|
||||||
|
|
||||||
|
const showReportFeatures = computed(() => {
|
||||||
|
if (isRiskAssessmentProduct(inquireProductEn.value)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !!(featureData.value.features && featureData.value.features.length > 0);
|
||||||
|
});
|
||||||
|
|
||||||
// 表单数据
|
// 表单数据
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
name: "",
|
name: "",
|
||||||
@@ -251,6 +272,9 @@ const backgroundStyle = computed(() => {
|
|||||||
|
|
||||||
// 动态加载牌匾背景图片
|
// 动态加载牌匾背景图片
|
||||||
const loadTrapezoidBackground = async () => {
|
const loadTrapezoidBackground = async () => {
|
||||||
|
if (!props.feature) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
let bgModule;
|
let bgModule;
|
||||||
if (props.feature === 'marriage') {
|
if (props.feature === 'marriage') {
|
||||||
@@ -318,8 +342,7 @@ function handleBindSuccess() {
|
|||||||
// 处理输入框点击事件
|
// 处理输入框点击事件
|
||||||
const handleInputClick = async () => {
|
const handleInputClick = async () => {
|
||||||
if (!isLoggedIn.value) {
|
if (!isLoggedIn.value) {
|
||||||
// 非微信浏览器环境:未登录用户提示跳转到登录页
|
if (!isWeChat.value && props.type !== 'promotion') {
|
||||||
if (!isWeChat.value) {
|
|
||||||
try {
|
try {
|
||||||
await showConfirmDialog({
|
await showConfirmDialog({
|
||||||
title: '提示',
|
title: '提示',
|
||||||
@@ -333,16 +356,14 @@ const handleInputClick = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 微信浏览器环境:已登录但检查是否需要绑定手机号
|
if (isWeChat.value && !userStore.mobile && props.type !== 'promotion') {
|
||||||
if (isWeChat.value && !userStore.mobile) {
|
|
||||||
dialogStore.openBindPhone();
|
dialogStore.openBindPhone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
// 非微信浏览器环境:检查登录状态
|
if (!isWeChat.value && !isLoggedIn.value && props.type !== 'promotion') {
|
||||||
if (!isWeChat.value && !isLoggedIn.value) {
|
|
||||||
router.push('/login');
|
router.push('/login');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -378,7 +399,7 @@ function handleSubmit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否需要绑定手机号
|
// 检查是否需要绑定手机号
|
||||||
if (!userStore.mobile) {
|
if (!userStore.mobile && props.type !== 'promotion') {
|
||||||
pendingPayment.value = true;
|
pendingPayment.value = true;
|
||||||
dialogStore.openBindPhone();
|
dialogStore.openBindPhone();
|
||||||
} else {
|
} else {
|
||||||
@@ -421,6 +442,9 @@ async function submitRequest() {
|
|||||||
localStorage.setItem("token", data.value.data.accessToken);
|
localStorage.setItem("token", data.value.data.accessToken);
|
||||||
localStorage.setItem("refreshAfter", data.value.data.refreshAfter);
|
localStorage.setItem("refreshAfter", data.value.data.refreshAfter);
|
||||||
localStorage.setItem("accessExpire", data.value.data.accessExpire);
|
localStorage.setItem("accessExpire", data.value.data.accessExpire);
|
||||||
|
// ⚠️ 重要:保存 token 后立即设置 tokenVersion,防止被 checkTokenVersion 清除
|
||||||
|
const tokenVersion = import.meta.env.VITE_TOKEN_VERSION || "1.1";
|
||||||
|
localStorage.setItem("tokenVersion", tokenVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
showPayment.value = true;
|
showPayment.value = true;
|
||||||
@@ -435,22 +459,27 @@ async function sendVerificationCode() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data, error } = await useApiFetch("/auth/sendSms")
|
// 使用阿里云滑块验证码保护发送短信接口
|
||||||
.post({ mobile: formData.mobile, actionType: "query" })
|
runWithCaptcha(
|
||||||
.json();
|
(captchaVerifyParam) => useApiFetch("/auth/sendSms")
|
||||||
|
.post({ mobile: formData.mobile, actionType: "query", captchaVerifyParam })
|
||||||
if (!error.value && data.value.code === 200) {
|
.json(),
|
||||||
showToast({ message: "验证码发送成功", type: "success" });
|
(result) => {
|
||||||
startCountdown();
|
// result 已经是解包后的响应数据(data.value)
|
||||||
nextTick(() => {
|
if (result && result.code === 200) {
|
||||||
const verificationCodeInput = document.getElementById('verificationCode');
|
showToast({ message: "验证码发送成功", type: "success" });
|
||||||
if (verificationCodeInput) {
|
startCountdown();
|
||||||
verificationCodeInput.focus();
|
nextTick(() => {
|
||||||
|
const verificationCodeInput = document.getElementById('verificationCode');
|
||||||
|
if (verificationCodeInput) {
|
||||||
|
verificationCodeInput.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (result) {
|
||||||
|
showToast({ message: result.msg || "验证码发送失败,请重试" });
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
} else {
|
);
|
||||||
showToast({ message: "验证码发送失败,请重试" });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let timer = null;
|
let timer = null;
|
||||||
@@ -488,18 +517,29 @@ const toHistory = () => {
|
|||||||
router.push("/historyQuery");
|
router.push("/historyQuery");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 加载背景图片
|
||||||
|
const loadBackgroundImage = async () => {
|
||||||
|
if (!props.feature) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const background = await loadProductBackground(props.feature);
|
||||||
|
productBackground.value = background || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听 feature 变化,重新加载背景图
|
||||||
|
watch(() => props.feature, async (newFeature) => {
|
||||||
|
if (newFeature) {
|
||||||
|
await loadBackgroundImage();
|
||||||
|
await loadTrapezoidBackground();
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
// 生命周期
|
// 生命周期
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadBackgroundImage();
|
await loadBackgroundImage();
|
||||||
await loadTrapezoidBackground();
|
await loadTrapezoidBackground();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 加载背景图片
|
|
||||||
const loadBackgroundImage = async () => {
|
|
||||||
const background = await loadProductBackground(props.feature);
|
|
||||||
productBackground.value = background || '';
|
|
||||||
};
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (timer) {
|
if (timer) {
|
||||||
clearInterval(timer);
|
clearInterval(timer);
|
||||||
@@ -576,4 +616,4 @@ button:active {
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<van-popup
|
<van-popup v-model:show="show" position="bottom" class="flex flex-col justify-between p-6"
|
||||||
v-model:show="show"
|
:style="{ height: '50%' }">
|
||||||
position="bottom"
|
<!-- Logo和应用名 -->
|
||||||
class="flex flex-col justify-between p-6"
|
<div class="flex items-center justify-center mb-2">
|
||||||
:style="{ height: '50%' }"
|
<img src="/logo.png" alt="一查查" class="w-8 h-8 mr-2" />
|
||||||
>
|
<span class="text-base font-semibold">一查查</span>
|
||||||
|
</div>
|
||||||
|
<!-- 日期时间 -->
|
||||||
|
<div class="text-center mb-2">
|
||||||
|
<div class="text-sm text-gray-600">{{ currentDateTime }}</div>
|
||||||
|
</div>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<h3 class="text-lg font-bold">支付</h3>
|
<h3 class="text-lg font-bold">支付</h3>
|
||||||
</div>
|
</div>
|
||||||
@@ -12,11 +17,8 @@
|
|||||||
<div class="font-bold text-xl">{{ data.product_name }}</div>
|
<div class="font-bold text-xl">{{ data.product_name }}</div>
|
||||||
<div class="text-3xl text-red-500 font-bold">
|
<div class="text-3xl text-red-500 font-bold">
|
||||||
<!-- 显示原价和折扣价格 -->
|
<!-- 显示原价和折扣价格 -->
|
||||||
<div
|
<div v-if="discountPrice" class="line-through text-gray-500 mt-4"
|
||||||
v-if="discountPrice"
|
:class="{ 'text-2xl': discountPrice }">
|
||||||
class="line-through text-gray-500 mt-4"
|
|
||||||
:class="{ 'text-2xl': discountPrice }"
|
|
||||||
>
|
|
||||||
¥ {{ data.sell_price }}
|
¥ {{ data.sell_price }}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -36,63 +38,46 @@
|
|||||||
<!-- 支付方式选择 -->
|
<!-- 支付方式选择 -->
|
||||||
<div class="">
|
<div class="">
|
||||||
<van-cell-group inset>
|
<van-cell-group inset>
|
||||||
<van-cell
|
<!-- 开发环境测试支付选项 -->
|
||||||
v-if="isWeChat"
|
<van-cell v-if="isDevMode" title="测试支付(开发环境)" clickable @click="selectedPaymentMethod = 'test'">
|
||||||
title="微信支付"
|
|
||||||
clickable
|
|
||||||
@click="selectedPaymentMethod = 'wechat'"
|
|
||||||
>
|
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<van-icon
|
<van-icon size="24" name="setting" color="#FF9800" class="mr-2" />
|
||||||
size="24"
|
|
||||||
name="wechat-pay"
|
|
||||||
color="#1AAD19"
|
|
||||||
class="mr-2"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
<template #right-icon>
|
<template #right-icon>
|
||||||
<van-radio
|
<van-radio v-model="selectedPaymentMethod" name="test" />
|
||||||
v-model="selectedPaymentMethod"
|
</template>
|
||||||
name="wechat"
|
</van-cell>
|
||||||
/>
|
|
||||||
|
<van-cell v-if="isWeChat" title="微信支付" clickable @click="selectedPaymentMethod = 'wechat'">
|
||||||
|
<template #icon>
|
||||||
|
<van-icon size="24" name="wechat-pay" color="#1AAD19" class="mr-2" />
|
||||||
|
</template>
|
||||||
|
<template #right-icon>
|
||||||
|
<van-radio v-model="selectedPaymentMethod" name="wechat" />
|
||||||
</template>
|
</template>
|
||||||
</van-cell>
|
</van-cell>
|
||||||
|
|
||||||
<!-- 支付宝支付 -->
|
<!-- 支付宝支付 -->
|
||||||
<van-cell
|
<van-cell v-else title="支付宝支付" clickable @click="selectedPaymentMethod = 'alipay'">
|
||||||
v-else
|
|
||||||
title="支付宝支付"
|
|
||||||
clickable
|
|
||||||
@click="selectedPaymentMethod = 'alipay'"
|
|
||||||
>
|
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<van-icon
|
<van-icon size="24" name="alipay" color="#00A1E9" class="mr-2" />
|
||||||
size="24"
|
|
||||||
name="alipay"
|
|
||||||
color="#00A1E9"
|
|
||||||
class="mr-2"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
<template #right-icon>
|
<template #right-icon>
|
||||||
<van-radio
|
<van-radio v-model="selectedPaymentMethod" name="alipay" />
|
||||||
v-model="selectedPaymentMethod"
|
|
||||||
name="alipay"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</van-cell>
|
</van-cell>
|
||||||
</van-cell-group>
|
</van-cell-group>
|
||||||
</div>
|
</div>
|
||||||
<!-- 确认按钮 -->
|
<!-- 确认按钮 -->
|
||||||
<div class="">
|
<div class="">
|
||||||
<van-button class="w-full" round type="primary" @click="getPayment"
|
<van-button class="w-full" round type="primary" @click="getPayment">确认支付</van-button>
|
||||||
>确认支付</van-button
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</van-popup>
|
</van-popup>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, defineProps } from "vue";
|
import { ref, computed, onMounted, onUnmounted } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
const { isWeChat } = useEnv();
|
const { isWeChat } = useEnv();
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -108,26 +93,70 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
returnUrl: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const show = defineModel();
|
const show = defineModel();
|
||||||
const selectedPaymentMethod = ref(isWeChat.value ? "wechat" : "alipay");
|
|
||||||
|
// 判断是否为开发环境
|
||||||
|
const isDevMode = computed(() => {
|
||||||
|
return import.meta.env.MODE === 'development' || import.meta.env.DEV;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 默认支付方式:开发环境优先使用测试支付,否则根据平台选择
|
||||||
|
const selectedPaymentMethod = ref(
|
||||||
|
isDevMode.value ? "test" : (isWeChat.value ? "wechat" : "alipay")
|
||||||
|
);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (isWeChat.value) {
|
if (!isDevMode.value) {
|
||||||
selectedPaymentMethod.value = "wechat";
|
// 非开发环境,根据平台选择支付方式
|
||||||
} else {
|
if (isWeChat.value) {
|
||||||
selectedPaymentMethod.value = "alipay";
|
selectedPaymentMethod.value = "wechat";
|
||||||
|
} else {
|
||||||
|
selectedPaymentMethod.value = "alipay";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 初始化日期时间并启动定时器
|
||||||
|
updateDateTime();
|
||||||
|
dateTimeTimer = setInterval(updateDateTime, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
// 清理定时器
|
||||||
|
if (dateTimeTimer) {
|
||||||
|
clearInterval(dateTimeTimer);
|
||||||
|
dateTimeTimer = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const orderNo = ref("");
|
const orderNo = ref("");
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const discountPrice = ref(false); // 是否应用折扣
|
const discountPrice = ref(false); // 是否应用折扣
|
||||||
onMounted(() => {
|
|
||||||
if (isWeChat.value) {
|
// 当前日期时间
|
||||||
selectedPaymentMethod.value = "wechat";
|
const currentDateTime = ref("");
|
||||||
} else {
|
let dateTimeTimer = null;
|
||||||
selectedPaymentMethod.value = "alipay";
|
|
||||||
}
|
// 格式化日期时间:YYYY年MM月DD日 h:m:s
|
||||||
});
|
function formatDateTime() {
|
||||||
|
const now = new Date();
|
||||||
|
const year = now.getFullYear();
|
||||||
|
const month = String(now.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(now.getDate()).padStart(2, "0");
|
||||||
|
const hours = now.getHours();
|
||||||
|
const minutes = now.getMinutes();
|
||||||
|
const seconds = now.getSeconds();
|
||||||
|
return `${year}年${month}月${day}日 ${hours}:${minutes}:${seconds}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新日期时间
|
||||||
|
function updateDateTime() {
|
||||||
|
currentDateTime.value = formatDateTime();
|
||||||
|
}
|
||||||
|
|
||||||
async function getPayment() {
|
async function getPayment() {
|
||||||
const { data, error } = await useApiFetch("/pay/payment")
|
const { data, error } = await useApiFetch("/pay/payment")
|
||||||
@@ -139,7 +168,18 @@ async function getPayment() {
|
|||||||
.json();
|
.json();
|
||||||
|
|
||||||
if (data.value && !error.value) {
|
if (data.value && !error.value) {
|
||||||
if (selectedPaymentMethod.value === "alipay") {
|
// 测试支付模式:直接跳转到结果页面
|
||||||
|
if (selectedPaymentMethod.value === "test" || selectedPaymentMethod.value === "test_empty") {
|
||||||
|
orderNo.value = data.value.data.order_no;
|
||||||
|
const queryParams = { orderNo: data.value.data.order_no };
|
||||||
|
if (props.returnUrl) {
|
||||||
|
queryParams.returnUrl = props.returnUrl;
|
||||||
|
}
|
||||||
|
router.push({
|
||||||
|
path: "/payment/result",
|
||||||
|
query: queryParams,
|
||||||
|
});
|
||||||
|
} else if (selectedPaymentMethod.value === "alipay") {
|
||||||
orderNo.value = data.value.data.order_no;
|
orderNo.value = data.value.data.order_no;
|
||||||
// 存储订单ID以便支付宝返回时获取
|
// 存储订单ID以便支付宝返回时获取
|
||||||
const prepayUrl = data.value.data.prepay_id;
|
const prepayUrl = data.value.data.prepay_id;
|
||||||
@@ -157,9 +197,13 @@ async function getPayment() {
|
|||||||
function (res) {
|
function (res) {
|
||||||
if (res.err_msg == "get_brand_wcpay_request:ok") {
|
if (res.err_msg == "get_brand_wcpay_request:ok") {
|
||||||
// 支付成功,直接跳转到结果页面
|
// 支付成功,直接跳转到结果页面
|
||||||
|
const queryParams = { orderNo: data.value.data.order_no };
|
||||||
|
if (props.returnUrl) {
|
||||||
|
queryParams.returnUrl = props.returnUrl;
|
||||||
|
}
|
||||||
router.push({
|
router.push({
|
||||||
path: "/payment/result",
|
path: "/payment/result",
|
||||||
query: { orderNo: data.value.data.order_no },
|
query: queryParams,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,15 +16,20 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between mt-2">
|
<div class="flex items-center justify-between mt-2">
|
||||||
<div>推广收益为<span class="text-orange-500"> {{ promotionRevenue }} </span>元</div>
|
<div>推广收益为<span class="text-orange-500"> {{ promotionRevenue }} </span>元</div>
|
||||||
<div>我的成本为<span class="text-orange-500"> {{ costPrice }} </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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card m-4">
|
<div class="card m-4">
|
||||||
<div class="text-lg mb-2">收益与成本说明</div>
|
<div class="text-lg mb-2">收益与成本说明</div>
|
||||||
|
|
||||||
<div>推广收益 = 客户查询价 - 我的成本</div>
|
<div>推广收益 = 客户查询价 - 我的成本</div>
|
||||||
<div>我的成本 = 提价成本 + 底价成本</div>
|
<div>我的成本 = 实际底价 + 提价成本</div>
|
||||||
<div class="mt-1">提价成本:超过平台标准定价部分,平台会收取部分成本价</div>
|
<div class="mt-1">提价成本:设置{{ productConfig.price_threshold }}元以上的部分收取{{
|
||||||
|
rateFormat(productConfig.price_fee_rate)
|
||||||
|
}}的提价成本</div>
|
||||||
<div class="">设定范围:<span class="text-orange-500">{{
|
<div class="">设定范围:<span class="text-orange-500">{{
|
||||||
productConfig.price_range_min }}</span>元 - <span class="text-orange-500">{{
|
productConfig.price_range_min }}</span>元 - <span class="text-orange-500">{{
|
||||||
productConfig.price_range_max }}</span>元</div>
|
productConfig.price_range_max }}</span>元</div>
|
||||||
@@ -60,24 +65,43 @@ watch(show, () => {
|
|||||||
|
|
||||||
const costPrice = computed(() => {
|
const costPrice = computed(() => {
|
||||||
if (!productConfig.value) return 0.00
|
if (!productConfig.value) return 0.00
|
||||||
// 平台定价成本
|
|
||||||
let platformPricing = 0
|
// 新代理系统:成本价 = 实际底价(actual_base_price)+ 提价成本
|
||||||
platformPricing += productConfig.value.cost_price
|
const actualBasePrice = Number(productConfig.value.actual_base_price) || 0;
|
||||||
if (price.value > productConfig.value.p_pricing_standard) {
|
const priceNum = Number(price.value) || 0;
|
||||||
platformPricing += (price.value - productConfig.value.p_pricing_standard) * productConfig.value.p_overpricing_ratio
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (productConfig.value.a_pricing_standard > platformPricing && productConfig.value.a_pricing_end > platformPricing && productConfig.value.a_overpricing_ratio > 0) {
|
// 总成本 = 实际底价 + 提价成本
|
||||||
if (price.value > productConfig.value.a_pricing_standard) {
|
const totalCost = actualBasePrice + priceCost;
|
||||||
if (price.value > productConfig.value.a_pricing_end) {
|
|
||||||
platformPricing += (productConfig.value.a_pricing_end - productConfig.value.a_pricing_standard) * productConfig.value.a_overpricing_ratio
|
|
||||||
} else {
|
|
||||||
platformPricing += (price.value - productConfig.value.a_pricing_standard) * productConfig.value.a_overpricing_ratio
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return safeTruncate(platformPricing)
|
return safeTruncate(totalCost);
|
||||||
|
})
|
||||||
|
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(() => {
|
const promotionRevenue = computed(() => {
|
||||||
@@ -158,4 +182,4 @@ const onBlurPrice = () => {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
<style lang="scss" scoped></style>
|
||||||
|
|||||||
@@ -1,74 +1,55 @@
|
|||||||
<template>
|
<template>
|
||||||
<van-popup v-model:show="show" round position="bottom" :style="{ maxHeight: '95vh' }">
|
<div class="qrcode-popup-container">
|
||||||
<div class="qrcode-popup-container">
|
<div class="qrcode-content">
|
||||||
<div class="qrcode-content">
|
<!-- 网格布局显示4张海报 -->
|
||||||
<van-swipe class="poster-swiper rounded-lg sm:rounded-xl shadow" indicator-color="white"
|
<div class="poster-grid">
|
||||||
@change="onSwipeChange">
|
<div v-for="(_, index) in posterImages" :key="index"
|
||||||
<van-swipe-item v-for="(_, index) in posterImages" :key="index">
|
:class="['poster-item', { 'poster-item-selected': currentIndex === index }]"
|
||||||
<canvas :ref="(el) => (posterCanvasRefs[index] = el)"
|
@click="selectPoster(index)">
|
||||||
class="poster-canvas rounded-lg sm:rounded-xl m-auto"></canvas>
|
<canvas :ref="(el) => (posterCanvasRefs[index] = el)"
|
||||||
</van-swipe-item>
|
class="poster-canvas rounded-lg sm:rounded-xl"></canvas>
|
||||||
</van-swipe>
|
<!-- 单张海报加载占位 -->
|
||||||
|
<div v-if="posterLoading[index]" class="poster-skeleton">
|
||||||
|
<van-loading size="20px" vertical>生成中</van-loading>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="mode === 'promote'"
|
<!-- 整体加载状态提示 -->
|
||||||
class="swipe-tip text-center text-gray-700 text-xs sm:text-sm mb-1 sm:mb-2 px-2">
|
<div v-if="isGeneratingPosters" class="poster-loading-overlay">
|
||||||
<span class="swipe-icon">←</span> 左右滑动切换海报
|
<van-loading size="24px" vertical>海报生成中...</van-loading>
|
||||||
<span class="swipe-icon">→</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="fixed bottom-0 left-0 right-0 bg-white">
|
||||||
<van-divider class="my-2 sm:my-3">分享到好友</van-divider>
|
<van-divider class="my-2 sm:my-3">分享到好友</van-divider>
|
||||||
|
|
||||||
<div class="flex items-center justify-around pb-3 sm:pb-4 px-4">
|
<div class="flex items-center justify-around pb-3 sm:pb-4 px-4">
|
||||||
<!-- 微信环境:显示分享、保存和复制按钮 -->
|
|
||||||
<template v-if="isWeChat">
|
<template v-if="isWeChat">
|
||||||
<!-- <div class="flex flex-col items-center justify-center cursor-pointer" @click="shareToFriend">
|
|
||||||
<img src="@/assets/images/icon_share_friends.svg"
|
|
||||||
class="share-icon w-9 h-9 sm:w-10 sm:h-10 rounded-full" />
|
|
||||||
<div class="text-center mt-1 text-gray-600 text-xs">
|
|
||||||
分享给好友
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col items-center justify-center cursor-pointer" @click="shareToTimeline">
|
|
||||||
<img src="@/assets/images/icon_share_wechat.svg"
|
|
||||||
class="share-icon w-9 h-9 sm:w-10 sm:h-10 rounded-full" />
|
|
||||||
<div class="text-center mt-1 text-gray-600 text-xs">
|
|
||||||
分享到朋友圈
|
|
||||||
</div>
|
|
||||||
</div> -->
|
|
||||||
<div class="flex flex-col items-center justify-center cursor-pointer" @click="savePosterForWeChat">
|
<div class="flex flex-col items-center justify-center cursor-pointer" @click="savePosterForWeChat">
|
||||||
<img src="@/assets/images/icon_share_img.svg"
|
<img src="@/assets/images/icon_share_img.svg"
|
||||||
class="share-icon w-9 h-9 sm:w-10 sm:h-10 rounded-full" />
|
class="share-icon w-9 h-9 sm:w-10 sm:h-10 rounded-full" />
|
||||||
<div class="text-center mt-1 text-gray-600 text-xs">
|
<div class="text-center mt-1 text-gray-600 text-xs">保存图片</div>
|
||||||
保存图片
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col items-center justify-center cursor-pointer" @click="copyUrl">
|
<div class="flex flex-col items-center justify-center cursor-pointer" @click="copyUrl">
|
||||||
<img src="@/assets/images/icon_share_url.svg"
|
<img src="@/assets/images/icon_share_url.svg"
|
||||||
class="share-icon w-9 h-9 sm:w-10 sm:h-10 rounded-full" />
|
class="share-icon w-9 h-9 sm:w-10 sm:h-10 rounded-full" />
|
||||||
<div class="text-center mt-1 text-gray-600 text-xs">
|
<div class="text-center mt-1 text-gray-600 text-xs">复制链接</div>
|
||||||
复制链接
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<!-- 非微信环境:显示保存和复制按钮 -->
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="flex flex-col items-center justify-center cursor-pointer" @click="savePoster">
|
<div class="flex flex-col items-center justify-center cursor-pointer" @click="savePoster">
|
||||||
<img src="@/assets/images/icon_share_img.svg"
|
<img src="@/assets/images/icon_share_img.svg"
|
||||||
class="share-icon w-9 h-9 sm:w-10 sm:h-10 rounded-full" />
|
class="share-icon w-9 h-9 sm:w-10 sm:h-10 rounded-full" />
|
||||||
<div class="text-center mt-1 text-gray-600 text-xs">
|
<div class="text-center mt-1 text-gray-600 text-xs">保存图片</div>
|
||||||
保存图片
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col items-center justify-center cursor-pointer" @click="copyUrl">
|
<div class="flex flex-col items-center justify-center cursor-pointer" @click="copyUrl">
|
||||||
<img src="@/assets/images/icon_share_url.svg"
|
<img src="@/assets/images/icon_share_url.svg"
|
||||||
class="share-icon w-9 h-9 sm:w-10 sm:h-10 rounded-full" />
|
class="share-icon w-9 h-9 sm:w-10 sm:h-10 rounded-full" />
|
||||||
<div class="text-center mt-1 text-gray-600 text-xs">
|
<div class="text-center mt-1 text-gray-600 text-xs">复制链接</div>
|
||||||
复制链接
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</van-popup>
|
</div>
|
||||||
|
|
||||||
<!-- 图片保存指引遮罩层 -->
|
<!-- 图片保存指引遮罩层 -->
|
||||||
<ImageSaveGuide :show="showImageGuide" :image-url="currentImageUrl" :title="imageGuideTitle"
|
<ImageSaveGuide :show="showImageGuide" :image-url="currentImageUrl" :title="imageGuideTitle"
|
||||||
@@ -85,18 +66,36 @@ import ImageSaveGuide from "./ImageSaveGuide.vue";
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
linkIdentifier: {
|
linkIdentifier: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: false, // 推广链接模式下需要
|
||||||
|
},
|
||||||
|
fullLink: {
|
||||||
|
type: String,
|
||||||
|
required: false, // 完整的推广链接(后端返回)
|
||||||
|
},
|
||||||
|
inviteLink: {
|
||||||
|
type: String,
|
||||||
|
required: false, // 邀请链接模式下需要
|
||||||
|
},
|
||||||
|
qrCodeUrl: {
|
||||||
|
type: String,
|
||||||
|
required: false, // 邀请链接模式下提供,直接使用后端返回的二维码
|
||||||
},
|
},
|
||||||
mode: {
|
mode: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "promote", // 例如 "promote" | "invitation"
|
default: "promote", // 例如 "promote" | "invitation"
|
||||||
},
|
},
|
||||||
|
productType: {
|
||||||
|
type: String,
|
||||||
|
required: false, // 产品类型,用于确定海报目录(推广模式需要)
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const { linkIdentifier, mode } = toRefs(props);
|
const { linkIdentifier, fullLink, inviteLink, qrCodeUrl, mode, productType } = toRefs(props);
|
||||||
const posterCanvasRefs = ref([]); // 用于绘制海报的canvas数组
|
const posterCanvasRefs = ref([]); // 用于绘制海报的canvas数组
|
||||||
const currentIndex = ref(0); // 当前显示的海报索引
|
const currentIndex = ref(0); // 当前选中的海报索引
|
||||||
const postersGenerated = ref([]); // 标记海报是否已经生成过,将在onMounted中初始化
|
const postersGenerated = ref([]); // 标记海报是否已经生成过,将在onMounted中初始化
|
||||||
const show = defineModel("show");
|
const posterLoading = ref([]); // 单张海报加载状态
|
||||||
|
const isGeneratingPosters = ref(true); // 是否正在生成所有海报
|
||||||
|
const generatedCount = ref(0); // 已生成的海报数量
|
||||||
|
|
||||||
// 微信环境检测
|
// 微信环境检测
|
||||||
const isWeChat = computed(() => {
|
const isWeChat = computed(() => {
|
||||||
@@ -111,35 +110,90 @@ const showImageGuide = ref(false);
|
|||||||
const currentImageUrl = ref('');
|
const currentImageUrl = ref('');
|
||||||
const imageGuideTitle = ref('');
|
const imageGuideTitle = ref('');
|
||||||
const url = computed(() => {
|
const url = computed(() => {
|
||||||
const baseUrl = window.location.origin; // 获取当前站点的域名
|
if (mode.value === "invitation" && inviteLink.value) {
|
||||||
return mode.value === "promote"
|
// 邀请模式:使用完整的邀请链接(已包含域名)
|
||||||
? `${baseUrl}/agent/promotionInquire/` // 使用动态的域名
|
return inviteLink.value;
|
||||||
: `${baseUrl}/agent/invitationAgentApply/`;
|
}
|
||||||
|
// 推广链接模式:使用后端返回的完整短链
|
||||||
|
if (mode.value === "promote" && fullLink.value) {
|
||||||
|
return fullLink.value;
|
||||||
|
}
|
||||||
|
// 如果没有完整链接,返回空字符串(不应该发生,因为后端应该总是返回 full_link)
|
||||||
|
console.warn("推广链接模式但未提供 fullLink");
|
||||||
|
return "";
|
||||||
});
|
});
|
||||||
|
|
||||||
// 海报图片数组
|
// 海报图片数组
|
||||||
const posterImages = ref([]);
|
const posterImages = ref([]);
|
||||||
|
|
||||||
// QR码位置配置(为每个海报单独配置)
|
// 产品类型到海报目录的映射
|
||||||
const qrCodePositions = ref({
|
const productTypeToDirMap = {
|
||||||
promote: [
|
'riskassessment': 'riskassessment',
|
||||||
{ x: 180, y: 1440, size: 300 }, // tg_qrcode_1.png
|
'companyinfo': 'companyinfo',
|
||||||
{ x: 525, y: 1955, size: 500 }, // tg_qrcode_2.jpg
|
'preloanbackgroundcheck': 'backgroundcheck',
|
||||||
{ x: 525, y: 1955, size: 500 }, // tg_qrcode_3.jpg
|
'marriage': 'marriage',
|
||||||
{ x: 525, y: 1955, size: 500 }, // tg_qrcode_4.jpg
|
'homeservice': 'homeservice',
|
||||||
{ x: 525, y: 1955, size: 500 }, // tg_qrcode_5.jpg
|
'backgroundcheck': 'backgroundcheck',
|
||||||
{ x: 525, y: 1955, size: 500 }, // tg_qrcode_6.jpg
|
'consumerFinanceReport': 'consumerFinanceReport',
|
||||||
{ x: 255, y: 940, size: 250 }, // tg_qrcode_7.jpg
|
'invitation': 'invitation'
|
||||||
{ x: 255, y: 940, size: 250 }, // tg_qrcode_8.jpg
|
};
|
||||||
],
|
|
||||||
// invitation模式的配置 (yq_qrcode)
|
|
||||||
invitation: [
|
|
||||||
{ x: 360, y: -1370, size: 360 }, // yq_qrcode_1.png
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
// 处理轮播图切换事件
|
// QR码位置配置(为每个产品类型的每张海报单独配置)
|
||||||
const onSwipeChange = (index) => {
|
// 格式:{ 产品类型: [{ x, y, size }, ...] },索引对应海报编号(01-04)
|
||||||
|
const qrCodePositions = {
|
||||||
|
// 风险评估
|
||||||
|
riskassessment: [
|
||||||
|
{ x: 310, y: 505, size: 325 }, // riskassessment_01.jpg
|
||||||
|
{ x: 310, y: 290, size: 325 }, // riskassessment_02.jpg
|
||||||
|
{ x: 602, y: 670, size: 680 }, // riskassessment_03.jpg
|
||||||
|
{ x: 602, y: 1150, size: 680 }, // riskassessment_04.jpg
|
||||||
|
],
|
||||||
|
// 企业信息
|
||||||
|
companyinfo: [
|
||||||
|
{ x: 602, y: 1305, size: 680 }, // companyinfo_01.jpg
|
||||||
|
{ x: 602, y: 1280, size: 680 }, // companyinfo_02.jpg
|
||||||
|
{ x: 602, y: 1450, size: 680 }, // companyinfo_03.jpg
|
||||||
|
{ x: 602, y: 800, size: 680 }, // companyinfo_04.jpg
|
||||||
|
],
|
||||||
|
// 入职背调
|
||||||
|
backgroundcheck: [
|
||||||
|
{ x: 602, y: 800, size: 680 }, // backgroundcheck_01.jpg
|
||||||
|
{ x: 602, y: 1535, size: 680 }, // backgroundcheck_02.jpg
|
||||||
|
{ x: 602, y: 850, size: 680 }, // backgroundcheck_03.jpg
|
||||||
|
{ x: 602, y: 1080, size: 680 }, // backgroundcheck_04.jpg
|
||||||
|
],
|
||||||
|
// 婚姻风险
|
||||||
|
marriage: [
|
||||||
|
{ x: 965, y: 1050, size: 680 }, // marriage_01.jpg
|
||||||
|
{ x: 602, y: 1050, size: 680 }, // marriage_02.jpg
|
||||||
|
{ x: 225, y: 1855, size: 680 }, // marriage_03.jpg
|
||||||
|
{ x: 602, y: 880, size: 680 }, // marriage_04.jpg
|
||||||
|
],
|
||||||
|
// 家政服务
|
||||||
|
homeservice: [
|
||||||
|
{ x: 1080, y: 700, size: 680 }, // homeservice_01.jpg
|
||||||
|
{ x: 1080, y: 1155, size: 680 }, // homeservice_02.jpg
|
||||||
|
{ x: 965, y: 768, size: 680 }, // homeservice_03.jpg
|
||||||
|
{ x: 998, y: 1012, size: 680 }, // homeservice_04.jpg
|
||||||
|
],
|
||||||
|
// 消费金融报告
|
||||||
|
consumerFinanceReport: [
|
||||||
|
{ x: 602, y: 920, size: 680 }, // consumerFinanceReport_01.jpg
|
||||||
|
{ x: 602, y: 920, size: 680 }, // consumerFinanceReport_02.jpg
|
||||||
|
{ x: 1012, y: 1160, size: 680 }, // consumerFinanceReport_03.jpg
|
||||||
|
{ x: 1055, y: 1060, size: 680 }, // consumerFinanceReport_04.jpg
|
||||||
|
],
|
||||||
|
// 邀请好友
|
||||||
|
invitation: [
|
||||||
|
{ x: 602, y: 1035, size: 680 }, // invitation_01.jpg
|
||||||
|
{ x: 135, y: 1150, size: 680 }, // invitation_02.jpg
|
||||||
|
{ x: 602, y: 970, size: 680 }, // invitation_03.jpg
|
||||||
|
{ x: 945, y: 1150, size: 680 }, // invitation_04.jpg
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// 选择海报
|
||||||
|
const selectPoster = (index) => {
|
||||||
currentIndex.value = index;
|
currentIndex.value = index;
|
||||||
if (!postersGenerated.value[index]) {
|
if (!postersGenerated.value[index]) {
|
||||||
generatePoster(index);
|
generatePoster(index);
|
||||||
@@ -149,45 +203,46 @@ const onSwipeChange = (index) => {
|
|||||||
// 加载海报图片
|
// 加载海报图片
|
||||||
const loadPosterImages = async () => {
|
const loadPosterImages = async () => {
|
||||||
const images = [];
|
const images = [];
|
||||||
const basePrefix = mode.value === "promote" ? "tg_qrcode_" : "yq_qrcode_";
|
|
||||||
|
|
||||||
// 根据模式确定要加载的图片编号
|
// 确定海报目录
|
||||||
const imageNumbers = mode.value === "promote" ? [1, 2, 3, 4, 5, 6, 7, 8] : [1];
|
let posterDir = 'invitation'; // 默认使用邀请目录
|
||||||
|
if (mode.value === "promote" && productType.value) {
|
||||||
|
// 推广模式:根据产品类型确定目录
|
||||||
|
posterDir = productTypeToDirMap[productType.value] || 'riskassessment';
|
||||||
|
} else if (mode.value === "invitation") {
|
||||||
|
// 邀请模式:使用邀请目录
|
||||||
|
posterDir = 'invitation';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载4张海报(01-04)
|
||||||
|
const imageNumbers = [1, 2, 3, 4];
|
||||||
|
|
||||||
// 加载图片
|
|
||||||
for (const i of imageNumbers) {
|
for (const i of imageNumbers) {
|
||||||
// 尝试加载 .png 文件
|
const imagePath = `/image/poster/${posterDir}/${posterDir}_${String(i).padStart(2, '0')}.jpg`;
|
||||||
try {
|
|
||||||
const module = await import(
|
// 使用 new Image() 预加载图片,确保图片存在
|
||||||
`@/assets/images/${basePrefix}${i}.png`
|
const img = new Image();
|
||||||
);
|
img.crossOrigin = 'anonymous'; // 如果需要跨域
|
||||||
images.push(module.default);
|
|
||||||
continue; // 如果成功加载了 png,则跳过后续的 jpg 尝试
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(
|
|
||||||
`Image ${basePrefix}${i}.png not found, trying jpg...`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果 .png 不存在,尝试加载 .jpg 文件
|
|
||||||
try {
|
try {
|
||||||
const module = await import(
|
await new Promise((resolve, reject) => {
|
||||||
`@/assets/images/${basePrefix}${i}.jpg`
|
img.onload = () => {
|
||||||
);
|
// 图片加载成功,使用完整路径
|
||||||
images.push(module.default);
|
images.push(imagePath);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
img.onerror = () => {
|
||||||
|
console.warn(`Failed to load poster: ${imagePath}`);
|
||||||
|
// 如果加载失败,仍然添加路径,让 canvas 处理错误
|
||||||
|
images.push(imagePath);
|
||||||
|
resolve(); // 继续加载其他图片
|
||||||
|
};
|
||||||
|
img.src = imagePath;
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(
|
console.error(`Error loading poster ${i}:`, error);
|
||||||
`Image ${basePrefix}${i}.jpg not found either, using fallback.`
|
// 即使出错也添加路径
|
||||||
);
|
images.push(imagePath);
|
||||||
if (i === 1) {
|
|
||||||
// 如果第一张也不存在,创建一个空白图片
|
|
||||||
const emptyImg = new Image();
|
|
||||||
emptyImg.width = 600;
|
|
||||||
emptyImg.height = 800;
|
|
||||||
images.push(emptyImg.src);
|
|
||||||
} else if (images.length > 0) {
|
|
||||||
images.push(images[0]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,6 +253,15 @@ onMounted(async () => {
|
|||||||
posterImages.value = await loadPosterImages();
|
posterImages.value = await loadPosterImages();
|
||||||
// 根据加载的图片数量初始化postersGenerated数组
|
// 根据加载的图片数量初始化postersGenerated数组
|
||||||
postersGenerated.value = Array(posterImages.value.length).fill(false);
|
postersGenerated.value = Array(posterImages.value.length).fill(false);
|
||||||
|
posterLoading.value = Array(posterImages.value.length).fill(true);
|
||||||
|
generatedCount.value = 0;
|
||||||
|
isGeneratingPosters.value = true;
|
||||||
|
// 自动生成所有海报
|
||||||
|
if (url.value) {
|
||||||
|
for (let i = 0; i < posterImages.value.length; i++) {
|
||||||
|
generatePoster(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 生成海报并合成二维码
|
// 生成海报并合成二维码
|
||||||
@@ -225,51 +289,98 @@ const generatePoster = async (index) => {
|
|||||||
// 2. 绘制海报图片
|
// 2. 绘制海报图片
|
||||||
ctx.drawImage(posterImg, 0, 0);
|
ctx.drawImage(posterImg, 0, 0);
|
||||||
|
|
||||||
// 3. 生成二维码
|
// 3. 生成或加载二维码
|
||||||
QRCode.toDataURL(
|
const loadQRCode = (qrCodeDataUrl) => {
|
||||||
generalUrl(),
|
// 4. 加载二维码图片
|
||||||
{ width: 150, margin: 0 },
|
const qrCodeImg = new Image();
|
||||||
(err, qrCodeUrl) => {
|
qrCodeImg.src = qrCodeDataUrl;
|
||||||
if (err) {
|
qrCodeImg.onload = () => {
|
||||||
console.error(err);
|
// 获取当前海报的二维码位置配置
|
||||||
return;
|
let positionConfig = null;
|
||||||
|
|
||||||
|
if (mode.value === "promote" && productType.value) {
|
||||||
|
// 推广模式:根据产品类型获取配置
|
||||||
|
const dirName = productTypeToDirMap[productType.value] || 'riskassessment';
|
||||||
|
const positions = qrCodePositions[dirName] || qrCodePositions['riskassessment'];
|
||||||
|
positionConfig = positions[index] || positions[0];
|
||||||
|
} else if (mode.value === "invitation") {
|
||||||
|
// 邀请模式:使用邀请配置
|
||||||
|
const positions = qrCodePositions['invitation'];
|
||||||
|
positionConfig = positions[index] || positions[0];
|
||||||
|
} else {
|
||||||
|
// 默认配置
|
||||||
|
positionConfig = { x: 180, y: 1440, size: 300 };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 加载二维码图片
|
const position = positionConfig;
|
||||||
const qrCodeImg = new Image();
|
|
||||||
qrCodeImg.src = qrCodeUrl;
|
|
||||||
qrCodeImg.onload = () => {
|
|
||||||
// 获取当前海报的二维码位置配置
|
|
||||||
const positions = qrCodePositions.value[mode.value];
|
|
||||||
const position = positions[index] || positions[0]; // 如果没有对应索引的配置,则使用第一个配置
|
|
||||||
|
|
||||||
// 计算Y坐标(负值表示从底部算起的位置)
|
// 计算Y坐标(负值表示从底部算起的位置)
|
||||||
const qrY =
|
const qrY =
|
||||||
position.y < 0
|
position.y < 0
|
||||||
? posterImg.height + position.y
|
? posterImg.height + position.y
|
||||||
: position.y;
|
: position.y;
|
||||||
|
|
||||||
// 绘制二维码
|
// 绘制二维码
|
||||||
ctx.drawImage(
|
ctx.drawImage(
|
||||||
qrCodeImg,
|
qrCodeImg,
|
||||||
position.x,
|
position.x,
|
||||||
qrY,
|
qrY,
|
||||||
position.size,
|
position.size,
|
||||||
position.size
|
position.size
|
||||||
);
|
);
|
||||||
|
|
||||||
// 标记海报已生成
|
// 标记海报已生成
|
||||||
postersGenerated.value[index] = true;
|
postersGenerated.value[index] = true;
|
||||||
};
|
posterLoading.value[index] = false;
|
||||||
}
|
generatedCount.value += 1;
|
||||||
);
|
if (generatedCount.value >= posterImages.value.length) {
|
||||||
|
isGeneratingPosters.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 生成二维码
|
||||||
|
const generateQRCode = () => {
|
||||||
|
QRCode.toDataURL(
|
||||||
|
generalUrl(),
|
||||||
|
{ width: 150, margin: 0 },
|
||||||
|
(err, qrCodeDataUrl) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loadQRCode(qrCodeDataUrl);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 邀请模式:直接在前端生成二维码(使用 inviteLink)
|
||||||
|
// 推广模式:也在前端生成二维码(使用 linkIdentifier 构建的 URL)
|
||||||
|
generateQRCode();
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// 监听 show 变化,show 为 true 时生成海报
|
watch(url, (newVal) => {
|
||||||
watch(show, (newVal) => {
|
if (newVal && posterImages.value.length > 0) {
|
||||||
if (newVal && !postersGenerated.value[currentIndex.value]) {
|
for (let i = 0; i < posterImages.value.length; i++) {
|
||||||
generatePoster(currentIndex.value); // 当弹窗显示且当前海报未生成时生成海报
|
if (!postersGenerated.value[i]) {
|
||||||
|
generatePoster(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听产品类型变化,重新加载海报
|
||||||
|
watch([productType, mode], async () => {
|
||||||
|
posterImages.value = await loadPosterImages();
|
||||||
|
postersGenerated.value = Array(posterImages.value.length).fill(false);
|
||||||
|
posterLoading.value = Array(posterImages.value.length).fill(true);
|
||||||
|
generatedCount.value = 0;
|
||||||
|
isGeneratingPosters.value = true;
|
||||||
|
if (url.value) {
|
||||||
|
for (let i = 0; i < posterImages.value.length; i++) {
|
||||||
|
generatePoster(i);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -289,7 +400,7 @@ const shareToFriend = () => {
|
|||||||
? "扫码查看一查查推广信息"
|
? "扫码查看一查查推广信息"
|
||||||
: "扫码申请一查查代理权限",
|
: "扫码申请一查查代理权限",
|
||||||
link: shareUrl,
|
link: shareUrl,
|
||||||
imgUrl: "https://www.quannengcha.com/logo.png"
|
imgUrl: "https://www.onecha.cn/logo.png"
|
||||||
};
|
};
|
||||||
|
|
||||||
configWeixinShare(shareConfig);
|
configWeixinShare(shareConfig);
|
||||||
@@ -314,7 +425,7 @@ const shareToTimeline = () => {
|
|||||||
? "扫码查看一查查推广信息"
|
? "扫码查看一查查推广信息"
|
||||||
: "扫码申请一查查代理权限",
|
: "扫码申请一查查代理权限",
|
||||||
link: shareUrl,
|
link: shareUrl,
|
||||||
imgUrl: "https://www.quannengcha.com/logo.png"
|
imgUrl: "https://www.onecha.cn/logo.png"
|
||||||
};
|
};
|
||||||
|
|
||||||
configWeixinShare(shareConfig);
|
configWeixinShare(shareConfig);
|
||||||
@@ -484,7 +595,10 @@ const tryShareAPI = async (dataURL) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const generalUrl = () => {
|
const generalUrl = () => {
|
||||||
return url.value + encodeURIComponent(linkIdentifier.value);
|
// 直接使用 url computed 属性,它已经处理了所有情况
|
||||||
|
// 推广模式:优先使用 fullLink,否则使用 linkIdentifier 构建
|
||||||
|
// 邀请模式:优先使用 inviteLink,否则使用 linkIdentifier 构建
|
||||||
|
return url.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyUrl = () => {
|
const copyUrl = () => {
|
||||||
@@ -525,82 +639,109 @@ const copyToClipboard = (text) => {
|
|||||||
.qrcode-popup-container {
|
.qrcode-popup-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
max-height: 95vh;
|
height: calc(100vh - 46px);
|
||||||
|
max-height: calc(100vh - 46px);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
.qrcode-content {
|
.qrcode-content {
|
||||||
|
position: relative;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
padding: 0.75rem;
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 小屏设备优化 */
|
/* 小屏设备优化 */
|
||||||
@media (max-width: 375px) {
|
@media (max-width: 375px) {
|
||||||
.qrcode-content {
|
.qrcode-content {
|
||||||
padding: 0.5rem;
|
padding: 0.25rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 中等及以上屏幕 */
|
/* 中等及以上屏幕 */
|
||||||
@media (min-width: 640px) {
|
@media (min-width: 640px) {
|
||||||
.qrcode-content {
|
.qrcode-content {
|
||||||
padding: 1rem;
|
padding: 0.75rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.poster-swiper {
|
/* 网格布局:2x2显示4张海报 */
|
||||||
height: calc(95vh - 180px);
|
.poster-grid {
|
||||||
min-height: 300px;
|
display: grid;
|
||||||
max-height: 500px;
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 0.3rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
align-items: start;
|
||||||
|
justify-items: center;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 小屏设备:更小的海报高度 */
|
/* 小屏设备优化 */
|
||||||
@media (max-width: 375px) {
|
@media (max-width: 375px) {
|
||||||
.poster-swiper {
|
.poster-grid {
|
||||||
height: calc(95vh - 160px);
|
gap: 0.25rem;
|
||||||
min-height: 280px;
|
|
||||||
max-height: 400px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 中等屏幕 */
|
/* 中等及以上屏幕 */
|
||||||
@media (min-width: 640px) and (max-width: 767px) {
|
@media (min-width: 640px) {
|
||||||
.poster-swiper {
|
.poster-grid {
|
||||||
height: calc(95vh - 190px);
|
gap: 0.75rem;
|
||||||
max-height: 520px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 大屏幕 */
|
.poster-item {
|
||||||
@media (min-width: 768px) {
|
position: relative;
|
||||||
.poster-swiper {
|
width: 100%;
|
||||||
height: calc(95vh - 200px);
|
/* 海报比例 1889:3425 ≈ 0.551 */
|
||||||
max-height: 600px;
|
cursor: pointer;
|
||||||
}
|
border: 3px solid transparent;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster-skeleton {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster-loading-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
pointer-events: none;
|
||||||
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.6), rgba(249, 250, 251, 0.6));
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster-item-selected {
|
||||||
|
border-color: #64b5f6;
|
||||||
|
border-width: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.poster-canvas {
|
.poster-canvas {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
border-radius: 0.25rem;
|
||||||
|
display: block;
|
||||||
.swipe-tip {
|
|
||||||
animation: fadeInOut 2s infinite;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swipe-icon {
|
|
||||||
display: inline-block;
|
|
||||||
animation: slideLeftRight 1.5s infinite;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
|
||||||
.swipe-icon {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.share-icon {
|
.share-icon {
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from "vue";
|
import { ref, computed } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
import { useDialogStore } from "@/stores/dialogStore";
|
import { useDialogStore } from "@/stores/dialogStore";
|
||||||
|
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 router = useRouter();
|
||||||
const dialogStore = useDialogStore();
|
const dialogStore = useDialogStore();
|
||||||
const agentStore = useAgentStore();
|
const agentStore = useAgentStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
import { showToast } from "vant";
|
const { runWithCaptcha } = useAliyunCaptcha();
|
||||||
// 表单数据
|
// 表单数据
|
||||||
const realName = ref("");
|
const realName = ref("");
|
||||||
const idCard = ref("");
|
const idCard = ref("");
|
||||||
@@ -47,25 +55,26 @@ const canSubmit = computed(() => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 发送验证码
|
// 发送验证码(使用阿里云滑块验证码防刷)
|
||||||
async function sendVerificationCode() {
|
async function sendVerificationCode() {
|
||||||
if (isCountingDown.value || !isPhoneNumberValid.value) return;
|
if (isCountingDown.value || !isPhoneNumberValid.value) return;
|
||||||
if (!isPhoneNumberValid.value) {
|
if (!isPhoneNumberValid.value) {
|
||||||
showToast({ message: "请输入有效的手机号" });
|
showToast({ message: "请输入有效的手机号" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { data, error } = await useApiFetch("auth/sendSms")
|
runWithCaptcha(
|
||||||
.post({ mobile: phoneNumber.value, actionType: "realName" })
|
(captchaVerifyParam) => useApiFetch("auth/sendSms")
|
||||||
.json();
|
.post({ mobile: phoneNumber.value, actionType: "realName", captchaVerifyParam })
|
||||||
|
.json(),
|
||||||
if (data.value && !error.value) {
|
(result) => {
|
||||||
if (data.value.code === 200) {
|
if (result && result.code === 200) {
|
||||||
showToast({ message: "获取成功" });
|
showToast({ message: "获取成功" });
|
||||||
startCountdown();
|
startCountdown();
|
||||||
} else {
|
} else if (result) {
|
||||||
showToast(data.value.msg);
|
showToast(result.msg || "发送失败");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function startCountdown() {
|
function startCountdown() {
|
||||||
@@ -104,14 +113,12 @@ async function handleSubmit() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data, error } = await useApiFetch("/agent/real_name")
|
const { data, error } = await realNameAuth({
|
||||||
.post({
|
name: realName.value,
|
||||||
name: realName.value,
|
id_card: idCard.value,
|
||||||
id_card: idCard.value,
|
mobile: phoneNumber.value,
|
||||||
mobile: phoneNumber.value,
|
code: verificationCode.value,
|
||||||
code: verificationCode.value,
|
});
|
||||||
})
|
|
||||||
.json();
|
|
||||||
|
|
||||||
if (data.value && !error.value) {
|
if (data.value && !error.value) {
|
||||||
if (data.value.code === 200) {
|
if (data.value.code === 200) {
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class=" m-4">
|
<div class=" m-4">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<img src="@/assets/images/report/wxts_icon.png" alt="温馨提示" class="tips-icon" />
|
<img src="@/assets/images/report/wxts_icon.png" :alt="title" class="tips-icon" />
|
||||||
<span class="tips-title">温馨提示!</span>
|
<span class="tips-title">{{ title }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-1 ml-4">
|
<div class="mt-1 ml-4">
|
||||||
<van-text-ellipsis rows="2" :content="content" expand-text="展开" collapse-text="收起" />
|
<van-text-ellipsis v-if="!defaultExpanded" :rows="2" :content="content" expand-text="展开"
|
||||||
|
collapse-text="收起" />
|
||||||
|
<div v-else class="tips-content">
|
||||||
|
{{ content }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -17,6 +21,14 @@ const props = defineProps({
|
|||||||
content: {
|
content: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '温馨提示!'
|
||||||
|
},
|
||||||
|
defaultExpanded: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -43,7 +55,7 @@ const isExpanded = ref(false);
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tips-content {
|
.tips-content {
|
||||||
font-size: 14px;
|
font-size: 16px;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,8 +89,7 @@ const isExpanded = ref(false);
|
|||||||
}
|
}
|
||||||
|
|
||||||
:deep(.van-text-ellipsis) {
|
:deep(.van-text-ellipsis) {
|
||||||
font-size: 12px;
|
font-size: 14px;
|
||||||
color: #999999;
|
color: #999999;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="features && features.length > 0" :class="containerClass">
|
<div v-if="displayFeatures.length > 0" :class="containerClass">
|
||||||
<div class="mb-3 text-base font-semibold flex items-center" :style="titleStyle">
|
<div class="mb-3 text-base font-semibold flex items-center" :style="titleStyle">
|
||||||
<div class="w-1 h-5 rounded-full mr-2"
|
<div class="w-1 h-5 rounded-full mr-2"
|
||||||
style="background: linear-gradient(to bottom, var(--van-theme-primary), var(--van-theme-primary-dark));">
|
style="background: linear-gradient(to bottom, var(--van-theme-primary), var(--van-theme-primary-dark));">
|
||||||
@@ -7,9 +7,27 @@
|
|||||||
报告包含内容
|
报告包含内容
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-4 gap-2">
|
<div class="grid grid-cols-4 gap-2">
|
||||||
<template v-for="(feature, index) in features" :key="feature.id">
|
<template v-for="(feature, index) in displayFeatures" :key="feature.id">
|
||||||
|
<!-- 个人风险评估报告:固定展示模块 -->
|
||||||
|
<template v-if="feature.api_id === 'RISKASSESSMENT_REPORT'">
|
||||||
|
<div v-for="(module, moduleIndex) in riskAssessmentReportModules"
|
||||||
|
:key="`${feature.id}-${moduleIndex}`"
|
||||||
|
class="aspect-square rounded-xl text-center text-sm text-gray-700 font-medium flex flex-col items-center justify-center p-2"
|
||||||
|
:class="getCardClass(moduleIndex)">
|
||||||
|
<div class="mb-1">
|
||||||
|
<img :src="`/inquire_icons/${module.icon}`" :alt="module.name"
|
||||||
|
class="w-6 h-6 mx-auto"
|
||||||
|
@error="handleIconError" />
|
||||||
|
</div>
|
||||||
|
<div class="text-xs leading-tight font-medium"
|
||||||
|
style="word-break: break-all; line-height: 1.1; min-height: 28px; display: flex; align-items: center; justify-content: center;">
|
||||||
|
{{ module.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- FLXG0V4B 特殊处理:显示8个独立的案件类型 -->
|
<!-- FLXG0V4B 特殊处理:显示8个独立的案件类型 -->
|
||||||
<template v-if="feature.api_id === 'FLXG0V4B'">
|
<template v-else-if="feature.api_id === 'FLXG0V4B'">
|
||||||
<div v-for="(caseType, caseIndex) in [
|
<div v-for="(caseType, caseIndex) in [
|
||||||
{ name: '管辖案件', icon: 'beijianguanrenyuan.svg' },
|
{ name: '管辖案件', icon: 'beijianguanrenyuan.svg' },
|
||||||
{ name: '刑事案件', icon: 'xingshi.svg' },
|
{ name: '刑事案件', icon: 'xingshi.svg' },
|
||||||
@@ -181,12 +199,29 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import {
|
||||||
|
isRiskAssessmentProduct,
|
||||||
|
resolveInquireProductEn,
|
||||||
|
riskAssessmentReportModules,
|
||||||
|
} from '@/constants/riskAssessmentReportFeatures';
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
features: {
|
features: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
/** 查询/推广产品标识(路由 feature 或推广链接 product_en) */
|
||||||
|
inquireProduct: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
/** 推广查询等场景下的完整产品数据,用于 fallback product_en */
|
||||||
|
inquireFeatureData: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
containerClass: {
|
containerClass: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
@@ -197,6 +232,17 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const resolvedProductEn = computed(() =>
|
||||||
|
resolveInquireProductEn(props.inquireProduct, props.inquireFeatureData)
|
||||||
|
);
|
||||||
|
|
||||||
|
const displayFeatures = computed(() => {
|
||||||
|
if (isRiskAssessmentProduct(resolvedProductEn.value)) {
|
||||||
|
return [{ id: 'riskassessment-report', api_id: 'RISKASSESSMENT_REPORT', name: '' }];
|
||||||
|
}
|
||||||
|
return props.features || [];
|
||||||
|
});
|
||||||
|
|
||||||
// 获取功能图标
|
// 获取功能图标
|
||||||
const getFeatureIcon = (apiId) => {
|
const getFeatureIcon = (apiId) => {
|
||||||
const iconMap = {
|
const iconMap = {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from "vue";
|
import { useShareReport } from "@/composables/useShareReport";
|
||||||
import { showToast, showDialog } from "vant";
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
orderId: {
|
orderId: {
|
||||||
@@ -21,104 +20,16 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const isLoading = ref(false);
|
const { isLoading, handleShare } = useShareReport();
|
||||||
|
|
||||||
const copyToClipboard = async (text) => {
|
const onShare = () => {
|
||||||
await navigator.clipboard.writeText(text);
|
handleShare(props.orderId, props.orderNo, props.isExample, props.disabled);
|
||||||
showToast({
|
|
||||||
type: "success",
|
|
||||||
message: "链接已复制到剪贴板",
|
|
||||||
position: "bottom",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const handleShare = async () => {
|
|
||||||
if (isLoading.value || props.disabled) return;
|
|
||||||
// 如果是示例模式,直接分享当前URL
|
|
||||||
if (props.isExample) {
|
|
||||||
try {
|
|
||||||
const currentUrl = window.location.href;
|
|
||||||
await copyToClipboard(currentUrl);
|
|
||||||
showToast({
|
|
||||||
type: "success",
|
|
||||||
message: "示例链接已复制到剪贴板",
|
|
||||||
position: "bottom",
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
showToast({
|
|
||||||
type: "fail",
|
|
||||||
message: "复制链接失败",
|
|
||||||
position: "bottom",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 优先使用 orderId,如果没有则使用 orderNo
|
|
||||||
const orderIdentifier = props.orderId || props.orderNo;
|
|
||||||
if (!orderIdentifier) {
|
|
||||||
showToast({
|
|
||||||
type: "fail",
|
|
||||||
message: "缺少订单标识",
|
|
||||||
position: "bottom",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoading.value = true;
|
|
||||||
try {
|
|
||||||
// 根据实际使用的标识构建请求参数
|
|
||||||
const requestData = props.orderId
|
|
||||||
? { order_id: parseInt(props.orderId) }
|
|
||||||
: { order_no: props.orderNo };
|
|
||||||
|
|
||||||
const { data, error } = await useApiFetch("/query/generate_share_link")
|
|
||||||
.post(requestData)
|
|
||||||
.json();
|
|
||||||
|
|
||||||
if (error.value) {
|
|
||||||
throw new Error(error.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.value?.code === 200 && data.value.data?.share_link) {
|
|
||||||
const baseUrl = window.location.origin;
|
|
||||||
const linkId = encodeURIComponent(data.value.data.share_link);
|
|
||||||
const fullShareUrl = `${baseUrl}/report/share/${linkId}`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 显示确认对话框
|
|
||||||
await showDialog({
|
|
||||||
title: "分享链接已生成",
|
|
||||||
message: "链接将在7天后过期,是否复制到剪贴板?",
|
|
||||||
confirmButtonText: "复制链接",
|
|
||||||
cancelButtonText: "取消",
|
|
||||||
showCancelButton: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 用户点击确认后复制链接
|
|
||||||
await copyToClipboard(fullShareUrl);
|
|
||||||
} catch (dialogErr) {
|
|
||||||
// 用户点击取消按钮时,dialogErr 会是 'cancel'
|
|
||||||
// 这里不需要显示错误提示,直接返回即可
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error(data.value?.message || "生成分享链接失败");
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
showToast({
|
|
||||||
type: "fail",
|
|
||||||
message: err.message || "生成分享链接失败",
|
|
||||||
position: "bottom",
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="bg-primary-second border border-primary-second rounded-[40px] px-3 py-1 flex items-center justify-center cursor-pointer hover:bg-primary-600 transition-colors duration-200"
|
<div class="bg-primary-second border border-primary-second rounded-[40px] px-3 py-1 flex items-center justify-center cursor-pointer hover:bg-primary-600 transition-colors duration-200"
|
||||||
:class="{ 'opacity-50 cursor-not-allowed': isLoading || disabled }" @click="handleShare">
|
:class="{ 'opacity-50 cursor-not-allowed': isLoading || disabled }" @click="onShare">
|
||||||
<img src="@/assets/images/report/fx.png" alt="分享" class="w-4 h-4 mr-1" />
|
<img src="@/assets/images/report/fx.png" alt="分享" class="w-4 h-4 mr-1" />
|
||||||
<span class="text-white text-sm font-medium">
|
<span class="text-white text-sm font-medium">
|
||||||
{{ isLoading ? "生成中..." : (isExample ? "分享示例" : "分享报告") }}
|
{{ isLoading ? "生成中..." : (isExample ? "分享示例" : "分享报告") }}
|
||||||
|
|||||||
38
src/components/ShareReportButtonList.vue
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useShareReport } from "@/composables/useShareReport";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
orderId: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
orderNo: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { isLoading, handleShare } = useShareReport();
|
||||||
|
|
||||||
|
const onShare = () => {
|
||||||
|
handleShare(props.orderId, props.orderNo, false, props.disabled);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
class="flex-1 px-4 py-2 text-sm bg-white border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
:disabled="isLoading || disabled"
|
||||||
|
@click="onShare">
|
||||||
|
{{ isLoading ? "生成中..." : "分享" }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
/* 样式已通过 Tailwind CSS 类实现 */
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -1,14 +1,50 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="title-banner">
|
<div class="title-banner-wrapper">
|
||||||
<slot></slot>
|
<div class="title-banner">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
<!-- 下架按钮(仅钻石代理显示) -->
|
||||||
|
<button v-if="showOfflineButton" @click="handleOfflineClick" class="offline-button"
|
||||||
|
:class="{ 'offline-button-disabled': isOfflined }"
|
||||||
|
:disabled="isSubmitting || isOfflined">
|
||||||
|
{{ isSubmitting ? '下架中...' : isOfflined ? '已下架' : '下架' }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
// 不需要额外的 props 或逻辑,只是一个简单的样式组件
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
showOfflineButton: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
whitelistPrice: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
isSubmitting: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
isOfflined: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['offline-click'])
|
||||||
|
const handleOfflineClick = () => {
|
||||||
|
emit('offline-click')
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.title-banner-wrapper {
|
||||||
|
@apply flex items-center justify-center gap-2 relative;
|
||||||
|
}
|
||||||
|
|
||||||
.title-banner {
|
.title-banner {
|
||||||
@apply mx-auto mt-2 w-64 py-1.5 text-center text-white font-bold text-lg relative rounded-2xl;
|
@apply mx-auto mt-2 w-64 py-1.5 text-center text-white font-bold text-lg relative rounded-2xl;
|
||||||
background: var(--color-primary-second);
|
background: var(--color-primary-second);
|
||||||
@@ -20,4 +56,17 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.offline-button {
|
||||||
|
@apply px-3 py-1 text-xs rounded-lg bg-red-500 text-white font-medium;
|
||||||
|
@apply hover:bg-red-600 active:bg-red-700 transition-colors;
|
||||||
|
@apply disabled:opacity-50 disabled:cursor-not-allowed;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offline-button-disabled {
|
||||||
|
@apply bg-gray-400 cursor-not-allowed;
|
||||||
|
@apply hover:bg-gray-400 active:bg-gray-400;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="mb-4" @click="toAgentVip">
|
|
||||||
<img
|
|
||||||
src="@/assets/images/vip_banner.png"
|
|
||||||
class="rounded-xl shadow-lg"
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
const router = useRouter();
|
|
||||||
const toAgentVip = () => {
|
|
||||||
router.push({ name: "agentVipApply" });
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
|
||||||
63
src/components/WechatGuard.vue
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="!isWeChat" class="wechat-guard-overlay">
|
||||||
|
<div class="wechat-guard-content">
|
||||||
|
<van-icon name="warning-o" class="warning-icon" />
|
||||||
|
<h3 class="guard-title">请在微信中打开</h3>
|
||||||
|
<p class="guard-message">
|
||||||
|
本应用仅支持在微信浏览器中打开<br />
|
||||||
|
请使用微信扫描二维码或通过微信分享链接访问
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useEnv } from '@/composables/useEnv'
|
||||||
|
|
||||||
|
const { isWeChat } = useEnv()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.wechat-guard-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.85);
|
||||||
|
z-index: 99999;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechat-guard-content {
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
padding: 40px 32px;
|
||||||
|
max-width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-icon {
|
||||||
|
font-size: 64px;
|
||||||
|
color: #ff9800;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guard-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guard-message {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 0;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="isWeChat" class="wechat-overlay">
|
<div v-if="isWeChat" class="wechat-overlay">
|
||||||
|
<img src="@/assets/images/ysjjt.png" alt="Arrow" class="wechat-image" />
|
||||||
<div class="wechat-content">
|
<div class="wechat-content">
|
||||||
<p class="wechat-message">
|
<p class="wechat-message">
|
||||||
点击右上角的<van-icon class="ml-2" name="weapp-nav" /><br />然后点击在浏览器中打开
|
1. 点击右上角<span class="dots">...</span><br />
|
||||||
|
2. 选择<span class="browser-text">手机浏览器</span>打开
|
||||||
|
</p>
|
||||||
|
<p class="tip-message">
|
||||||
|
如遇手机自带浏览器无法打开支付宝时,可重新在此页面选择其他浏览器打开;
|
||||||
</p>
|
</p>
|
||||||
<img src="@/assets/images/llqdk.jpg" alt="In WeChat" class="wechat-image" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -37,36 +41,54 @@ onMounted(() => {
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
background: rgba(0, 0, 0, 0.7);
|
background: rgba(0, 0, 0, 0.7);
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
display: flex;
|
}
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
/* 图片样式 - 定位到右上角 */
|
||||||
flex-direction: column;
|
.wechat-image {
|
||||||
|
position: absolute;
|
||||||
|
top: 30px;
|
||||||
|
right: 30px;
|
||||||
|
width: 100px;
|
||||||
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 遮罩中的内容 */
|
/* 遮罩中的内容 */
|
||||||
.wechat-content {
|
.wechat-content {
|
||||||
text-align: center;
|
position: absolute;
|
||||||
|
top: 20%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
text-align: left;
|
||||||
color: white;
|
color: white;
|
||||||
font-size: 16px;
|
width: 80%;
|
||||||
}
|
max-width: 400px;
|
||||||
|
|
||||||
/* 图片样式 */
|
|
||||||
.wechat-image {
|
|
||||||
/* position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0; */
|
|
||||||
margin-top: 20px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 提示信息的样式 */
|
/* 提示信息的样式 */
|
||||||
.wechat-message {
|
.wechat-message {
|
||||||
font-size: 24px;
|
font-size: 18px;
|
||||||
|
line-height: 1.8;
|
||||||
|
margin: 0 0 20px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 图标样式 */
|
/* 三个点特殊样式 */
|
||||||
.icon-more-vert {
|
.dots {
|
||||||
font-size: 20px;
|
font-size: 36px;
|
||||||
|
font-weight: bold;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 手机浏览器特殊颜色 */
|
||||||
|
.browser-text {
|
||||||
|
color: #ffd700;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 额外提示信息样式 */
|
||||||
|
.tip-message {
|
||||||
|
font-size: 14px;
|
||||||
|
color: white;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
226
src/components/WhitelistModuleDialog.vue
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
<template>
|
||||||
|
<van-popup v-model:show="show" position="bottom" round :style="{ height: '70%' }" class="whitelist-module-dialog">
|
||||||
|
<div class="flex flex-col h-full">
|
||||||
|
<!-- 标题栏 -->
|
||||||
|
<div class="flex items-center justify-between p-4 border-b">
|
||||||
|
<h3 class="text-lg font-bold">屏蔽模块</h3>
|
||||||
|
<van-icon name="cross" size="20" @click="close" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 内容区域 -->
|
||||||
|
<div class="flex-1 overflow-y-auto p-4">
|
||||||
|
<div v-if="loading" class="flex items-center justify-center h-40">
|
||||||
|
<van-loading type="spinner" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="featureList.length === 0" class="flex items-center justify-center h-40 text-gray-500">
|
||||||
|
暂无可屏蔽的模块
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<div class="mb-4 text-sm text-gray-600">
|
||||||
|
选择要屏蔽的模块,屏蔽后该身份证号查询时将不显示这些模块的数据
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 模块列表 -->
|
||||||
|
<van-checkbox-group v-model="selectedFeatureIds">
|
||||||
|
<div v-for="feature in featureList" :key="feature.feature_id" class="mb-3">
|
||||||
|
<van-cell :title="feature.feature_name" :label="`价格:¥${feature.whitelist_price.toFixed(2)}`"
|
||||||
|
clickable @click="toggleFeature(feature.feature_id)">
|
||||||
|
<template #right-icon>
|
||||||
|
<van-checkbox :name="feature.feature_id" />
|
||||||
|
</template>
|
||||||
|
</van-cell>
|
||||||
|
</div>
|
||||||
|
</van-checkbox-group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部操作栏 -->
|
||||||
|
<div class="p-4 border-t bg-gray-50">
|
||||||
|
<div class="mb-3 flex items-center justify-between">
|
||||||
|
<span class="text-sm text-gray-600">已选择:{{ selectedFeatureIds.length }} 个模块</span>
|
||||||
|
<span class="text-lg font-bold text-red-500">
|
||||||
|
总计:¥{{ totalAmount.toFixed(2) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<van-button type="primary" block round :loading="isSubmitting"
|
||||||
|
:disabled="selectedFeatureIds.length === 0" @click="handleConfirm">
|
||||||
|
确认屏蔽
|
||||||
|
</van-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 支付弹窗 -->
|
||||||
|
<Payment v-model="showPayment" :data="paymentData"
|
||||||
|
:id="paymentId" :type="paymentType" />
|
||||||
|
</van-popup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { getWhitelistFeatures, createWhitelistOrder } from '@/api/agent'
|
||||||
|
import { showSuccessToast, showFailToast, showConfirmDialog } from 'vant'
|
||||||
|
import { useAgentStore } from '@/stores/agentStore'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import Payment from './Payment.vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
idCard: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
orderId: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const show = defineModel('show', { type: Boolean, default: false })
|
||||||
|
|
||||||
|
const agentStore = useAgentStore()
|
||||||
|
const { isDiamond } = storeToRefs(agentStore)
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const featureList = ref([])
|
||||||
|
const selectedFeatureIds = ref([])
|
||||||
|
const isSubmitting = ref(false)
|
||||||
|
|
||||||
|
// 支付相关状态
|
||||||
|
const showPayment = ref(false)
|
||||||
|
const paymentData = ref({
|
||||||
|
product_name: '',
|
||||||
|
sell_price: 0,
|
||||||
|
})
|
||||||
|
const paymentId = ref('')
|
||||||
|
const paymentType = ref('whitelist')
|
||||||
|
const currentOrderId = ref('')
|
||||||
|
|
||||||
|
// 计算总金额
|
||||||
|
const totalAmount = computed(() => {
|
||||||
|
return featureList.value
|
||||||
|
.filter(f => selectedFeatureIds.value.includes(f.feature_id))
|
||||||
|
.reduce((sum, f) => sum + f.whitelist_price, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 切换选择
|
||||||
|
const toggleFeature = (featureId) => {
|
||||||
|
const index = selectedFeatureIds.value.indexOf(featureId)
|
||||||
|
if (index > -1) {
|
||||||
|
selectedFeatureIds.value.splice(index, 1)
|
||||||
|
} else {
|
||||||
|
selectedFeatureIds.value.push(featureId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭弹窗
|
||||||
|
const close = () => {
|
||||||
|
show.value = false
|
||||||
|
selectedFeatureIds.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载可屏蔽的模块列表
|
||||||
|
const loadFeatures = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const { data, error } = await getWhitelistFeatures()
|
||||||
|
if (data.value && !error.value && data.value.code === 200) {
|
||||||
|
featureList.value = data.value.data.list || []
|
||||||
|
} else {
|
||||||
|
showFailToast(data.value?.msg || '获取模块列表失败')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('获取模块列表失败:', err)
|
||||||
|
showFailToast('获取模块列表失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认屏蔽
|
||||||
|
const handleConfirm = async () => {
|
||||||
|
if (selectedFeatureIds.value.length === 0) {
|
||||||
|
showFailToast('请至少选择一个模块')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认对话框
|
||||||
|
try {
|
||||||
|
await showConfirmDialog({
|
||||||
|
title: '确认屏蔽',
|
||||||
|
message: `确定要屏蔽 ${selectedFeatureIds.value.length} 个模块吗?总费用:¥${totalAmount.value.toFixed(2)}`,
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
return // 用户取消
|
||||||
|
}
|
||||||
|
|
||||||
|
isSubmitting.value = true
|
||||||
|
try {
|
||||||
|
// 1. 创建订单
|
||||||
|
const { data: orderData, error: orderError } = await createWhitelistOrder({
|
||||||
|
id_card: props.idCard,
|
||||||
|
feature_ids: selectedFeatureIds.value,
|
||||||
|
order_id: props.orderId || undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!orderData.value || orderError.value || orderData.value.code !== 200) {
|
||||||
|
showFailToast(orderData.value?.msg || '创建订单失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderId = orderData.value.data.order_id
|
||||||
|
const orderNo = orderData.value.data.order_no
|
||||||
|
const totalAmount = orderData.value.data.total_amount
|
||||||
|
|
||||||
|
// 2. 使用统一支付组件进行支付
|
||||||
|
// PaymentReq.Id 约定格式:白名单订单使用 "{idCard}|{featureApiId}" 格式
|
||||||
|
// 但批量订单使用订单号,需要根据后端接口调整
|
||||||
|
// 这里先使用订单号,如果后端需要特定格式,需要调整
|
||||||
|
paymentData.value = {
|
||||||
|
product_name: `模块屏蔽(${selectedFeatureIds.value.length}个模块)`,
|
||||||
|
sell_price: totalAmount,
|
||||||
|
}
|
||||||
|
paymentId.value = orderNo // 使用订单号作为支付ID
|
||||||
|
paymentType.value = 'whitelist'
|
||||||
|
currentOrderId.value = orderId
|
||||||
|
showPayment.value = true
|
||||||
|
} catch (err) {
|
||||||
|
console.error('屏蔽模块失败:', err)
|
||||||
|
showFailToast('屏蔽模块失败')
|
||||||
|
} finally {
|
||||||
|
isSubmitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听支付弹窗关闭,如果支付成功会跳转到支付结果页面
|
||||||
|
watch(showPayment, (newVal) => {
|
||||||
|
if (!newVal && currentOrderId.value) {
|
||||||
|
// 支付弹窗关闭,可能是支付完成(会跳转到结果页面)或用户取消
|
||||||
|
// 这里不做特殊处理,因为支付成功会跳转到结果页面
|
||||||
|
currentOrderId.value = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['success'])
|
||||||
|
|
||||||
|
// 监听弹窗显示,加载数据
|
||||||
|
watch(show, (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
// 验证是否为钻石代理
|
||||||
|
if (!isDiamond.value) {
|
||||||
|
showFailToast('只有钻石代理可以操作白名单')
|
||||||
|
close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loadFeatures()
|
||||||
|
selectedFeatureIds.value = []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.whitelist-module-dialog {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
172
src/composables/useAliyunCaptcha.js
Normal 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;
|
||||||
@@ -6,7 +6,9 @@ import { useAgentStore } from "@/stores/agentStore";
|
|||||||
// 创建全局的 fetch 实例
|
// 创建全局的 fetch 实例
|
||||||
const useApiFetch = createFetch({
|
const useApiFetch = createFetch({
|
||||||
// 统一从环境变量读取域名与前缀(不提供默认值)
|
// 统一从环境变量读取域名与前缀(不提供默认值)
|
||||||
baseUrl: `${import.meta.env.VITE_API_URL}${import.meta.env.VITE_API_PREFIX}`,
|
baseUrl: `${import.meta.env.VITE_API_URL}${
|
||||||
|
import.meta.env.VITE_API_PREFIX
|
||||||
|
}`,
|
||||||
options: {
|
options: {
|
||||||
async beforeFetch({ url, options }) {
|
async beforeFetch({ url, options }) {
|
||||||
showLoadingToast({
|
showLoadingToast({
|
||||||
@@ -28,13 +30,24 @@ const useApiFetch = createFetch({
|
|||||||
if (isWechat) {
|
if (isWechat) {
|
||||||
platform = "wxh5";
|
platform = "wxh5";
|
||||||
}
|
}
|
||||||
options.headers['X-Platform'] = platform
|
options.headers["X-Platform"] = platform;
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
options.headers = {
|
options.headers = {
|
||||||
...options.headers,
|
...options.headers,
|
||||||
Authorization: `${token}`,
|
Authorization: `${token}`,
|
||||||
};
|
};
|
||||||
|
console.log(
|
||||||
|
`[useApiFetch] 请求 ${url}, 携带token, token长度: ${
|
||||||
|
token.length
|
||||||
|
}, token前20字符: ${token.substring(0, 20)}...`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
`[useApiFetch] 请求 ${url}, 没有token, localStorage中token为: ${localStorage.getItem(
|
||||||
|
"token"
|
||||||
|
)}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return { url, options };
|
return { url, options };
|
||||||
},
|
},
|
||||||
@@ -44,30 +57,41 @@ const useApiFetch = createFetch({
|
|||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
// 清除本地存储的 token
|
// 清除本地存储的 token
|
||||||
localStorage.removeItem("token");
|
localStorage.removeItem("token");
|
||||||
localStorage.removeItem('refreshAfter')
|
localStorage.removeItem("refreshAfter");
|
||||||
localStorage.removeItem('accessExpire')
|
localStorage.removeItem("accessExpire");
|
||||||
// 跳转到登录页
|
// 跳转到登录页
|
||||||
router.replace("/login");
|
router.replace("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.code !== 200) {
|
if (data.code !== 200) {
|
||||||
if (data.code === 100009) {
|
if (data.code === 100009) {
|
||||||
|
// 用户未找到:清除所有数据并刷新页面
|
||||||
// 改进的存储管理
|
// 改进的存储管理
|
||||||
localStorage.removeItem('token')
|
localStorage.removeItem("token");
|
||||||
localStorage.removeItem('refreshAfter')
|
localStorage.removeItem("refreshAfter");
|
||||||
localStorage.removeItem('accessExpire')
|
localStorage.removeItem("accessExpire");
|
||||||
localStorage.removeItem('userInfo')
|
localStorage.removeItem("userInfo");
|
||||||
localStorage.removeItem('agentInfo')
|
localStorage.removeItem("agentInfo");
|
||||||
|
localStorage.removeItem("tokenVersion");
|
||||||
|
|
||||||
// 重置状态
|
// 重置状态
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const agentStore = useAgentStore();
|
const agentStore = useAgentStore();
|
||||||
userStore.resetUser()
|
userStore.resetUser();
|
||||||
agentStore.resetAgent()
|
agentStore.resetAgent();
|
||||||
location.reload()
|
location.reload();
|
||||||
|
|
||||||
}
|
}
|
||||||
if (data.code !== 200002 && data.code !== 200003 && data.code !== 200004 && data.code !== 100009) {
|
if (
|
||||||
|
data.code !== 200002 &&
|
||||||
|
data.code !== 200003 &&
|
||||||
|
data.code !== 200004 &&
|
||||||
|
data.code !== 100009 &&
|
||||||
|
data.code !== 100008
|
||||||
|
) {
|
||||||
|
showToast({ message: data.msg });
|
||||||
|
}
|
||||||
|
// 对于 100008(自定义错误),显示错误消息但不刷新页面
|
||||||
|
if (data.code === 100008) {
|
||||||
showToast({ message: data.msg });
|
showToast({ message: data.msg });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,11 +100,11 @@ const useApiFetch = createFetch({
|
|||||||
async onFetchError({ error, response }) {
|
async onFetchError({ error, response }) {
|
||||||
console.log("error", error);
|
console.log("error", error);
|
||||||
closeToast();
|
closeToast();
|
||||||
if (response.status === 401) {
|
if (response && response.status === 401) {
|
||||||
// 清除本地存储的 token
|
// 清除本地存储的 token
|
||||||
localStorage.removeItem("token");
|
localStorage.removeItem("token");
|
||||||
localStorage.removeItem('refreshAfter')
|
localStorage.removeItem("refreshAfter");
|
||||||
localStorage.removeItem('accessExpire')
|
localStorage.removeItem("accessExpire");
|
||||||
// 跳转到登录页
|
// 跳转到登录页
|
||||||
router.replace("/login");
|
router.replace("/login");
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,197 +1,213 @@
|
|||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from "vue";
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from "vue-router";
|
||||||
|
|
||||||
export function useSEO() {
|
export function useSEO() {
|
||||||
const route = useRoute()
|
const route = useRoute();
|
||||||
|
|
||||||
// 默认SEO信息
|
// 默认SEO信息
|
||||||
const defaultSEO = {
|
const defaultSEO = {
|
||||||
title: '一查查|大数据风险报告查询与代理平台,支持个人和企业多场景风控应用',
|
title: "一查查|大数据风险报告查询与代理平台,支持个人和企业多场景风控应用",
|
||||||
description: '一查查,专业大数据风险报告查询与代理平台,支持个人信用查询、小微企业风控、贷前风险背调等多场景报告应用,免费开通代理权限,助力高效识别信用与风险。',
|
description:
|
||||||
keywords: '大数据风险报告查询、大数据风险评估、大数据分析报告、个人大数据风险查询、小微企业风险、贷前风险背调、代理管理平台、免费开通代理、风险管控平台、信用风险分析、企业风险报告、贷前信用审核、失信人名单查询、被执行人信息、信用黑名单查询',
|
"一查查,专业大数据风险报告查询与代理平台,支持个人风险查询、小微企业风控、贷前风险背调等多场景报告应用,免费开通代理权限,助力高效识别信用与风险。",
|
||||||
url: 'https://www.zhinengcha.cn'
|
keywords:
|
||||||
}
|
"大数据风险报告查询、大数据风险评估、大数据分析报告、个人大数据风险查询、小微企业风险、贷前风险背调、代理管理平台、免费开通代理、风险管控平台、信用风险分析、企业风险报告、贷前信用审核、失信人名单查询、被执行人信息、信用黑名单查询",
|
||||||
|
url: "https://www.zhinengcha.cn",
|
||||||
|
};
|
||||||
|
|
||||||
// 页面SEO配置
|
// 页面SEO配置
|
||||||
const pageSEO = ref({
|
const pageSEO = ref({
|
||||||
title: '',
|
title: "",
|
||||||
description: '',
|
description: "",
|
||||||
keywords: '',
|
keywords: "",
|
||||||
url: ''
|
url: "",
|
||||||
})
|
});
|
||||||
|
|
||||||
// 更新页面SEO信息
|
// 更新页面SEO信息
|
||||||
const updateSEO = (seoConfig) => {
|
const updateSEO = (seoConfig) => {
|
||||||
const config = { ...defaultSEO, ...seoConfig }
|
const config = { ...defaultSEO, ...seoConfig };
|
||||||
|
|
||||||
// 更新页面标题
|
// 更新页面标题
|
||||||
document.title = config.title
|
document.title = config.title;
|
||||||
|
|
||||||
// 更新meta描述
|
// 更新meta描述
|
||||||
let metaDescription = document.querySelector('meta[name="description"]')
|
let metaDescription = document.querySelector(
|
||||||
|
'meta[name="description"]'
|
||||||
|
);
|
||||||
if (!metaDescription) {
|
if (!metaDescription) {
|
||||||
metaDescription = document.createElement('meta')
|
metaDescription = document.createElement("meta");
|
||||||
metaDescription.name = 'description'
|
metaDescription.name = "description";
|
||||||
document.head.appendChild(metaDescription)
|
document.head.appendChild(metaDescription);
|
||||||
}
|
}
|
||||||
metaDescription.content = config.description
|
metaDescription.content = config.description;
|
||||||
|
|
||||||
// 更新meta关键词
|
// 更新meta关键词
|
||||||
let metaKeywords = document.querySelector('meta[name="keywords"]')
|
let metaKeywords = document.querySelector('meta[name="keywords"]');
|
||||||
if (!metaKeywords) {
|
if (!metaKeywords) {
|
||||||
metaKeywords = document.createElement('meta')
|
metaKeywords = document.createElement("meta");
|
||||||
metaKeywords.name = 'keywords'
|
metaKeywords.name = "keywords";
|
||||||
document.head.appendChild(metaKeywords)
|
document.head.appendChild(metaKeywords);
|
||||||
}
|
}
|
||||||
metaKeywords.content = config.keywords
|
metaKeywords.content = config.keywords;
|
||||||
|
|
||||||
// 更新Open Graph标签
|
// 更新Open Graph标签
|
||||||
updateOpenGraph(config)
|
updateOpenGraph(config);
|
||||||
|
|
||||||
// 更新Twitter Cards
|
// 更新Twitter Cards
|
||||||
updateTwitterCards(config)
|
updateTwitterCards(config);
|
||||||
|
|
||||||
// 更新canonical URL
|
// 更新canonical URL
|
||||||
updateCanonicalURL(config.url)
|
updateCanonicalURL(config.url);
|
||||||
|
|
||||||
// 更新结构化数据
|
// 更新结构化数据
|
||||||
updateStructuredData(config)
|
updateStructuredData(config);
|
||||||
}
|
};
|
||||||
|
|
||||||
// 更新Open Graph标签
|
// 更新Open Graph标签
|
||||||
const updateOpenGraph = (config) => {
|
const updateOpenGraph = (config) => {
|
||||||
const ogTags = {
|
const ogTags = {
|
||||||
'og:title': config.title,
|
"og:title": config.title,
|
||||||
'og:description': config.description,
|
"og:description": config.description,
|
||||||
'og:url': config.url,
|
"og:url": config.url,
|
||||||
'og:type': 'website',
|
"og:type": "website",
|
||||||
'og:site_name': '一查查',
|
"og:site_name": "一查查",
|
||||||
'og:locale': 'zh_CN'
|
"og:locale": "zh_CN",
|
||||||
}
|
};
|
||||||
|
|
||||||
Object.entries(ogTags).forEach(([property, content]) => {
|
Object.entries(ogTags).forEach(([property, content]) => {
|
||||||
let meta = document.querySelector(`meta[property="${property}"]`)
|
let meta = document.querySelector(`meta[property="${property}"]`);
|
||||||
if (!meta) {
|
if (!meta) {
|
||||||
meta = document.createElement('meta')
|
meta = document.createElement("meta");
|
||||||
meta.setAttribute('property', property)
|
meta.setAttribute("property", property);
|
||||||
document.head.appendChild(meta)
|
document.head.appendChild(meta);
|
||||||
}
|
}
|
||||||
meta.content = content
|
meta.content = content;
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
// 更新Twitter Cards
|
// 更新Twitter Cards
|
||||||
const updateTwitterCards = (config) => {
|
const updateTwitterCards = (config) => {
|
||||||
const twitterTags = {
|
const twitterTags = {
|
||||||
'twitter:card': 'summary',
|
"twitter:card": "summary",
|
||||||
'twitter:title': config.title,
|
"twitter:title": config.title,
|
||||||
'twitter:description': config.description,
|
"twitter:description": config.description,
|
||||||
'twitter:url': config.url
|
"twitter:url": config.url,
|
||||||
}
|
};
|
||||||
|
|
||||||
Object.entries(twitterTags).forEach(([name, content]) => {
|
Object.entries(twitterTags).forEach(([name, content]) => {
|
||||||
let meta = document.querySelector(`meta[name="${name}"]`)
|
let meta = document.querySelector(`meta[name="${name}"]`);
|
||||||
if (!meta) {
|
if (!meta) {
|
||||||
meta = document.createElement('meta')
|
meta = document.createElement("meta");
|
||||||
meta.name = name
|
meta.name = name;
|
||||||
document.head.appendChild(meta)
|
document.head.appendChild(meta);
|
||||||
}
|
}
|
||||||
meta.content = content
|
meta.content = content;
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
// 更新canonical URL
|
// 更新canonical URL
|
||||||
const updateCanonicalURL = (url) => {
|
const updateCanonicalURL = (url) => {
|
||||||
let canonical = document.querySelector('link[rel="canonical"]')
|
let canonical = document.querySelector('link[rel="canonical"]');
|
||||||
if (!canonical) {
|
if (!canonical) {
|
||||||
canonical = document.createElement('link')
|
canonical = document.createElement("link");
|
||||||
canonical.rel = 'canonical'
|
canonical.rel = "canonical";
|
||||||
document.head.appendChild(canonical)
|
document.head.appendChild(canonical);
|
||||||
}
|
}
|
||||||
canonical.href = url
|
canonical.href = url;
|
||||||
}
|
};
|
||||||
|
|
||||||
// 更新结构化数据
|
// 更新结构化数据
|
||||||
const updateStructuredData = (config) => {
|
const updateStructuredData = (config) => {
|
||||||
// 移除现有的结构化数据
|
// 移除现有的结构化数据
|
||||||
const existingScripts = document.querySelectorAll('script[type="application/ld+json"]')
|
const existingScripts = document.querySelectorAll(
|
||||||
existingScripts.forEach(script => {
|
'script[type="application/ld+json"]'
|
||||||
|
);
|
||||||
|
existingScripts.forEach((script) => {
|
||||||
if (script.textContent.includes('"@type":"WebPage"')) {
|
if (script.textContent.includes('"@type":"WebPage"')) {
|
||||||
script.remove()
|
script.remove();
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
// 添加新的结构化数据
|
// 添加新的结构化数据
|
||||||
const structuredData = {
|
const structuredData = {
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "WebPage",
|
"@type": "WebPage",
|
||||||
"name": config.title,
|
name: config.title,
|
||||||
"description": config.description,
|
description: config.description,
|
||||||
"url": config.url,
|
url: config.url,
|
||||||
"mainEntity": {
|
mainEntity: {
|
||||||
"@type": "Organization",
|
"@type": "Organization",
|
||||||
"name": "一查查",
|
name: "一查查",
|
||||||
"url": "https://www.zhinengcha.cn/",
|
url: "https://www.zhinengcha.cn/",
|
||||||
"description": "专业大数据风险报告查询与代理平台,支持个人和企业多场景风控应用"
|
description:
|
||||||
}
|
"专业大数据风险报告查询与代理平台,支持个人和企业多场景风控应用",
|
||||||
}
|
},
|
||||||
|
};
|
||||||
const script = document.createElement('script')
|
|
||||||
script.type = 'application/ld+json'
|
const script = document.createElement("script");
|
||||||
script.textContent = JSON.stringify(structuredData)
|
script.type = "application/ld+json";
|
||||||
document.head.appendChild(script)
|
script.textContent = JSON.stringify(structuredData);
|
||||||
}
|
document.head.appendChild(script);
|
||||||
|
};
|
||||||
|
|
||||||
// 根据路由自动更新SEO
|
// 根据路由自动更新SEO
|
||||||
const updateSEOByRoute = () => {
|
const updateSEOByRoute = () => {
|
||||||
const routeConfigs = {
|
const routeConfigs = {
|
||||||
'/': {
|
"/": {
|
||||||
title: '一查查|大数据风险报告查询与代理平台,支持个人和企业多场景风控应用',
|
title: "一查查|大数据风险报告查询与代理平台,支持个人和企业多场景风控应用",
|
||||||
description: '一查查,专业大数据风险报告查询与代理平台,支持个人信用查询、小微企业风控、贷前风险背调等多场景报告应用,免费开通代理权限,助力高效识别信用与风险。',
|
description:
|
||||||
keywords: '大数据风险报告查询、大数据风险评估、大数据分析报告、个人大数据风险查询、小微企业风险、贷前风险背调、代理管理平台、免费开通代理、风险管控平台、信用风险分析、企业风险报告、贷前信用审核、失信人名单查询、被执行人信息、信用黑名单查询'
|
"一查查,专业大数据风险报告查询与代理平台,支持个人风险查询、小微企业风控、贷前风险背调等多场景报告应用,免费开通代理权限,助力高效识别信用与风险。",
|
||||||
|
keywords:
|
||||||
|
"大数据风险报告查询、大数据风险评估、大数据分析报告、个人大数据风险查询、小微企业风险、贷前风险背调、代理管理平台、免费开通代理、风险管控平台、信用风险分析、企业风险报告、贷前信用审核、失信人名单查询、被执行人信息、信用黑名单查询",
|
||||||
},
|
},
|
||||||
'/agent': {
|
"/agent": {
|
||||||
title: '一查查代理 - 免费开通代理权限 | 大数据风险报告代理',
|
title: "一查查代理 - 免费开通代理权限 | 大数据风险报告代理",
|
||||||
description: '一查查代理平台,免费开通代理权限,享受大数据风险报告查询服务代理收益。专业的大数据风险报告、婚姻查询、个人信用评估等服务的代理合作。',
|
description:
|
||||||
keywords: '一查查代理, 免费代理, 大数据风险报告代理, 代理权限, 代理收益'
|
"一查查代理平台,免费开通代理权限,享受大数据风险报告查询服务代理收益。专业的大数据风险报告、婚姻查询、个人风险评估等服务的代理合作。",
|
||||||
|
keywords:
|
||||||
|
"一查查代理, 免费代理, 大数据风险报告代理, 代理权限, 代理收益",
|
||||||
},
|
},
|
||||||
'/help': {
|
"/help": {
|
||||||
title: '帮助中心 - 一查查使用指南 | 常见问题解答',
|
title: "帮助中心 - 一查查使用指南 | 常见问题解答",
|
||||||
description: '一查查帮助中心,提供详细的使用指南、常见问题解答、操作教程等,帮助用户更好地使用大数据风险报告查询服务。',
|
description:
|
||||||
keywords: '一查查帮助, 使用指南, 常见问题, 操作教程, 客服支持'
|
"一查查帮助中心,提供详细的使用指南、常见问题解答、操作教程等,帮助用户更好地使用大数据风险报告查询服务。",
|
||||||
|
keywords: "一查查帮助, 使用指南, 常见问题, 操作教程, 客服支持",
|
||||||
},
|
},
|
||||||
'/help/guide': {
|
"/help/guide": {
|
||||||
title: '使用指南 - 一查查操作教程 | 功能说明',
|
title: "使用指南 - 一查查操作教程 | 功能说明",
|
||||||
description: '一查查详细使用指南,包含各功能模块的操作教程、功能说明、注意事项等,让用户快速上手使用。',
|
description:
|
||||||
keywords: '使用指南, 操作教程, 功能说明, 快速上手, 一查查教程'
|
"一查查详细使用指南,包含各功能模块的操作教程、功能说明、注意事项等,让用户快速上手使用。",
|
||||||
|
keywords: "使用指南, 操作教程, 功能说明, 快速上手, 一查查教程",
|
||||||
},
|
},
|
||||||
'/example': {
|
"/example": {
|
||||||
title: '示例报告 - 一查查报告展示 | 大数据风险报告样例',
|
title: "示例报告 - 一查查报告展示 | 大数据风险报告样例",
|
||||||
description: '一查查示例报告展示,包含大数据风险报告、婚姻状况查询、个人信用评估等服务的报告样例,让用户了解报告内容和格式。',
|
description:
|
||||||
keywords: '示例报告, 报告展示, 报告样例, 大数据风险报告, 婚姻查询报告'
|
"一查查示例报告展示,包含大数据风险报告、婚姻状况查询、个人风险评估等服务的报告样例,让用户了解报告内容和格式。",
|
||||||
|
keywords:
|
||||||
|
"示例报告, 报告展示, 报告样例, 大数据风险报告, 婚姻查询报告",
|
||||||
},
|
},
|
||||||
'/service': {
|
"/service": {
|
||||||
title: '客服中心 - 一查查在线客服 | 技术支持',
|
title: "客服中心 - 一查查在线客服 | 技术支持",
|
||||||
description: '一查查客服中心,提供在线客服支持、技术咨询、问题反馈等服务,确保用户获得及时有效的帮助。',
|
description:
|
||||||
keywords: '客服中心, 在线客服, 技术支持, 问题反馈, 一查查客服'
|
"一查查客服中心,提供在线客服支持、技术咨询、问题反馈等服务,确保用户获得及时有效的帮助。",
|
||||||
}
|
keywords: "客服中心, 在线客服, 技术支持, 问题反馈, 一查查客服",
|
||||||
}
|
},
|
||||||
|
};
|
||||||
const currentPath = route?.path || '/'
|
|
||||||
const config = routeConfigs[currentPath] || defaultSEO
|
const currentPath = route?.path || "/";
|
||||||
|
const config = routeConfigs[currentPath] || defaultSEO;
|
||||||
|
|
||||||
updateSEO({
|
updateSEO({
|
||||||
...config,
|
...config,
|
||||||
url: `https://www.zhinengcha.cn${currentPath}`
|
url: `https://www.zhinengcha.cn${currentPath}`,
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
// 监听路由变化
|
// 监听路由变化
|
||||||
watch(() => route?.path, updateSEOByRoute, { immediate: true })
|
watch(() => route?.path, updateSEOByRoute, { immediate: true });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
updateSEO,
|
updateSEO,
|
||||||
updateSEOByRoute,
|
updateSEOByRoute,
|
||||||
pageSEO
|
pageSEO,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
107
src/composables/useShareReport.js
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { ref } from "vue";
|
||||||
|
import { showToast, showDialog } from "vant";
|
||||||
|
import useApiFetch from "@/composables/useApiFetch";
|
||||||
|
|
||||||
|
export function useShareReport() {
|
||||||
|
const isLoading = ref(false);
|
||||||
|
|
||||||
|
const copyToClipboard = async (text) => {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
showToast({
|
||||||
|
type: "success",
|
||||||
|
message: "链接已复制到剪贴板",
|
||||||
|
position: "bottom",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleShare = async (orderId, orderNo, isExample = false, disabled = false) => {
|
||||||
|
if (isLoading.value || disabled) return;
|
||||||
|
|
||||||
|
// 如果是示例模式,直接分享当前URL
|
||||||
|
if (isExample) {
|
||||||
|
try {
|
||||||
|
const currentUrl = window.location.href;
|
||||||
|
await copyToClipboard(currentUrl);
|
||||||
|
showToast({
|
||||||
|
type: "success",
|
||||||
|
message: "示例链接已复制到剪贴板",
|
||||||
|
position: "bottom",
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
showToast({
|
||||||
|
type: "fail",
|
||||||
|
message: "复制链接失败",
|
||||||
|
position: "bottom",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优先使用 orderId,如果没有则使用 orderNo
|
||||||
|
const orderIdentifier = orderId || orderNo;
|
||||||
|
if (!orderIdentifier) {
|
||||||
|
showToast({
|
||||||
|
type: "fail",
|
||||||
|
message: "缺少订单标识",
|
||||||
|
position: "bottom",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
// 根据实际使用的标识构建请求参数
|
||||||
|
const requestData = orderId
|
||||||
|
? { order_id: orderId }
|
||||||
|
: { order_no: orderNo };
|
||||||
|
|
||||||
|
const { data, error } = await useApiFetch("/query/generate_share_link")
|
||||||
|
.post(requestData)
|
||||||
|
.json();
|
||||||
|
|
||||||
|
if (error.value) {
|
||||||
|
throw new Error(error.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.value?.code === 200 && data.value.data?.share_link) {
|
||||||
|
const baseUrl = window.location.origin;
|
||||||
|
const linkId = encodeURIComponent(data.value.data.share_link);
|
||||||
|
const fullShareUrl = `${baseUrl}/report/share/${linkId}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 显示确认对话框
|
||||||
|
await showDialog({
|
||||||
|
title: "分享链接已生成",
|
||||||
|
message: "链接将在7天后过期,是否复制到剪贴板?",
|
||||||
|
confirmButtonText: "复制链接",
|
||||||
|
cancelButtonText: "取消",
|
||||||
|
showCancelButton: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 用户点击确认后复制链接
|
||||||
|
await copyToClipboard(fullShareUrl);
|
||||||
|
} catch (dialogErr) {
|
||||||
|
// 用户点击取消按钮时,dialogErr 会是 'cancel'
|
||||||
|
// 这里不需要显示错误提示,直接返回即可
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(data.value?.message || "生成分享链接失败");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showToast({
|
||||||
|
type: "fail",
|
||||||
|
message: err.message || "生成分享链接失败",
|
||||||
|
position: "bottom",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
isLoading,
|
||||||
|
handleShare,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
30
src/constants/riskAssessmentReportFeatures.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/** 个人大数据 / 风险评估产品英文标识 */
|
||||||
|
export const RISK_ASSESSMENT_PRODUCT_EN = 'riskassessment';
|
||||||
|
|
||||||
|
/** 查询页、推广查询页固定展示的「报告包含内容」 */
|
||||||
|
export const riskAssessmentReportModules = [
|
||||||
|
{ name: '综合评分', icon: 'huankuanyali.svg' },
|
||||||
|
{ name: '数据分析', icon: 'fengxianxingwei.svg' },
|
||||||
|
{ name: '申请次数', icon: 'jiedaishenqing.svg' },
|
||||||
|
{ name: '申请机构', icon: 'renqiguanxi.svg' },
|
||||||
|
{ name: '行为分析', icon: 'jiedaixingwei.svg' },
|
||||||
|
{ name: '风险汇总', icon: 'yuepeichang.svg' },
|
||||||
|
{ name: '违约逾期', icon: 'jiedaiweiyue.svg' },
|
||||||
|
{ name: '特殊名单', icon: 'beijianguanrenyuan.svg' },
|
||||||
|
{ name: '个人涉诉', icon: 'sifasheyu.svg' },
|
||||||
|
{ name: '失信公告', icon: 'shixinren.svg' },
|
||||||
|
{ name: '限高公告', icon: 'xianzhigaoxiaofei.svg' },
|
||||||
|
{ name: '裁判公告', icon: 'minshianjianguanli.svg' },
|
||||||
|
{ name: '开庭公告', icon: 'xingshi.svg' },
|
||||||
|
{ name: '执行公告', icon: 'zhixinganjian.svg' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function isRiskAssessmentProduct(productEn) {
|
||||||
|
return productEn === RISK_ASSESSMENT_PRODUCT_EN;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 代理查询用 feature 路由参数;推广查询用 link 接口返回的 product_en */
|
||||||
|
export function resolveInquireProductEn(feature, featureData) {
|
||||||
|
const fromData = featureData && typeof featureData === 'object' ? featureData.product_en : '';
|
||||||
|
return feature || fromData || '';
|
||||||
|
}
|
||||||
@@ -20,17 +20,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="disclaimer">
|
<div class="disclaimer">
|
||||||
<div class="flex flex-col items-center">
|
<div class="flex flex-col items-center">
|
||||||
<div class="flex items-center">
|
<!-- <div class="flex items-center">
|
||||||
<img class="w-4 h-4 mr-2" src="@/assets/images/public_security_record_icon.png" alt="公安备案" />
|
<img class="w-4 h-4 mr-2" src="@/assets/images/public_security_record_icon.png" alt="公安备案" />
|
||||||
<text>琼公网安备46010002000584号</text>
|
<text>琼公网安备46010002000584号</text>
|
||||||
</div>
|
</div> -->
|
||||||
<div>
|
<div>
|
||||||
<a class="text-blue-500" href="https://beian.miit.gov.cn">
|
<a class="text-blue-500" href="https://beian.miit.gov.cn">
|
||||||
琼ICP备2024048057号-2
|
琼ICP备2024038584号-10
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>海南省学宇思网络科技有限公司版权所有</div>
|
<div>海南海宇大数据有限公司版权所有</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -43,8 +43,8 @@ const router = useRouter();
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const tabbar = ref("promote");
|
const tabbar = ref("promote");
|
||||||
const menu = reactive([
|
const menu = reactive([
|
||||||
{ title: "推广", icon: "cluster-o", name: "promote" },
|
{ title: "首页", icon: "home-o", name: "promote" },
|
||||||
{ title: "资产", icon: "gold-coin-o", name: "agent" },
|
{ title: "数据", icon: "bar-chart-o", name: "agent" },
|
||||||
{ title: "我的", icon: "user-o", name: "me" }
|
{ title: "我的", icon: "user-o", name: "me" }
|
||||||
]);
|
]);
|
||||||
// 根据当前路由设置 Tabbar 的高亮项
|
// 根据当前路由设置 Tabbar 的高亮项
|
||||||
@@ -83,7 +83,7 @@ const tabChange = (name, a, b, c) => {
|
|||||||
// 跳转到投诉页面
|
// 跳转到投诉页面
|
||||||
const toComplaint = () => {
|
const toComplaint = () => {
|
||||||
window.location.href =
|
window.location.href =
|
||||||
"https://work.weixin.qq.com/kfid/kfc8a32720024833f57"; // 跳转到客服页面
|
"https://work.weixin.qq.com/kfid/kfc82d4424e4b19e5f3"; // 跳转到客服页面
|
||||||
// router.push({ name: 'complaint' }); // 使用 Vue Router 进行跳转
|
// router.push({ name: 'complaint' }); // 使用 Vue Router 进行跳转
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from "vue-router";
|
||||||
import NProgress from 'nprogress'
|
import NProgress from "nprogress";
|
||||||
import GlobalLayout from '@/layouts/GlobalLayout.vue'
|
import GlobalLayout from "@/layouts/GlobalLayout.vue";
|
||||||
import HomeLayout from '@/layouts/HomeLayout.vue'
|
import HomeLayout from "@/layouts/HomeLayout.vue";
|
||||||
import PageLayout from '@/layouts/PageLayout.vue'
|
import PageLayout from "@/layouts/PageLayout.vue";
|
||||||
import index from '@/views/index.vue'
|
import index from "@/views/index.vue";
|
||||||
import Promote from '@/views/Promote.vue'
|
import Promote from "@/views/Promote.vue";
|
||||||
import PromotePage from '@/views/PromotePage.vue'
|
import PromotePage from "@/views/PromotePage.vue";
|
||||||
import { useAgentStore } from '@/stores/agentStore'
|
import { useAgentStore } from "@/stores/agentStore";
|
||||||
import { useUserStore } from '@/stores/userStore'
|
import { useUserStore } from "@/stores/userStore";
|
||||||
import { useDialogStore } from '@/stores/dialogStore'
|
import { useDialogStore } from "@/stores/dialogStore";
|
||||||
import { useEnv } from '@/composables/useEnv'
|
import { useEnv } from "@/composables/useEnv";
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from "pinia";
|
||||||
import { useSEO } from '@/composables/useSEO'
|
import { useSEO } from "@/composables/useSEO";
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
@@ -27,262 +27,309 @@ const router = createRouter({
|
|||||||
},
|
},
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: "/",
|
||||||
component: GlobalLayout, // 使用 Layout 作为父组件
|
component: GlobalLayout, // 使用 Layout 作为父组件
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: "",
|
||||||
component: HomeLayout, // 使用 Layout 作为父组件
|
component: HomeLayout, // 使用 Layout 作为父组件
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: "",
|
||||||
name: 'promote',
|
name: "promote",
|
||||||
component: PromotePage,
|
component: PromotePage,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'index',
|
path: "index",
|
||||||
name: 'index',
|
name: "index",
|
||||||
component: index,
|
component: index,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/agent',
|
path: "/agent",
|
||||||
name: 'agent',
|
name: "agent",
|
||||||
component: () => import('@/views/Agent.vue'),
|
component: () => import("@/views/Agent.vue"),
|
||||||
|
meta: { title: "代理主页", requiresAuth: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'me',
|
path: "me",
|
||||||
name: 'me',
|
name: "me",
|
||||||
component: () => import('@/views/Me.vue'),
|
component: () => import("@/views/Me.vue"),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '',
|
path: "",
|
||||||
component: PageLayout,
|
component: PageLayout,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '/historyQuery',
|
path: "/historyQuery",
|
||||||
name: 'history',
|
name: "history",
|
||||||
component: () => import('@/views/HistoryQuery.vue'),
|
component: () => import("@/views/HistoryQuery.vue"),
|
||||||
meta: { title: '历史报告', requiresAuth: true },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/help',
|
|
||||||
name: 'help',
|
|
||||||
component: () => import('@/views/Help.vue'),
|
|
||||||
meta: { title: '帮助中心' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/help/detail',
|
|
||||||
name: 'helpDetail',
|
|
||||||
component: () => import('@/views/HelpDetail.vue'),
|
|
||||||
meta: { title: '帮助中心' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/help/guide',
|
|
||||||
name: 'helpGuide',
|
|
||||||
component: () => import('@/views/HelpGuide.vue'),
|
|
||||||
meta: { title: '引导指南' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/withdraw',
|
|
||||||
name: 'withdraw',
|
|
||||||
component: () => import('@/views/Withdraw.vue'),
|
|
||||||
meta: { title: '提现', requiresAuth: true },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/service',
|
|
||||||
name: 'service',
|
|
||||||
component: () => import('@/views/Service.vue'),
|
|
||||||
meta: { title: '客服' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/complaint',
|
|
||||||
name: 'complaint',
|
|
||||||
component: () => import('@/views/Complaint.vue'),
|
|
||||||
meta: { title: '投诉' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/report',
|
|
||||||
name: 'report',
|
|
||||||
component: () => import('@/views/Report.vue'),
|
|
||||||
meta: {
|
meta: {
|
||||||
title: '报告结果', requiresAuth: true, notNeedBindPhone: true
|
title: "历史报告",
|
||||||
|
requiresAuth: true,
|
||||||
|
notNeedBindPhone: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/example',
|
path: "/help",
|
||||||
name: 'example',
|
name: "help",
|
||||||
component: () => import('@/views/Example.vue'),
|
component: () => import("@/views/Help.vue"),
|
||||||
meta: { title: '示例报告', notNeedBindPhone: true },
|
meta: { title: "帮助中心" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/vant-theme-test',
|
path: "/help/detail",
|
||||||
name: 'vantThemeTest',
|
name: "helpDetail",
|
||||||
component: () => import('@/views/VantThemeTest.vue'),
|
component: () => import("@/views/HelpDetail.vue"),
|
||||||
meta: { title: 'Vant主题色测试' },
|
meta: { title: "帮助中心" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/privacyPolicy',
|
path: "/help/guide",
|
||||||
name: 'privacyPolicy',
|
name: "helpGuide",
|
||||||
component: () => import('@/views/PrivacyPolicy.vue'),
|
component: () => import("@/views/HelpGuide.vue"),
|
||||||
meta: { title: '隐私政策' },
|
meta: { title: "引导指南" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/userAgreement',
|
path: "/agent/system-guide",
|
||||||
name: 'userAgreement',
|
name: "agentSystemGuide",
|
||||||
component: () => import('@/views/UserAgreement.vue'),
|
component: () =>
|
||||||
meta: { title: '用户协议' },
|
import("@/views/AgentSystemGuide.vue"),
|
||||||
|
meta: { title: "代理系统指南" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/agentManageAgreement',
|
path: "/withdraw",
|
||||||
name: 'agentManageAgreement',
|
name: "withdraw",
|
||||||
component: () => import('@/views/AgentManageAgreement.vue'),
|
component: () => import("@/views/Withdraw.vue"),
|
||||||
meta: { title: '代理管理协议' },
|
meta: { title: "提现", requiresAuth: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/agentSerivceAgreement',
|
path: "/service",
|
||||||
name: 'agentSerivceAgreement',
|
name: "service",
|
||||||
component: () => import('@/views/AgentServiceAgreement.vue'),
|
component: () => import("@/views/Service.vue"),
|
||||||
meta: { title: '信息技术服务合同' },
|
meta: { title: "客服" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/inquire/:feature',
|
path: "/complaint",
|
||||||
name: 'inquire',
|
name: "complaint",
|
||||||
component: () => import('@/views/Inquire.vue'),
|
component: () => import("@/views/Complaint.vue"),
|
||||||
meta: { title: '查询报告' },
|
meta: { title: "投诉" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/authorization',
|
path: "/report",
|
||||||
name: 'authorization',
|
name: "report",
|
||||||
component: () => import('@/views/Authorization.vue'),
|
component: () => import("@/views/Report.vue"),
|
||||||
meta: { title: '授权书' },
|
meta: {
|
||||||
|
title: "报告结果",
|
||||||
|
requiresAuth: true,
|
||||||
|
notNeedBindPhone: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/payment/result',
|
path: "/example",
|
||||||
name: 'paymentResult',
|
name: "example",
|
||||||
component: () => import('@/views/PaymentResult.vue'),
|
component: () => import("@/views/Example.vue"),
|
||||||
meta: { title: '支付结果', requiresAuth: true },
|
meta: { title: "示例报告", notNeedBindPhone: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/vant-theme-test",
|
||||||
|
name: "vantThemeTest",
|
||||||
|
component: () =>
|
||||||
|
import("@/views/VantThemeTest.vue"),
|
||||||
|
meta: { title: "Vant主题色测试" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/privacyPolicy",
|
||||||
|
name: "privacyPolicy",
|
||||||
|
component: () =>
|
||||||
|
import("@/views/PrivacyPolicy.vue"),
|
||||||
|
meta: { title: "隐私政策" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/userAgreement",
|
||||||
|
name: "userAgreement",
|
||||||
|
component: () =>
|
||||||
|
import("@/views/UserAgreement.vue"),
|
||||||
|
meta: { title: "用户协议" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/agentManageAgreement",
|
||||||
|
name: "agentManageAgreement",
|
||||||
|
component: () =>
|
||||||
|
import("@/views/AgentManageAgreement.vue"),
|
||||||
|
meta: { title: "代理管理协议" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/agentSerivceAgreement",
|
||||||
|
name: "agentSerivceAgreement",
|
||||||
|
component: () =>
|
||||||
|
import("@/views/AgentServiceAgreement.vue"),
|
||||||
|
meta: { title: "信息技术服务合同" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/inquire/:feature",
|
||||||
|
name: "inquire",
|
||||||
|
component: () => import("@/views/Inquire.vue"),
|
||||||
|
meta: { title: "查询报告" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/authorization",
|
||||||
|
name: "authorization",
|
||||||
|
component: () =>
|
||||||
|
import("@/views/Authorization.vue"),
|
||||||
|
meta: { title: "授权书" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/payment/result",
|
||||||
|
name: "paymentResult",
|
||||||
|
component: () =>
|
||||||
|
import("@/views/PaymentResult.vue"),
|
||||||
|
meta: {
|
||||||
|
title: "支付结果",
|
||||||
|
requiresAuth: true,
|
||||||
|
notNeedBindPhone: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'agent',
|
path: "agent",
|
||||||
component: PageLayout,
|
component: PageLayout,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '/agent/promote',
|
path: "/agent/promote",
|
||||||
name: 'agentPromote',
|
name: "agentPromote",
|
||||||
component: Promote,
|
component: Promote,
|
||||||
meta: {
|
meta: {
|
||||||
title: '推广报告',
|
title: "推广报告",
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
requiresAgent: true,
|
requiresAgent: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'promoteDetails',
|
path: "/agent/promote/poster",
|
||||||
name: 'promoteDetails',
|
name: "agentPromotePoster",
|
||||||
component: () => import('@/views/AgentPromoteDetails.vue'),
|
component: () => import("@/views/PromotePoster.vue"),
|
||||||
meta: {
|
meta: {
|
||||||
title: '直推报告收益明细',
|
title: "推广海报",
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
requiresAgent: true,
|
requiresAgent: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'rewardsDetails',
|
path: "/agent/promotion/query/list",
|
||||||
name: 'rewardsDetails',
|
name: "agentPromotionQueryList",
|
||||||
component: () => import('@/views/AgentRewardsDetails.vue'),
|
component: () =>
|
||||||
|
import("@/views/AgentPromotionHistory.vue"),
|
||||||
meta: {
|
meta: {
|
||||||
title: '代理奖励收益明细',
|
title: "推广查询记录",
|
||||||
|
requiresAuth: true,
|
||||||
|
requiresAgent: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "promoteDetails",
|
||||||
|
name: "promoteDetails",
|
||||||
|
component: () =>
|
||||||
|
import("@/views/AgentPromoteDetails.vue"),
|
||||||
|
meta: {
|
||||||
|
title: "我的推广收益",
|
||||||
|
requiresAuth: true,
|
||||||
|
requiresAgent: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "rewardsDetails",
|
||||||
|
name: "rewardsDetails",
|
||||||
|
component: () =>
|
||||||
|
import("@/views/AgentRewardsDetails.vue"),
|
||||||
|
meta: {
|
||||||
|
title: "下级推广收益",
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
requiresAgent: true,
|
requiresAgent: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: 'invitation',
|
path: "invitation",
|
||||||
name: 'invitation',
|
name: "invitation",
|
||||||
component: () => import('@/views/Invitation.vue'),
|
component: () =>
|
||||||
|
import("@/views/InvitationPage.vue"),
|
||||||
meta: {
|
meta: {
|
||||||
title: '邀请下级',
|
title: "邀请下级",
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
requiresAgent: true,
|
requiresAgent: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'agentVip',
|
path: "withdraw",
|
||||||
name: 'agentVip',
|
name: "withdraw",
|
||||||
component: () => import('@/views/AgentVip.vue'),
|
component: () => import("@/views/Withdraw.vue"),
|
||||||
meta: {
|
meta: {
|
||||||
title: '代理会员',
|
title: "提现",
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
requiresAgent: true,
|
requiresAgent: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'vipApply',
|
path: "withdrawDetails",
|
||||||
name: 'agentVipApply',
|
name: "withdrawDetails",
|
||||||
component: () => import('@/views/AgentVipApply.vue'),
|
component: () =>
|
||||||
|
import("@/views/WithdrawDetails.vue"),
|
||||||
meta: {
|
meta: {
|
||||||
title: 'VIP代理申请',
|
title: "提现记录",
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
requiresAgent: true,
|
requiresAgent: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'vipConfig',
|
path: "teamList",
|
||||||
name: 'agentVipConfig',
|
name: "teamList",
|
||||||
component: () => import('@/views/AgentVipConfig.vue'),
|
component: () => import("@/views/TeamList.vue"),
|
||||||
meta: {
|
meta: {
|
||||||
title: '代理会员报告配置',
|
title: "我的团队",
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
requiresAgent: true,
|
requiresAgent: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'withdraw',
|
path: "upgradeSubordinate",
|
||||||
name: 'withdraw',
|
name: "upgradeSubordinate",
|
||||||
component: () => import('@/views/Withdraw.vue'),
|
component: () =>
|
||||||
|
import("@/views/UpgradeSubordinate.vue"),
|
||||||
meta: {
|
meta: {
|
||||||
title: '提现',
|
title: "调整下级级别",
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
requiresAgent: true,
|
requiresAgent: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'withdrawDetails',
|
path: "upgrade",
|
||||||
name: 'withdrawDetails',
|
name: "agentUpgrade",
|
||||||
component: () => import('@/views/WithdrawDetails.vue'),
|
component: () => import("@/views/AgentUpgrade.vue"),
|
||||||
meta: {
|
meta: {
|
||||||
title: '提现记录',
|
title: "升级代理",
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
requiresAgent: true,
|
requiresAgent: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'invitationAgentApply/self',
|
path: "subordinateList",
|
||||||
name: 'invitationAgentApplySelf',
|
name: "subordinateList",
|
||||||
component: () => import('@/views/InvitationAgentApply.vue'),
|
component: () =>
|
||||||
meta: { title: '代理申请', requiresAuth: true },
|
import("@/views/SubordinateList.vue"),
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'subordinateList',
|
|
||||||
name: 'subordinateList',
|
|
||||||
component: () => import('@/views/SubordinateList.vue'),
|
|
||||||
meta: {
|
meta: {
|
||||||
title: '我的下级',
|
title: "我的下级",
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
requiresAgent: true,
|
requiresAgent: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'subordinateDetail/:id',
|
path: "subordinateDetail/:id",
|
||||||
name: 'subordinateDetail',
|
name: "subordinateDetail",
|
||||||
component: () => import('@/views/SubordinateDetail.vue'),
|
component: () =>
|
||||||
|
import("@/views/SubordinateDetail.vue"),
|
||||||
meta: {
|
meta: {
|
||||||
title: '下级贡献详情',
|
title: "下级贡献详情",
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
requiresAgent: true,
|
requiresAgent: true,
|
||||||
},
|
},
|
||||||
@@ -290,37 +337,42 @@ const router = createRouter({
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'app',
|
path: "app",
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'authorization',
|
path: "authorization",
|
||||||
name: 'appAuthorization',
|
name: "appAuthorization",
|
||||||
component: () => import('@/views/Authorization.vue'),
|
component: () =>
|
||||||
meta: { title: '授权书' },
|
import("@/views/Authorization.vue"),
|
||||||
|
meta: { title: "授权书" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'privacyPolicy',
|
path: "privacyPolicy",
|
||||||
name: 'appPrivacyPolicy',
|
name: "appPrivacyPolicy",
|
||||||
component: () => import('@/views/PrivacyPolicy.vue'),
|
component: () =>
|
||||||
meta: { title: '隐私政策' },
|
import("@/views/PrivacyPolicy.vue"),
|
||||||
|
meta: { title: "隐私政策" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'userAgreement',
|
path: "userAgreement",
|
||||||
name: 'appUserAgreement',
|
name: "appUserAgreement",
|
||||||
component: () => import('@/views/UserAgreement.vue'),
|
component: () =>
|
||||||
meta: { title: '用户协议' },
|
import("@/views/UserAgreement.vue"),
|
||||||
|
meta: { title: "用户协议" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'agentManageAgreement',
|
path: "agentManageAgreement",
|
||||||
name: 'appAgentManageAgreement',
|
name: "appAgentManageAgreement",
|
||||||
component: () => import('@/views/AgentManageAgreement.vue'),
|
component: () =>
|
||||||
meta: { title: '代理管理协议' },
|
import("@/views/AgentManageAgreement.vue"),
|
||||||
|
meta: { title: "代理管理协议" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'agentSerivceAgreement',
|
path: "agentSerivceAgreement",
|
||||||
name: 'appAgentSerivceAgreement',
|
name: "appAgentSerivceAgreement",
|
||||||
component: () => import('@/views/AgentServiceAgreement.vue'),
|
component: () =>
|
||||||
meta: { title: '信息技术服务合同' },
|
import("@/views/AgentServiceAgreement.vue"),
|
||||||
|
meta: { title: "信息技术服务合同" },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -328,45 +380,50 @@ const router = createRouter({
|
|||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: '/login',
|
path: "/login",
|
||||||
name: 'login',
|
name: "login",
|
||||||
component: () => import('@/views/Login.vue'),
|
component: () => import("@/views/Login.vue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/agent/promotionInquire/:linkIdentifier',
|
path: "/register",
|
||||||
name: 'promotionInquire',
|
name: "register",
|
||||||
component: () => import('@/views/PromotionInquire.vue'),
|
component: () => import("@/views/Register.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/agent/promotionInquire/:linkIdentifier",
|
||||||
|
name: "promotionInquire",
|
||||||
|
component: () => import("@/views/PromotionInquire.vue"),
|
||||||
meta: { notNeedBindPhone: true },
|
meta: { notNeedBindPhone: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/agent/invitationAgentApply/:linkIdentifier',
|
path: "/agent/invitationAgentApply/:linkIdentifier",
|
||||||
name: 'invitationAgentApply',
|
name: "invitationAgentApply",
|
||||||
component: () => import('@/views/InvitationAgentApply.vue'),
|
component: () => import("@/views/InvitationAgentApply.vue"),
|
||||||
meta: { title: '代理申请' },
|
meta: { title: "代理申请" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/report/share/:linkIdentifier',
|
path: "/report/share/:linkIdentifier",
|
||||||
name: 'reportShare',
|
name: "reportShare",
|
||||||
component: () => import('@/views/ReportShare.vue'),
|
component: () => import("@/views/ReportShare.vue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/:pathMatch(.*)*',
|
path: "/:pathMatch(.*)*",
|
||||||
name: 'NotFound',
|
name: "NotFound",
|
||||||
component: () => import('@/views/NotFound.vue'),
|
component: () => import("@/views/NotFound.vue"),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
});
|
||||||
NProgress.configure({
|
NProgress.configure({
|
||||||
easing: 'ease', // 动画方式
|
easing: "ease", // 动画方式
|
||||||
speed: 500, // 递增进度条的速度(毫秒)
|
speed: 500, // 递增进度条的速度(毫秒)
|
||||||
showSpinner: false, // 是否显示加载的圆圈
|
showSpinner: false, // 是否显示加载的圆圈
|
||||||
trickleSpeed: 200, // 自动递增间隔
|
trickleSpeed: 200, // 自动递增间隔
|
||||||
minimum: 0.3, // 初始化最小百分比
|
minimum: 0.3, // 初始化最小百分比
|
||||||
})
|
});
|
||||||
|
|
||||||
// 路由导航守卫
|
// 路由导航守卫
|
||||||
router.beforeEach(async (to, from, next) => {
|
router.beforeEach(async (to, from, next) => {
|
||||||
NProgress.start(); // 启动进度条
|
NProgress.start();
|
||||||
const isAuthenticated = localStorage.getItem("token");
|
const isAuthenticated = localStorage.getItem("token");
|
||||||
const agentStore = useAgentStore();
|
const agentStore = useAgentStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
@@ -375,93 +432,89 @@ router.beforeEach(async (to, from, next) => {
|
|||||||
const { isWeChat } = useEnv();
|
const { isWeChat } = useEnv();
|
||||||
const { isAgent, isLoaded } = storeToRefs(agentStore);
|
const { isAgent, isLoaded } = storeToRefs(agentStore);
|
||||||
const { mobile, isLoggedIn } = storeToRefs(userStore);
|
const { mobile, isLoggedIn } = storeToRefs(userStore);
|
||||||
const { isWeixinAuthing, weixinAuthComplete } = storeToRefs(authStore);
|
// 检查 token 是否过期
|
||||||
|
const accessExpire = localStorage.getItem("accessExpire");
|
||||||
// 微信环境下,如果正在进行授权,等待授权完成
|
const now = Date.now();
|
||||||
if (isWeChat.value && isWeixinAuthing.value && !weixinAuthComplete.value) {
|
let isTokenExpired = false;
|
||||||
// 等待授权完成,使用响应式监听
|
if (accessExpire) {
|
||||||
await new Promise((resolve) => {
|
isTokenExpired = now > parseInt(accessExpire) * 1000;
|
||||||
const stopWatcher = watch(
|
|
||||||
[isWeixinAuthing, weixinAuthComplete],
|
|
||||||
([authing, complete]) => {
|
|
||||||
if (!authing || complete) {
|
|
||||||
stopWatcher();
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
// ============================================================
|
||||||
// 处理需要登录的页面
|
// 场景 2: 需要登录的页面 + 无 token 或 token 过期 → 跳转登录
|
||||||
if (to.meta.requiresAuth && !isAuthenticated) {
|
// ============================================================
|
||||||
if (isWeChat.value) {
|
if (to.meta.requiresAuth && (!isAuthenticated || isTokenExpired)) {
|
||||||
// 微信环境下,如果授权失败或超时,重定向到首页
|
const loginQuery = {
|
||||||
if (!weixinAuthComplete.value) {
|
redirect: to.fullPath,
|
||||||
next("/");
|
};
|
||||||
location.reload();
|
if (from && from.name === "promotionInquire") {
|
||||||
} else {
|
loginQuery.from = "promotionInquire";
|
||||||
// 授权完成但仍无token,可能是授权失败
|
|
||||||
next("/");
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
next("/login");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
next({ path: "/login", query: loginQuery });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 已登录状态下的处理
|
// ============================================================
|
||||||
if (isAuthenticated) {
|
// 场景 3: 已登录状态下的处理
|
||||||
|
// ============================================================
|
||||||
|
if (isAuthenticated && !isTokenExpired) {
|
||||||
// 确保用户信息已加载
|
// 确保用户信息已加载
|
||||||
if (!isLoggedIn.value) {
|
if (!isLoggedIn.value) {
|
||||||
await userStore.fetchUserInfo();
|
try {
|
||||||
}
|
await userStore.fetchUserInfo();
|
||||||
|
} catch (err) {
|
||||||
// 检查手机号绑定状态
|
console.error("Error loading user info:", err);
|
||||||
// 只有在未绑定手机号,且目标路由需要登录并且没有设置notNeedBindPhone时,才弹出绑定手机号弹窗
|
|
||||||
if (
|
|
||||||
!mobile.value &&
|
|
||||||
to.meta.requiresAuth &&
|
|
||||||
!to.meta.notNeedBindPhone
|
|
||||||
) {
|
|
||||||
dialogStore.openBindPhone();
|
|
||||||
next(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查代理权限
|
|
||||||
if (to.meta.requiresAgent) {
|
|
||||||
if (!isLoaded.value) {
|
|
||||||
await agentStore.fetchAgentStatus();
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查代理权限(仅在 requiresAgent 为 true 时)
|
||||||
|
if (to.meta.requiresAgent) {
|
||||||
|
if (!mobile.value) {
|
||||||
|
if (to.meta.notNeedBindPhone) {
|
||||||
|
dialogStore.openBindPhone();
|
||||||
|
} else {
|
||||||
|
next("/register");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保代理信息已加载
|
||||||
|
if (!isLoaded.value) {
|
||||||
|
try {
|
||||||
|
await agentStore.fetchAgentStatus();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error loading agent info:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果不是代理,跳转到注册页
|
||||||
if (!isAgent.value) {
|
if (!isAgent.value) {
|
||||||
next("/agent/invitationAgentApply/self");
|
next("/register");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
// 其他情况正常通过
|
// 其他情况正常通过
|
||||||
|
// ============================================================
|
||||||
next();
|
next();
|
||||||
})
|
});
|
||||||
|
|
||||||
router.afterEach((to) => {
|
router.afterEach((to) => {
|
||||||
NProgress.done() // 结束进度条
|
NProgress.done(); // 结束进度条
|
||||||
|
|
||||||
// SEO优化:更新页面标题和meta信息
|
// SEO优化:更新页面标题和meta信息
|
||||||
const { updateSEO } = useSEO()
|
const { updateSEO } = useSEO();
|
||||||
|
|
||||||
// 根据路由meta信息更新SEO
|
// 根据路由meta信息更新SEO
|
||||||
if (to.meta.title) {
|
if (to.meta.title) {
|
||||||
const seoConfig = {
|
const seoConfig = {
|
||||||
title: `${to.meta.title} - 一查查`,
|
title: `${to.meta.title} - 一查查`,
|
||||||
description: `一查查${to.meta.title}页面,提供专业的大数据风险管控服务。`,
|
description: `一查查${to.meta.title}页面,提供专业的大数据风险管控服务。`,
|
||||||
url: `https://www.zhinengcha.cn${to.path}`
|
url: `https://www.zhinengcha.cn${to.path}`,
|
||||||
}
|
};
|
||||||
updateSEO(seoConfig)
|
updateSEO(seoConfig);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
export default router
|
export default router;
|
||||||
|
|||||||
58
src/services/authService.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import useApiFetch from "@/composables/useApiFetch";
|
||||||
|
|
||||||
|
class AuthService {
|
||||||
|
detectPlatform() {
|
||||||
|
const ua = navigator.userAgent.toLowerCase();
|
||||||
|
if (/micromessenger/.test(ua)) return "wxh5";
|
||||||
|
return "h5";
|
||||||
|
}
|
||||||
|
|
||||||
|
async authenticate() {
|
||||||
|
const platform = this.detectPlatform();
|
||||||
|
return await this.authByPlatform(platform);
|
||||||
|
}
|
||||||
|
|
||||||
|
async authByPlatform(platform) {
|
||||||
|
switch (platform) {
|
||||||
|
case "h5":
|
||||||
|
return await this.authBrowser();
|
||||||
|
case "wxh5":
|
||||||
|
return await this.authWechatH5();
|
||||||
|
default:
|
||||||
|
return await this.authBrowser();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async authBrowser() {
|
||||||
|
let token = localStorage.getItem("token");
|
||||||
|
if (!token) {
|
||||||
|
const { data } = await useApiFetch("/user/auth")
|
||||||
|
.post({ platform: "h5" })
|
||||||
|
.json();
|
||||||
|
token = data.accessToken;
|
||||||
|
localStorage.setItem("token", token);
|
||||||
|
localStorage.setItem("refreshAfter", data.refreshAfter);
|
||||||
|
localStorage.setItem("accessExpire", data.accessExpire);
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
async authWechatH5() {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const code = params.get("code");
|
||||||
|
if (code) {
|
||||||
|
const { data } = await useApiFetch("/user/auth")
|
||||||
|
.post({ platform: "wxh5", code })
|
||||||
|
.json();
|
||||||
|
window.history.replaceState({}, "", window.location.pathname);
|
||||||
|
localStorage.setItem("token", data.accessToken);
|
||||||
|
localStorage.setItem("refreshAfter", data.refreshAfter);
|
||||||
|
localStorage.setItem("accessExpire", data.accessExpire);
|
||||||
|
return data.accessToken;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const authService = new AuthService();
|
||||||
@@ -1,47 +1,97 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
|
import { getAgentInfo } from '@/api/agent'
|
||||||
|
|
||||||
export const useAgentStore = defineStore('agent', {
|
export const useAgentStore = defineStore('agent', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
isLoaded: false,
|
isLoaded: false,
|
||||||
level: '',
|
level: 0, // 1=普通,2=黄金,3=钻石
|
||||||
status: 3, // 0=待审核,1=审核通过,2=审核未通过,3=未申请
|
levelName: '', // 等级名称
|
||||||
isAgent: false,
|
isAgent: false,
|
||||||
ancestorID: null,
|
|
||||||
agentID: null,
|
agentID: null,
|
||||||
|
agentCode: 0,
|
||||||
mobile: '',
|
mobile: '',
|
||||||
ExpiryTime: '',
|
region: '',
|
||||||
|
wechatId: '',
|
||||||
|
teamLeaderId: null,
|
||||||
isRealName: false,
|
isRealName: false,
|
||||||
}),
|
}),
|
||||||
|
getters: {
|
||||||
|
// 是否是代理
|
||||||
|
isAgentUser: (state) => state.isAgent && state.agentID !== null,
|
||||||
|
// 是否是钻石代理
|
||||||
|
isDiamond: (state) => state.level === 3,
|
||||||
|
// 是否是黄金代理
|
||||||
|
isGold: (state) => state.level === 2,
|
||||||
|
// 是否是普通代理
|
||||||
|
isNormal: (state) => state.level === 1,
|
||||||
|
// 获取等级显示名称
|
||||||
|
levelDisplayName: (state) => {
|
||||||
|
const levelMap = {
|
||||||
|
1: '普通代理',
|
||||||
|
2: '黄金代理',
|
||||||
|
3: '钻石代理'
|
||||||
|
}
|
||||||
|
return levelMap[state.level] || '普通代理'
|
||||||
|
}
|
||||||
|
},
|
||||||
actions: {
|
actions: {
|
||||||
async fetchAgentStatus() {
|
async fetchAgentStatus() {
|
||||||
const { data, error } = await useApiFetch('/agent/info').get().json()
|
const { data, error } = await getAgentInfo()
|
||||||
|
|
||||||
if (data.value && !error.value) {
|
if (data.value && !error.value) {
|
||||||
if (data.value.code === 200) {
|
if (data.value.code === 200) {
|
||||||
this.level = data.value.data.level
|
const agentData = data.value.data
|
||||||
this.isAgent = data.value.data.is_agent // 判断是否是代理
|
// 如果 agent_id 为 0,说明不是代理
|
||||||
this.status = data.value.data.status // 获取代理状态 0=待审核,1=审核通过,2=审核未通过,3=未申请
|
if (agentData.agent_id === 0) {
|
||||||
this.agentID = data.value.data.agent_id
|
this.resetAgent()
|
||||||
this.mobile = data.value.data.mobile
|
} else {
|
||||||
this.ExpiryTime = data.value.data.expiry_time
|
this.level = agentData.level || 0
|
||||||
this.isRealName = data.value.data.is_real_name
|
this.levelName = agentData.level_name || ''
|
||||||
|
this.isAgent = true // 如果能获取到信息,说明是代理
|
||||||
|
this.agentID = agentData.agent_id
|
||||||
|
this.agentCode = agentData.agent_code || 0
|
||||||
|
this.mobile = agentData.mobile || ''
|
||||||
|
this.region = agentData.region || ''
|
||||||
|
this.wechatId = agentData.wechat_id || ''
|
||||||
|
this.teamLeaderId = agentData.team_leader_id || null
|
||||||
|
this.isRealName = agentData.is_real_name || false
|
||||||
|
|
||||||
// 保存到localStorage
|
// 保存到localStorage
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
'agentInfo',
|
'agentInfo',
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
isAgent: this.isAgent,
|
isAgent: this.isAgent,
|
||||||
level: this.level,
|
level: this.level,
|
||||||
status: this.status,
|
levelName: this.levelName,
|
||||||
agentID: this.agentID,
|
agentID: this.agentID,
|
||||||
mobile: this.mobile,
|
agentCode: this.agentCode,
|
||||||
ExpiryTime: this.ExpiryTime,
|
mobile: this.mobile,
|
||||||
isRealName: this.isRealName,
|
region: this.region,
|
||||||
|
wechatId: this.wechatId,
|
||||||
})
|
teamLeaderId: this.teamLeaderId,
|
||||||
)
|
isRealName: this.isRealName,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('Error fetching agent info', data.value)
|
// 检查是否是临时用户需要绑定手机号的错误(100010)
|
||||||
|
if (data.value.code === 100010) {
|
||||||
|
// 临时用户,不重置状态,静默处理
|
||||||
|
console.log('临时用户需要绑定手机号,跳过代理状态检查')
|
||||||
|
} else {
|
||||||
|
// 如果不是代理或获取失败,重置状态
|
||||||
|
this.resetAgent()
|
||||||
|
console.log('Error fetching agent info', data.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 检查错误码
|
||||||
|
if (error.value && error.value.code === 100010) {
|
||||||
|
// 临时用户需要绑定手机号,静默处理
|
||||||
|
console.log('临时用户需要绑定手机号,跳过代理状态检查')
|
||||||
|
} else {
|
||||||
|
// 请求失败或未登录,重置状态
|
||||||
|
this.resetAgent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.isLoaded = true
|
this.isLoaded = true
|
||||||
@@ -51,25 +101,30 @@ export const useAgentStore = defineStore('agent', {
|
|||||||
updateAgentInfo(agentInfo) {
|
updateAgentInfo(agentInfo) {
|
||||||
if (agentInfo) {
|
if (agentInfo) {
|
||||||
this.isAgent = agentInfo.isAgent || false
|
this.isAgent = agentInfo.isAgent || false
|
||||||
this.level = agentInfo.level || ''
|
this.level = agentInfo.level || 0
|
||||||
this.status = agentInfo.status || 3
|
this.levelName = agentInfo.levelName || ''
|
||||||
this.agentID = agentInfo.agentID || null
|
this.agentID = agentInfo.agentID || null
|
||||||
this.mobile = agentInfo.mobile || ''
|
this.mobile = agentInfo.mobile || ''
|
||||||
this.isLoaded = true
|
this.region = agentInfo.region || ''
|
||||||
|
this.wechatId = agentInfo.wechatId || ''
|
||||||
|
this.teamLeaderId = agentInfo.teamLeaderId || null
|
||||||
this.isRealName = agentInfo.isRealName || false
|
this.isRealName = agentInfo.isRealName || false
|
||||||
|
this.isLoaded = true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 重置代理信息
|
// 重置代理信息
|
||||||
resetAgent() {
|
resetAgent() {
|
||||||
this.isLoaded = false
|
this.isLoaded = false
|
||||||
this.level = ''
|
this.level = 0
|
||||||
this.status = 3
|
this.levelName = ''
|
||||||
this.isAgent = false
|
this.isAgent = false
|
||||||
this.ancestorID = null
|
|
||||||
this.agentID = null
|
this.agentID = null
|
||||||
|
this.agentCode = 0
|
||||||
this.mobile = ''
|
this.mobile = ''
|
||||||
|
this.region = ''
|
||||||
|
this.wechatId = ''
|
||||||
|
this.teamLeaderId = null
|
||||||
this.isRealName = false
|
this.isRealName = false
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
21
src/stores/appStore.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useAppStore = defineStore('app', {
|
||||||
|
state: () => ({
|
||||||
|
queryRetentionDays: 0,
|
||||||
|
isLoaded: false,
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
async fetchAppConfig() {
|
||||||
|
try {
|
||||||
|
const { data, error } = await useApiFetch('/app/config').get().json()
|
||||||
|
if (data.value && !error.value && data.value.code === 200) {
|
||||||
|
const cfg = data.value.data
|
||||||
|
this.queryRetentionDays = Number(cfg.query_retention_days) || 0
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
this.isLoaded = true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -6,22 +6,51 @@ export const useAuthStore = defineStore("auth", {
|
|||||||
isWeixinAuthing: false, // 是否正在进行微信授权
|
isWeixinAuthing: false, // 是否正在进行微信授权
|
||||||
weixinAuthComplete: false, // 微信授权是否完成
|
weixinAuthComplete: false, // 微信授权是否完成
|
||||||
pendingRoute: null, // 等待授权完成后跳转的路由
|
pendingRoute: null, // 等待授权完成后跳转的路由
|
||||||
|
authStartTime: 0, // 授权开始时间,用于防止超时重复授权
|
||||||
}),
|
}),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
// 开始微信授权
|
// 开始微信授权
|
||||||
startWeixinAuth(targetRoute = null) {
|
startWeixinAuth(targetRoute = null) {
|
||||||
|
// 如果已经在授权过程中,不再重复启动
|
||||||
|
if (this.isWeixinAuthing) {
|
||||||
|
console.warn("WeChat auth already in progress");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.isWeixinAuthing = true;
|
this.isWeixinAuthing = true;
|
||||||
this.weixinAuthComplete = false;
|
this.weixinAuthComplete = false;
|
||||||
this.pendingRoute = targetRoute;
|
this.pendingRoute = targetRoute;
|
||||||
|
this.authStartTime = Date.now();
|
||||||
|
|
||||||
// 保存到localStorage,防止页面刷新后状态丢失
|
// 保存到localStorage,防止页面刷新后状态丢失
|
||||||
localStorage.setItem("weixinAuthing", "true");
|
localStorage.setItem("weixinAuthing", "true");
|
||||||
|
localStorage.setItem(
|
||||||
|
"authStartTime",
|
||||||
|
this.authStartTime.toString()
|
||||||
|
);
|
||||||
|
|
||||||
if (targetRoute) {
|
if (targetRoute) {
|
||||||
localStorage.setItem(
|
const routeData =
|
||||||
"pendingRoute",
|
typeof targetRoute === "string"
|
||||||
JSON.stringify(targetRoute)
|
? targetRoute
|
||||||
);
|
: targetRoute.fullPath || targetRoute.path || "";
|
||||||
|
if (routeData) {
|
||||||
|
let sanitized = routeData;
|
||||||
|
const parts = routeData.split("?");
|
||||||
|
if (parts.length > 1) {
|
||||||
|
const base = parts[0];
|
||||||
|
const params = new URLSearchParams(parts[1]);
|
||||||
|
params.delete("code");
|
||||||
|
params.delete("state");
|
||||||
|
const qs = params.toString();
|
||||||
|
sanitized = qs ? `${base}?${qs}` : base;
|
||||||
|
}
|
||||||
|
localStorage.setItem(
|
||||||
|
"pendingRoute",
|
||||||
|
JSON.stringify(sanitized)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -29,16 +58,11 @@ export const useAuthStore = defineStore("auth", {
|
|||||||
completeWeixinAuth() {
|
completeWeixinAuth() {
|
||||||
this.isWeixinAuthing = false;
|
this.isWeixinAuthing = false;
|
||||||
this.weixinAuthComplete = true;
|
this.weixinAuthComplete = true;
|
||||||
|
this.authStartTime = 0;
|
||||||
|
|
||||||
// 清除localStorage中的授权状态
|
// 清除localStorage中的授权状态
|
||||||
localStorage.removeItem("weixinAuthing");
|
localStorage.removeItem("weixinAuthing");
|
||||||
localStorage.removeItem("pendingRoute");
|
localStorage.removeItem("authStartTime");
|
||||||
},
|
|
||||||
|
|
||||||
// 清除待处理路由
|
|
||||||
clearPendingRoute() {
|
|
||||||
this.pendingRoute = null;
|
|
||||||
localStorage.removeItem("pendingRoute");
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// 重置授权状态
|
// 重置授权状态
|
||||||
@@ -46,22 +70,47 @@ export const useAuthStore = defineStore("auth", {
|
|||||||
this.isWeixinAuthing = false;
|
this.isWeixinAuthing = false;
|
||||||
this.weixinAuthComplete = false;
|
this.weixinAuthComplete = false;
|
||||||
this.pendingRoute = null;
|
this.pendingRoute = null;
|
||||||
|
this.authStartTime = 0;
|
||||||
localStorage.removeItem("weixinAuthing");
|
localStorage.removeItem("weixinAuthing");
|
||||||
localStorage.removeItem("pendingRoute");
|
localStorage.removeItem("pendingRoute");
|
||||||
|
localStorage.removeItem("authStartTime");
|
||||||
|
},
|
||||||
|
|
||||||
|
// 检查授权是否超时(超过30秒视为超时)
|
||||||
|
isAuthTimeout() {
|
||||||
|
if (!this.authStartTime) return false;
|
||||||
|
const elapsed = Date.now() - this.authStartTime;
|
||||||
|
return elapsed > 30000; // 30秒超时
|
||||||
},
|
},
|
||||||
|
|
||||||
// 从localStorage恢复状态(页面刷新后调用)
|
// 从localStorage恢复状态(页面刷新后调用)
|
||||||
restoreFromStorage() {
|
restoreFromStorage() {
|
||||||
const isAuthing = localStorage.getItem("weixinAuthing") === "true";
|
const isAuthing = localStorage.getItem("weixinAuthing") === "true";
|
||||||
const pendingRouteStr = localStorage.getItem("pendingRoute");
|
const pendingRouteStr = localStorage.getItem("pendingRoute");
|
||||||
|
const authStartTime = localStorage.getItem("authStartTime");
|
||||||
|
|
||||||
if (isAuthing) {
|
if (isAuthing) {
|
||||||
|
console.log("🔄 Restoring WeChat auth state from storage");
|
||||||
this.isWeixinAuthing = true;
|
this.isWeixinAuthing = true;
|
||||||
this.weixinAuthComplete = false;
|
this.weixinAuthComplete = false;
|
||||||
|
this.authStartTime = authStartTime
|
||||||
|
? parseInt(authStartTime)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
// 检查是否超时,如果超时则重置
|
||||||
|
if (this.isAuthTimeout()) {
|
||||||
|
console.warn("WeChat auth timeout, resetting state");
|
||||||
|
this.resetAuthState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (pendingRouteStr) {
|
if (pendingRouteStr) {
|
||||||
try {
|
try {
|
||||||
this.pendingRoute = JSON.parse(pendingRouteStr);
|
this.pendingRoute = JSON.parse(pendingRouteStr);
|
||||||
|
console.log(
|
||||||
|
"✅ Restored pendingRoute from storage:",
|
||||||
|
this.pendingRoute
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to parse pending route:", e);
|
console.error("Failed to parse pending route:", e);
|
||||||
this.pendingRoute = null;
|
this.pendingRoute = null;
|
||||||
@@ -69,5 +118,12 @@ export const useAuthStore = defineStore("auth", {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 清除待处理路由时,确保同步清除内存和localStorage
|
||||||
|
clearPendingRoute() {
|
||||||
|
this.pendingRoute = null;
|
||||||
|
localStorage.removeItem("pendingRoute");
|
||||||
|
console.log("✅ Cleared pendingRoute from both memory and storage");
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { defineStore } from 'pinia'
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
export const useDialogStore = defineStore('dialog', () => {
|
export const useDialogStore = defineStore('dialog', () => {
|
||||||
const showBindPhone = ref(false)
|
const showBindPhone = ref(false) // 推广页面专用的绑定手机号(不要求邀请码)
|
||||||
|
const showRegisterAgent = ref(false) // 注册成为代理(带邀请码)
|
||||||
const showRealNameAuth = ref(false)
|
const showRealNameAuth = ref(false)
|
||||||
|
|
||||||
function openBindPhone() {
|
function openBindPhone() {
|
||||||
@@ -13,6 +14,14 @@ export const useDialogStore = defineStore('dialog', () => {
|
|||||||
showBindPhone.value = false
|
showBindPhone.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openRegisterAgent() {
|
||||||
|
showRegisterAgent.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeRegisterAgent() {
|
||||||
|
showRegisterAgent.value = false
|
||||||
|
}
|
||||||
|
|
||||||
function openRealNameAuth() {
|
function openRealNameAuth() {
|
||||||
showRealNameAuth.value = true
|
showRealNameAuth.value = true
|
||||||
}
|
}
|
||||||
@@ -25,6 +34,9 @@ export const useDialogStore = defineStore('dialog', () => {
|
|||||||
showBindPhone,
|
showBindPhone,
|
||||||
openBindPhone,
|
openBindPhone,
|
||||||
closeBindPhone,
|
closeBindPhone,
|
||||||
|
showRegisterAgent,
|
||||||
|
openRegisterAgent,
|
||||||
|
closeRegisterAgent,
|
||||||
showRealNameAuth,
|
showRealNameAuth,
|
||||||
openRealNameAuth,
|
openRealNameAuth,
|
||||||
closeRealNameAuth,
|
closeRealNameAuth,
|
||||||
|
|||||||
@@ -1,59 +1,76 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from "pinia";
|
||||||
|
|
||||||
export const useUserStore = defineStore('user', {
|
export const useUserStore = defineStore("user", {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
userName: '',
|
userName: "",
|
||||||
mobile: '',
|
mobile: "",
|
||||||
userAvatar: '',
|
userAvatar: "",
|
||||||
isLoggedIn: false,
|
isLoggedIn: false,
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
async fetchUserInfo() {
|
async fetchUserInfo() {
|
||||||
const { data, error } = await useApiFetch('/user/detail').get().json()
|
const { data, error } = await useApiFetch("/user/detail")
|
||||||
if (data.value && !error.value) {
|
.get()
|
||||||
if (data.value.code === 200) {
|
.json();
|
||||||
const userinfo = data.value.data.userInfo
|
if (data.value && !error.value) {
|
||||||
this.userName = userinfo.mobile || ''
|
if (data.value.code === 200) {
|
||||||
this.mobile = userinfo.mobile || ''
|
const userinfo = data.value.data.userInfo;
|
||||||
this.userAvatar = userinfo.userAvatar
|
this.userName = userinfo.mobile || "";
|
||||||
this.isLoggedIn = true
|
this.mobile = userinfo.mobile || "";
|
||||||
|
this.userAvatar = userinfo.userAvatar;
|
||||||
|
this.isLoggedIn = true;
|
||||||
|
|
||||||
// 保存到localStorage
|
// 保存到localStorage
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
'userInfo',
|
"userInfo",
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
nickName: this.userName,
|
nickName: this.userName,
|
||||||
avatar: this.userAvatar,
|
avatar: this.userAvatar,
|
||||||
})
|
})
|
||||||
)
|
);
|
||||||
} else if (data.value.code === 100009) {
|
} else if (data.value.code === 100009) {
|
||||||
localStorage.removeItem('token')
|
// Token 无效或用户不存在,清除数据但不 reload
|
||||||
localStorage.removeItem('refreshAfter')
|
// reload 会导致无限循环
|
||||||
localStorage.removeItem('accessExpire')
|
console.warn(
|
||||||
localStorage.removeItem('userInfo')
|
"User not found or token invalid (100009), clearing auth data"
|
||||||
localStorage.removeItem('agentInfo')
|
);
|
||||||
|
localStorage.removeItem("token");
|
||||||
|
localStorage.removeItem("refreshAfter");
|
||||||
|
localStorage.removeItem("accessExpire");
|
||||||
|
localStorage.removeItem("userInfo");
|
||||||
|
localStorage.removeItem("agentInfo");
|
||||||
|
|
||||||
this.resetUser()
|
this.resetUser();
|
||||||
window.location.reload()
|
|
||||||
}
|
// 不要 reload,让调用者处理错误
|
||||||
} else {
|
throw new Error("User not found or token invalid");
|
||||||
}
|
} else {
|
||||||
|
// 其他错误
|
||||||
|
console.error("Unexpected response code:", data.value.code);
|
||||||
|
throw new Error(
|
||||||
|
`Unexpected response code: ${data.value.code}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error("API error:", error.value);
|
||||||
|
throw error.value || new Error("Unknown error");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新用户信息
|
||||||
|
updateUserInfo(userInfo) {
|
||||||
|
if (userInfo) {
|
||||||
|
this.userName = userInfo.mobile || userInfo.nickName || "";
|
||||||
|
this.userAvatar = userInfo.avatar || "";
|
||||||
|
this.isLoggedIn = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 重置用户信息
|
||||||
|
resetUser() {
|
||||||
|
this.userName = "";
|
||||||
|
this.userAvatar = "";
|
||||||
|
this.isLoggedIn = false;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
});
|
||||||
// 更新用户信息
|
|
||||||
updateUserInfo(userInfo) {
|
|
||||||
if (userInfo) {
|
|
||||||
this.userName = userInfo.mobile || userInfo.nickName || ''
|
|
||||||
this.userAvatar = userInfo.avatar || ''
|
|
||||||
this.isLoggedIn = true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 重置用户信息
|
|
||||||
resetUser() {
|
|
||||||
this.userName = ''
|
|
||||||
this.userAvatar = ''
|
|
||||||
this.isLoggedIn = false
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -338,7 +338,7 @@
|
|||||||
|
|
||||||
<!-- 温馨提示 -->
|
<!-- 温馨提示 -->
|
||||||
<LRemark
|
<LRemark
|
||||||
content="法院曝光台信息展示申请人的各类案件信息,包括涉案公告、执行案件、失信案件和限高案件。数据来源于全国法院执行信息公开网等权威司法数据库。案件状态包括审理中、执行中、已结案等,执行金额和已还款金额直接反映债务履行情况。失信和限高记录对个人信用影响较大,建议重点关注。数据更新频率依赖于司法系统,可能存在延迟。" />
|
content="法院曝光台信息展示申请人的各类案件信息,包括涉案公告、执行案件、失信案件和限高案件。数据来源于全国法院执行信息公开网等权威司法数据库。案件状态包括审理中、执行中、已结案等,执行金额和已还款金额直接反映债务履行情况。失信和限高记录对个人风险影响较大,建议重点关注。数据更新频率依赖于司法系统,可能存在延迟。" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
|||||||
@@ -1,302 +1,305 @@
|
|||||||
// 案件类型映射表
|
// 案件类型映射表
|
||||||
export const lawsuitTypeMap = {
|
export const lawsuitTypeMap = {
|
||||||
sxbzxr: {
|
sxbzxr: {
|
||||||
text: '失信被执行',
|
text: "失信被执行",
|
||||||
color: 'text-red-600 bg-red-50',
|
color: "text-red-600 bg-red-50",
|
||||||
darkColor: 'bg-red-500',
|
darkColor: "bg-red-500",
|
||||||
riskLevel: 'high', // 高风险
|
riskLevel: "high", // 高风险
|
||||||
},
|
},
|
||||||
xgbzxr: {
|
xgbzxr: {
|
||||||
text: '限高被执行',
|
text: "限高被执行",
|
||||||
color: 'text-orange-600 bg-orange-50',
|
color: "text-orange-600 bg-orange-50",
|
||||||
darkColor: 'bg-orange-500',
|
darkColor: "bg-orange-500",
|
||||||
riskLevel: 'high', // 高风险
|
riskLevel: "high", // 高风险
|
||||||
},
|
},
|
||||||
criminal: {
|
criminal: {
|
||||||
text: '刑事案件',
|
text: "刑事案件",
|
||||||
color: 'text-red-600 bg-red-50',
|
color: "text-red-600 bg-red-50",
|
||||||
darkColor: 'bg-red-500',
|
darkColor: "bg-red-500",
|
||||||
riskLevel: 'high', // 高风险
|
riskLevel: "high", // 高风险
|
||||||
},
|
},
|
||||||
civil: {
|
civil: {
|
||||||
text: '民事案件',
|
text: "民事案件",
|
||||||
color: 'text-blue-600 bg-blue-50',
|
color: "text-blue-600 bg-blue-50",
|
||||||
darkColor: 'bg-blue-500',
|
darkColor: "bg-blue-500",
|
||||||
riskLevel: 'medium', // 中风险
|
riskLevel: "medium", // 中风险
|
||||||
},
|
},
|
||||||
administrative: {
|
administrative: {
|
||||||
text: '行政案件',
|
text: "行政案件",
|
||||||
color: 'text-purple-600 bg-purple-50',
|
color: "text-purple-600 bg-purple-50",
|
||||||
darkColor: 'bg-purple-500',
|
darkColor: "bg-purple-500",
|
||||||
riskLevel: 'medium', // 中风险
|
riskLevel: "medium", // 中风险
|
||||||
},
|
},
|
||||||
implement: {
|
implement: {
|
||||||
text: '执行案件',
|
text: "执行案件",
|
||||||
color: 'text-orange-600 bg-orange-50',
|
color: "text-orange-600 bg-orange-50",
|
||||||
darkColor: 'bg-orange-500',
|
darkColor: "bg-orange-500",
|
||||||
riskLevel: 'medium', // 中风险
|
riskLevel: "medium", // 中风险
|
||||||
},
|
},
|
||||||
bankrupt: {
|
bankrupt: {
|
||||||
text: '强制清算与破产案件',
|
text: "强制清算与破产案件",
|
||||||
color: 'text-rose-600 bg-rose-50',
|
color: "text-rose-600 bg-rose-50",
|
||||||
darkColor: 'bg-rose-500',
|
darkColor: "bg-rose-500",
|
||||||
riskLevel: 'high', // 高风险
|
riskLevel: "high", // 高风险
|
||||||
},
|
},
|
||||||
preservation: {
|
preservation: {
|
||||||
text: '非诉保全审查',
|
text: "非诉保全审查",
|
||||||
color: 'text-amber-600 bg-amber-50',
|
color: "text-amber-600 bg-amber-50",
|
||||||
darkColor: 'bg-amber-500',
|
darkColor: "bg-amber-500",
|
||||||
riskLevel: 'low', // 低风险
|
riskLevel: "low", // 低风险
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
// 案件类型文本
|
// 案件类型文本
|
||||||
export const getCaseTypeText = type => {
|
export const getCaseTypeText = (type) => {
|
||||||
return lawsuitTypeMap[type]?.text || '其他案件'
|
return lawsuitTypeMap[type]?.text || "其他案件";
|
||||||
}
|
};
|
||||||
|
|
||||||
// 案件类型颜色
|
// 案件类型颜色
|
||||||
export const getCaseTypeColor = type => {
|
export const getCaseTypeColor = (type) => {
|
||||||
return lawsuitTypeMap[type]?.color || 'text-gray-600 bg-gray-50'
|
return lawsuitTypeMap[type]?.color || "text-gray-600 bg-gray-50";
|
||||||
}
|
};
|
||||||
|
|
||||||
// 案件类型深色
|
// 案件类型深色
|
||||||
export const getCaseTypeDarkColor = type => {
|
export const getCaseTypeDarkColor = (type) => {
|
||||||
return lawsuitTypeMap[type]?.darkColor || 'bg-gray-500'
|
return lawsuitTypeMap[type]?.darkColor || "bg-gray-500";
|
||||||
}
|
};
|
||||||
|
|
||||||
// 格式化日期显示
|
// 格式化日期显示
|
||||||
export const formatDate = dateStr => {
|
export const formatDate = (dateStr) => {
|
||||||
if (!dateStr) return '—'
|
if (!dateStr) return "—";
|
||||||
// 转换YYYY-MM-DD为年月日格式
|
// 转换YYYY-MM-DD为年月日格式
|
||||||
if (dateStr.includes('-')) {
|
if (dateStr.includes("-")) {
|
||||||
const parts = dateStr.split('-')
|
const parts = dateStr.split("-");
|
||||||
if (parts.length === 3) {
|
if (parts.length === 3) {
|
||||||
return `${parts[0]}年${parts[1]}月${parts[2]}日`
|
return `${parts[0]}年${parts[1]}月${parts[2]}日`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
return dateStr; // 如果不是标准格式则返回原始字符串
|
||||||
return dateStr // 如果不是标准格式则返回原始字符串
|
};
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化金额显示(单位:万元)
|
// 格式化金额显示(单位:元)
|
||||||
export const formatLawsuitMoney = money => {
|
export const formatLawsuitMoney = (money) => {
|
||||||
if (!money) return '—'
|
if (!money) return "—";
|
||||||
|
|
||||||
const value = parseFloat(money)
|
const value = parseFloat(money);
|
||||||
if (isNaN(value)) return '—'
|
if (isNaN(value)) return "—";
|
||||||
|
|
||||||
// 超过1亿显示亿元
|
// 直接显示原始金额(元)
|
||||||
if (value >= 10000) {
|
|
||||||
return (
|
return (
|
||||||
(value / 10000).toLocaleString('zh-CN', {
|
value.toLocaleString("zh-CN", {
|
||||||
minimumFractionDigits: 0,
|
minimumFractionDigits: 0,
|
||||||
maximumFractionDigits: 2,
|
maximumFractionDigits: 2,
|
||||||
}) + ' 亿元'
|
}) + " 元"
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
// 否则显示万元
|
|
||||||
return (
|
|
||||||
value.toLocaleString('zh-CN', {
|
|
||||||
minimumFractionDigits: 0,
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
}) + ' 万元'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取案件状态样式
|
// 获取案件状态样式
|
||||||
export const getCaseStatusClass = status => {
|
export const getCaseStatusClass = (status) => {
|
||||||
if (!status) return 'bg-gray-100 text-gray-500'
|
if (!status) return "bg-gray-100 text-gray-500";
|
||||||
|
|
||||||
if (status.includes('已结') || status.includes('已办结')) {
|
if (status.includes("已结") || status.includes("已办结")) {
|
||||||
return 'bg-green-50 text-green-600'
|
return "bg-green-50 text-green-600";
|
||||||
} else if (status.includes('执行中') || status.includes('审理中')) {
|
} else if (status.includes("执行中") || status.includes("审理中")) {
|
||||||
return 'bg-blue-50 text-blue-600'
|
return "bg-blue-50 text-blue-600";
|
||||||
} else if (status.includes('未执行')) {
|
} else if (status.includes("未执行")) {
|
||||||
return 'bg-amber-50 text-amber-600'
|
return "bg-amber-50 text-amber-600";
|
||||||
} else {
|
} else {
|
||||||
return 'bg-gray-100 text-gray-500'
|
return "bg-gray-100 text-gray-500";
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// 获取企业状态对应的样式
|
// 获取企业状态对应的样式
|
||||||
export const getStatusClass = status => {
|
export const getStatusClass = (status) => {
|
||||||
if (!status) return 'bg-gray-100 text-gray-500'
|
if (!status) return "bg-gray-100 text-gray-500";
|
||||||
|
|
||||||
if (status.includes('注销') || status.includes('吊销')) {
|
if (status.includes("注销") || status.includes("吊销")) {
|
||||||
return 'bg-red-50 text-red-600'
|
return "bg-red-50 text-red-600";
|
||||||
} else if (status.includes('存续') || status.includes('在营')) {
|
} else if (status.includes("存续") || status.includes("在营")) {
|
||||||
return 'bg-green-50 text-green-600'
|
return "bg-green-50 text-green-600";
|
||||||
} else if (status.includes('筹建') || status.includes('新设')) {
|
} else if (status.includes("筹建") || status.includes("新设")) {
|
||||||
return 'bg-blue-50 text-blue-600'
|
return "bg-blue-50 text-blue-600";
|
||||||
} else {
|
} else {
|
||||||
return 'bg-yellow-50 text-yellow-600'
|
return "bg-yellow-50 text-yellow-600";
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// 格式化资本金额显示
|
// 格式化资本金额显示
|
||||||
export const formatCapital = (capital, currency) => {
|
export const formatCapital = (capital, currency) => {
|
||||||
if (!capital) return '—'
|
if (!capital) return "—";
|
||||||
|
|
||||||
// 检查是否包含"万"字或需要显示为万元
|
// 检查是否包含"万"字或需要显示为万元
|
||||||
let unit = ''
|
let unit = "";
|
||||||
let value = parseFloat(capital)
|
let value = parseFloat(capital);
|
||||||
|
|
||||||
// 处理原始数据中可能带有的单位
|
// 处理原始数据中可能带有的单位
|
||||||
if (typeof capital === 'string' && capital.includes('万')) {
|
if (typeof capital === "string" && capital.includes("万")) {
|
||||||
unit = '万'
|
unit = "万";
|
||||||
// 提取数字部分
|
// 提取数字部分
|
||||||
const numMatch = capital.match(/[\d.]+/)
|
const numMatch = capital.match(/[\d.]+/);
|
||||||
value = numMatch ? parseFloat(numMatch[0]) : 0
|
value = numMatch ? parseFloat(numMatch[0]) : 0;
|
||||||
} else if (value >= 10000) {
|
} else if (value >= 10000) {
|
||||||
// 大额数字转换为万元显示
|
// 大额数字转换为万元显示
|
||||||
value = value / 10000
|
value = value / 10000;
|
||||||
unit = '万'
|
unit = "万";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化数字,保留两位小数(如果有小数部分)
|
// 格式化数字,保留两位小数(如果有小数部分)
|
||||||
const formattedValue = value.toLocaleString('zh-CN', {
|
const formattedValue = value.toLocaleString("zh-CN", {
|
||||||
minimumFractionDigits: 0,
|
minimumFractionDigits: 0,
|
||||||
maximumFractionDigits: 2,
|
maximumFractionDigits: 2,
|
||||||
})
|
});
|
||||||
|
|
||||||
return `${formattedValue}${unit} ${currency || '人民币'}`
|
return `${formattedValue}${unit} ${currency || "人民币"}`;
|
||||||
}
|
};
|
||||||
|
|
||||||
// 获取涉诉风险等级
|
// 获取涉诉风险等级
|
||||||
export const getRiskLevel = lawsuitInfo => {
|
export const getRiskLevel = (lawsuitInfo) => {
|
||||||
if (!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 {
|
return {
|
||||||
level: 'low',
|
level: "low",
|
||||||
text: '低风险',
|
text: "低风险",
|
||||||
color: 'text-green-600 bg-green-50',
|
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',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取涉诉案件统计
|
// 获取涉诉案件统计
|
||||||
export const getLawsuitStats = lawsuitInfo => {
|
export const getLawsuitStats = (lawsuitInfo) => {
|
||||||
if (!lawsuitInfo) return null
|
if (!lawsuitInfo) return null;
|
||||||
|
|
||||||
const stats = {
|
const stats = {
|
||||||
total: 0,
|
total: 0,
|
||||||
types: [],
|
types: [],
|
||||||
}
|
};
|
||||||
|
|
||||||
// 统计各类型案件数量
|
// 统计各类型案件数量
|
||||||
Object.keys(lawsuitTypeMap).forEach(type => {
|
Object.keys(lawsuitTypeMap).forEach((type) => {
|
||||||
let count = 0
|
let count = 0;
|
||||||
|
|
||||||
if (type === 'sxbzxr') {
|
if (type === "sxbzxr") {
|
||||||
count = lawsuitInfo.sxbzxr && lawsuitInfo.sxbzxr.length > 0 ? lawsuitInfo.sxbzxr.length : 0
|
count =
|
||||||
} else if (type === 'xgbzxr') {
|
lawsuitInfo.sxbzxr && lawsuitInfo.sxbzxr.length > 0
|
||||||
count = lawsuitInfo.xgbzxr && lawsuitInfo.xgbzxr.length > 0 ? lawsuitInfo.xgbzxr.length : 0
|
? lawsuitInfo.sxbzxr.length
|
||||||
} else if (lawsuitInfo.data && lawsuitInfo.data[type] && Object.keys(lawsuitInfo.data[type]).length > 0) {
|
: 0;
|
||||||
const typeData = lawsuitInfo.data[type]
|
} else if (type === "xgbzxr") {
|
||||||
count = typeData.cases && typeData.cases.length ? typeData.cases.length : 0
|
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) {
|
if (count > 0) {
|
||||||
stats.total += count
|
stats.total += count;
|
||||||
stats.types.push({
|
stats.types.push({
|
||||||
type,
|
type,
|
||||||
count,
|
count,
|
||||||
name: getCaseTypeText(type),
|
name: getCaseTypeText(type),
|
||||||
color: getCaseTypeColor(type),
|
color: getCaseTypeColor(type),
|
||||||
darkColor: getCaseTypeDarkColor(type),
|
darkColor: getCaseTypeDarkColor(type),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
return stats
|
return stats;
|
||||||
}
|
};
|
||||||
|
|
||||||
// 获取案件类型优先级顺序
|
// 获取案件类型优先级顺序
|
||||||
export const getCaseTypePriority = () => {
|
export const getCaseTypePriority = () => {
|
||||||
return [
|
return [
|
||||||
'sxbzxr', // 失信被执行人(最高风险)
|
"sxbzxr", // 失信被执行人(最高风险)
|
||||||
'xgbzxr', // 限高被执行人
|
"xgbzxr", // 限高被执行人
|
||||||
'criminal', // 刑事案件
|
"criminal", // 刑事案件
|
||||||
'civil', // 民事案件
|
"civil", // 民事案件
|
||||||
'administrative', // 行政案件
|
"administrative", // 行政案件
|
||||||
'implement', // 执行案件
|
"implement", // 执行案件
|
||||||
'bankrupt', // 强制清算与破产案件
|
"bankrupt", // 强制清算与破产案件
|
||||||
'preservation', // 非诉保全审查
|
"preservation", // 非诉保全审查
|
||||||
]
|
];
|
||||||
}
|
};
|
||||||
|
|
||||||
// 根据案件类型获取风险等级
|
// 根据案件类型获取风险等级
|
||||||
export const getCaseTypeRiskLevel = caseType => {
|
export const getCaseTypeRiskLevel = (caseType) => {
|
||||||
const typeInfo = lawsuitTypeMap[caseType]
|
const typeInfo = lawsuitTypeMap[caseType];
|
||||||
if (!typeInfo) {
|
if (!typeInfo) {
|
||||||
return {
|
return {
|
||||||
level: 'low',
|
level: "low",
|
||||||
text: '低风险',
|
text: "低风险",
|
||||||
color: 'text-green-600 bg-green-50',
|
color: "text-green-600 bg-green-50",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const riskLevelMap = {
|
const riskLevelMap = {
|
||||||
high: {
|
high: {
|
||||||
text: '高风险',
|
text: "高风险",
|
||||||
color: 'text-red-600 bg-red-50',
|
color: "text-red-600 bg-red-50",
|
||||||
},
|
},
|
||||||
medium: {
|
medium: {
|
||||||
text: '中风险',
|
text: "中风险",
|
||||||
color: 'text-amber-600 bg-amber-50',
|
color: "text-amber-600 bg-amber-50",
|
||||||
},
|
},
|
||||||
low: {
|
low: {
|
||||||
text: '低风险',
|
text: "低风险",
|
||||||
color: 'text-green-600 bg-green-50',
|
color: "text-green-600 bg-green-50",
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
level: typeInfo.riskLevel,
|
level: typeInfo.riskLevel,
|
||||||
...riskLevelMap[typeInfo.riskLevel],
|
...riskLevelMap[typeInfo.riskLevel],
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -465,12 +465,12 @@ const riskScore = computed(() => {
|
|||||||
const lowRiskCount = summaryData.value.byRiskLevel.find(item => item.id === 'low')?.triggered || 0;
|
const lowRiskCount = summaryData.value.byRiskLevel.find(item => item.id === 'low')?.triggered || 0;
|
||||||
|
|
||||||
// 计算风险分数
|
// 计算风险分数
|
||||||
// 高风险项:每个扣 30 分
|
// 高风险项(无法收回):每个扣 40 分
|
||||||
// 中风险项:每个扣 15 分
|
// 中风险项(严重逾期):每个扣 20 分
|
||||||
// 低风险项:每个扣 5 分
|
// 低风险项(短期逾期):每个扣 5 分
|
||||||
let score = 100;
|
let score = 100;
|
||||||
score -= highRiskCount * 30;
|
score -= highRiskCount * 40;
|
||||||
score -= mediumRiskCount * 15;
|
score -= mediumRiskCount * 20;
|
||||||
score -= lowRiskCount * 5;
|
score -= lowRiskCount * 5;
|
||||||
|
|
||||||
return Math.max(0, Math.min(100, score));
|
return Math.max(0, Math.min(100, score));
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
|
|
||||||
<!-- 所有风险类型列表 -->
|
<!-- 所有风险类型列表 -->
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<!-- 正常人员 -->
|
<!-- 人员状态 -->
|
||||||
<div class="rounded-lg p-4 border-2 relative" :class="getRiskItemClass('0')">
|
<div class="rounded-lg p-4 border-2 relative" :class="getRiskItemClass('0')">
|
||||||
<div
|
<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')]">
|
: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>
|
||||||
<div class="flex items-center pr-12">
|
<div class="flex items-center pr-12">
|
||||||
<div class="w-8 h-8 mr-3 flex-shrink-0 flex items-center justify-center">
|
<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>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="font-bold text-sm" :class="getRiskItemTextColor('0')">
|
<div class="font-bold text-sm" :class="getRiskItemTextColor('0')">
|
||||||
{{ getRiskTypeInfo('0').text }}
|
{{ isNormalPerson ? '正常人员' : '人员状态' }}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -460,12 +462,12 @@ const isHit = (code) => {
|
|||||||
// 如果level是'0',则正常人员命中
|
// 如果level是'0',则正常人员命中
|
||||||
return hitRiskCodes.value.includes('0')
|
return hitRiskCodes.value.includes('0')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果直接包含该代码,则命中
|
// 如果直接包含该代码,则命中
|
||||||
if (hitRiskCodes.value.includes(code)) {
|
if (hitRiskCodes.value.includes(code)) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 对于父级类型(A、B、C、D),如果子类型命中,父类型也算命中
|
// 对于父级类型(A、B、C、D),如果子类型命中,父类型也算命中
|
||||||
if (code === 'A') {
|
if (code === 'A') {
|
||||||
// 如果 A1、A2、A3、A4、A5 任何一个命中,A 也算命中
|
// 如果 A1、A2、A3、A4、A5 任何一个命中,A 也算命中
|
||||||
@@ -483,7 +485,7 @@ const isHit = (code) => {
|
|||||||
// 如果 D1、D2、D3、D4、D5 任何一个命中,D 也算命中
|
// 如果 D1、D2、D3、D4、D5 任何一个命中,D 也算命中
|
||||||
return ['D1', 'D2', 'D3', 'D4', 'D5'].some(subCode => hitRiskCodes.value.includes(subCode))
|
return ['D1', 'D2', 'D3', 'D4', 'D5'].some(subCode => hitRiskCodes.value.includes(subCode))
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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}`">
|
:class="`status-label rounded-full px-6 py-3 text-center font-bold shadow-md ${currentStatus.bgClass} ${currentStatus.textClass}`">
|
||||||
{{ currentStatus.text }}
|
{{ currentStatus.text }}
|
||||||
</div>
|
</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>
|
<p v-html="currentStatus.description" class="status-description mt-3 text-sm text-gray-600"></p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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>
|
<style lang="scss" scoped>
|
||||||
.status-info {
|
.status-info {
|
||||||
|
|||||||