Compare commits

...

20 Commits

Author SHA1 Message Date
df78375d0a Merge branch 'main' of http://1.117.67.95:3000/team/ycc-proxy-webview 2026-06-01 15:05:44 +08:00
14d282b366 f 2026-06-01 15:05:27 +08:00
Mrx
be1b1faf86 f 2026-05-08 16:43:17 +08:00
Mrx
24b068574c f 2026-05-06 16:53:49 +08:00
Mrx
c7d23975b3 Merge branch 'main' of http://1.117.67.95:3000/team/ycc-proxy-webview 2026-03-21 11:37:32 +08:00
Mrx
e37c4e446f close date 2026-03-21 11:37:24 +08:00
777f2e2a7b Merge branch 'main' of http://1.117.67.95:3000/team/ycc-proxy-webview 2026-03-16 16:42:30 +08:00
14d7131447 借贷意向分值权重调整 2026-03-16 16:41:44 +08:00
Mrx
1652ee1e55 1 2026-03-05 16:54:19 +08:00
Mrx
030ed260c1 f 2026-02-28 12:18:33 +08:00
Mrx
576ab0c362 up add sms 2026-02-25 16:37:42 +08:00
Mrx
3e2d828f4d open 2026-02-14 16:33:46 +08:00
Mrx
f48052ff10 f 2026-02-09 12:30:32 +08:00
3eb3636b22 f 2026-02-08 16:56:41 +08:00
9e0b38a6d5 修改:我的团队页面item统计项修改 2025-12-16 19:33:48 +08:00
0e210505f5 修改:推广报告设置价格弹窗提价成本提示修改 2025-12-16 19:33:21 +08:00
db4f287bf5 新增:JRZQ6F2A借贷申请组件 2025-12-16 19:32:45 +08:00
c96def406e 新增:代理系统指南 2025-12-16 19:29:49 +08:00
bceca129be 付费升级代理页面修改 2025-12-16 19:28:00 +08:00
430b8f12ba t 2025-12-16 19:27:20 +08:00
180 changed files with 16378 additions and 5836 deletions

13
.env
View File

@@ -1,16 +1,23 @@
VITE_API_URL=
VITE_API_PREFIX=/api/v1
VITE_COMPANY_NAME=海南省学宇思网络科技有限公司
VITE_COMPANY_NAME=海南海宇大数据有限公司
VITE_INQUIRE_AES_KEY=ff83609b2b24fc73196aac3d3dfb874f
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_IV=345GDFED433223DF
VITE_SHARE_TITLE=一查查|大数据风险报告查询与代理平台,支持个人和企业多场景风控应用
VITE_SHARE_DESC=提供个人信用评估、入职背调、信贷风控、企业风险监测等服务
VITE_SHARE_IMG=https://www.tianyuandb.com/logo.png
VITE_SHARE_DESC=提供个人风险评估、入职背调、信贷风控、企业风险监测等服务
VITE_SHARE_IMG=https://www.onecha.cn/logo.png
VITE_TOKEN_VERSION=1.5

View File

@@ -3,10 +3,24 @@
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" 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">
<link
rel="apple-touch-icon"
sizes="180x180"
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
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=3, user-scalable=no"
@@ -18,7 +32,7 @@
</title>
<meta
name="description"
content="一查查,专业大数据风险报告查询与代理平台,支持个人信用查询、小微企业风控、贷前风险背调等多场景报告应用,免费开通代理权限,助力高效识别信用与风险。"
content="一查查,专业大数据风险报告查询与代理平台,支持个人风险查询、小微企业风控、贷前风险背调等多场景报告应用,免费开通代理权限,助力高效识别信用与风险。"
/>
<meta
name="keywords"
@@ -32,16 +46,28 @@
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://www.zhinengcha.cn/" />
<meta property="og:title" content="一查查|大数据风险报告查询与代理平台,支持个人和企业多场景风控应用" />
<meta property="og:description" content="一查查,专业大数据风险报告查询与代理平台,支持个人信用查询、小微企业风控、贷前风险背调等多场景报告应用,免费开通代理权限,助力高效识别信用与风险。" />
<meta
property="og:title"
content="一查查|大数据风险报告查询与代理平台,支持个人和企业多场景风控应用"
/>
<meta
property="og:description"
content="一查查,专业大数据风险报告查询与代理平台,支持个人风险查询、小微企业风控、贷前风险背调等多场景报告应用,免费开通代理权限,助力高效识别信用与风险。"
/>
<meta property="og:site_name" content="一查查" />
<meta property="og:locale" content="zh_CN" />
<!-- Twitter -->
<meta property="twitter:card" content="summary" />
<meta property="twitter:url" content="https://www.zhinengcha.cn/" />
<meta property="twitter:title" content="一查查|大数据风险报告查询与代理平台,支持个人和企业多场景风控应用" />
<meta property="twitter:description" content="一查查,专业大数据风险报告查询与代理平台,支持个人信用查询、小微企业风控、贷前风险背调等多场景报告应用,免费开通代理权限,助力高效识别信用与风险。" />
<meta
property="twitter:title"
content="一查查|大数据风险报告查询与代理平台,支持个人和企业多场景风控应用"
/>
<meta
property="twitter:description"
content="一查查,专业大数据风险报告查询与代理平台,支持个人风险查询、小微企业风控、贷前风险背调等多场景报告应用,免费开通代理权限,助力高效识别信用与风险。"
/>
<!-- 其他重要meta标签 -->
<meta name="theme-color" content="#3498db" />
@@ -58,7 +84,7 @@
"@type": "WebSite",
"name": "一查查",
"url": "https://www.zhinengcha.cn/",
"description": "专业大数据风险报告查询与代理平台,支持个人信用查询、小微企业风控、贷前风险背调等多场景报告应用",
"description": "专业大数据风险报告查询与代理平台,支持个人风险查询、小微企业风控、贷前风险背调等多场景报告应用",
"potentialAction": {
"@type": "SearchAction",
"target": "https://www.zhinengcha.cn/search?q={search_term_string}",
@@ -83,11 +109,17 @@
delete window.wx;
</script>
<!-- 阿里云滑块验证码 -->
<script>
window.AliyunCaptchaConfig = { region: "cn", prefix: "12zxnj" };
</script>
<script type="text/javascript" src="https://o.alicdn.com/captcha-frontend/aliyunCaptcha/AliyunCaptcha.js"></script>
<!-- 预加载关键资源 -->
<link rel="preconnect" href="https://www.zhinengcha.cn">
<link rel="preconnect" href="https://res.wx.qq.com">
<link rel="dns-prefetch" href="https://www.zhinengcha.cn">
<link rel="dns-prefetch" href="https://res.wx.qq.com">
<link rel="preconnect" href="https://www.zhinengcha.cn" />
<link rel="preconnect" href="https://res.wx.qq.com" />
<link rel="dns-prefetch" href="https://www.zhinengcha.cn" />
<link rel="dns-prefetch" href="https://res.wx.qq.com" />
<style>
/* 基础样式 */
@@ -181,6 +213,8 @@
<div class="loading-text">加载中</div>
</div>
<div id="app"></div>
<!-- 阿里云验证码容器 -->
<div id="captcha-element"></div>
<script type="module" src="/src/main.js"></script>
</body>

1
lEbH141J7d.txt Normal file
View File

@@ -0,0 +1 @@
f07fc7c629885231180fb79885dba876

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@@ -1,7 +1,7 @@
{
"name": "一查查|大数据风险报告查询与代理平台,支持个人和企业多场景风控应用",
"short_name": "一查查",
"description": "专业大数据风险报告查询与代理平台,支持个人信用查询、小微企业风控、贷前风险背调等多场景报告应用",
"description": "专业大数据风险报告查询与代理平台,支持个人风险查询、小微企业风控、贷前风险背调等多场景报告应用",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
@@ -34,7 +34,11 @@
"type": "image/png"
}
],
"categories": ["business", "finance", "utilities"],
"categories": [
"business",
"finance",
"utilities"
],
"lang": "zh-CN",
"dir": "ltr"
}

View File

@@ -1,211 +1,322 @@
<script setup>
import { RouterLink, RouterView, useRouter } from "vue-router";
import { RouterView, useRouter, useRoute } from "vue-router";
const { isWeChat } = useEnv();
import { useAgentStore } from "@/stores/agentStore";
import { useUserStore } from "@/stores/userStore";
import { useDialogStore } from "@/stores/dialogStore";
import { useAuthStore } from "@/stores/authStore";
import { useAppStore } from "@/stores/appStore";
import { useWeixinShare } from "@/composables/useWeixinShare";
import BindPhoneDialog from "@/components/BindPhoneDialog.vue";
import BindPhoneOnlyDialog from "@/components/BindPhoneOnlyDialog.vue";
const router = useRouter();
const route = useRoute();
const agentStore = useAgentStore();
const userStore = useUserStore();
const dialogStore = useDialogStore();
const authStore = useAuthStore();
const { configWeixinShare, setDynamicShare } = useWeixinShare();
const appStore = useAppStore();
const { setDynamicShare } = useWeixinShare();
onMounted(() => {
// 检查token版本如果版本不匹配则清除旧token
checkTokenVersion()
onMounted(async () => {
// 初始化应用配置
await appStore.fetchAppConfig();
// 恢复微信授权状态(页面刷新
// 恢复微信授权状态(页面刷新或回调时
authStore.restoreFromStorage();
// 检查是否是微信授权回调
const url = new URL(window.location.href);
const hasWeixinCode = url.searchParams.has('code') && url.searchParams.has('state');
if (hasWeixinCode) {
// 如果是授权回调,标记为正在授权
authStore.startWeixinAuth();
}
RefreshToken();
const token = localStorage.getItem("token");
if (token) {
agentStore.fetchAgentStatus();
userStore.fetchUserInfo();
}
// 配置微信分享
// 延迟执行确保微信SDK已加载
setTimeout(async () => {
if (isWeChat.value && window.jWeixin) {
await setDynamicShare();
}
}, 500);
// 监听路由变化,更新微信分享配置
router.afterEach(() => {
setTimeout(async () => {
if (isWeChat.value && window.jWeixin) {
await setDynamicShare();
}
}, 300);
});
});
const checkTokenVersion = () => {
// 设置新的token版本号当后端token格式改变时修改这个版本号
const CURRENT_TOKEN_VERSION = '2.1'
const storedTokenVersion = localStorage.getItem('tokenVersion')
if (!storedTokenVersion || storedTokenVersion !== CURRENT_TOKEN_VERSION) {
// 清除所有旧的认证信息
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检查当前时间是否超过 refreshAfterrefreshAfter 是秒级,需要转换为毫秒级)
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");
const code = url.searchParams.get("code");
const state = url.searchParams.get("state");
if (code && state) {
// 这是微信授权回调,处理授权结果
console.log("Handling WeChat auth callback");
await handleWeixinAuthCallback(code);
} else {
// 正常初始化:加载用户信息等
await initializeApp();
}
getWeixinAuthUrl();
// 延迟配置微信分享
setTimeout(async () => {
if (isWeChat.value && window.jWeixin) {
await setDynamicShare();
}
}, 1000);
// 监听路由变化更新分享配置
router.afterEach(async () => {
if (isWeChat.value && window.jWeixin && !authStore.isWeixinAuthing) {
await setDynamicShare();
}
});
});
/**
* 处理微信授权回调
*/
const handleWeixinAuthCallback = async (code) => {
try {
console.log("🔄 WeChat auth callback: code=", code);
// 调用后端接口交换 token
const { data, error } = await useApiFetch("/user/wxh5Auth")
.post({ code })
.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
);
console.log("📡 wxh5Auth response:", {
code: data.value?.code,
hasToken: !!data.value?.data?.accessToken,
error: error.value
});
if (data.value && !error.value && data.value.code === 200) {
// 保存 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);
console.log("✅ Token saved successfully, token:", token.substring(0, 20) + "...");
console.log("✅ Token saved to localStorage, userId:", data.value.data.userId || "unknown");
console.log(`✅ TokenVersion set to ${tokenVersion}`);
// 验证 token 是否真的保存成功
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 参数
const url = new URL(window.location.href);
const params = new URLSearchParams(url.search);
params.delete("code");
params.delete("state");
// 更新 URL不刷新页面
const newUrl = `${url.origin}${url.pathname
}?${params.toString()}`;
const newUrl = `${url.origin}${url.pathname}${params.toString() ? "?" + params.toString() : ""}`;
window.history.replaceState({}, "", newUrl);
console.log("✅ URL cleaned, removed code and state parameters");
// 标记微信授权完成
// 获取用户信息
console.log("👤 Fetching user info...");
try {
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");
// 获取用户和代理信息
await Promise.all([
agentStore.fetchAgentStatus(),
userStore.fetchUserInfo()
]);
// 获取 pendingRoute 并跳转
const pendingRoute = authStore.pendingRoute
console.log("🎯 pendingRoute:", pendingRoute);
// 如果有待处理的路由,跳转到该路由
if (authStore.pendingRoute) {
router.replace(authStore.pendingRoute);
authStore.clearPendingRoute();
}
}
}
// 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 {
// 没有授权参数,需要开始微信授权
// 保存当前路由作为授权完成后的目标路由
const currentRoute = router.currentRoute.value;
authStore.startWeixinAuth(currentRoute);
h5WeixinGetCode();
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");
}
} catch (err) {
console.error("❌ Error handling WeChat auth callback:", err);
authStore.resetAuthState();
await router.replace("/login");
}
};
const h5WeixinGetCode = () => {
const currentUrl = window.location.href;
let redirectUri = encodeURIComponent(currentUrl);
let appId = import.meta.env.VITE_WECHAT_APP_ID;
let state = "snsapi_base";
let scope = "snsapi_base";
let authUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}&redirect_uri=${redirectUri}&response_type=code&scope=${scope}&state=${state}#wechat_redirect`;
// 跳转到授权URL
window.location.href = authUrl;
/**
* 初始化应用:检查 token加载用户信息
*/
const initializeApp = async () => {
// 检查 token 版本
checkTokenVersion();
// 尝试刷新 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>
<template>
<RouterView />
<BindPhoneDialog />
<BindPhoneOnlyDialog />
</template>
<style scoped></style>

359
src/api/agent.js Normal file
View 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();
}

View File

@@ -1,8 +1,24 @@
import axios from "axios";
import useApiFetch from "@/composables/useApiFetch";
// 获取API基础URL与生产规则一致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
export function cancelAccount() {
return axios({

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 1.5 MiB

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -93,7 +93,9 @@ declare global {
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const showConfirmDialog: typeof import('vant/es')['showConfirmDialog']
const showFailToast: typeof import('vant/es')['showFailToast']
const showLoadingToast: typeof import('vant/es')['showLoadingToast']
const showSuccessToast: typeof import('vant/es')['showSuccessToast']
const showToast: typeof import('vant/es')['showToast']
const syncRef: typeof import('@vueuse/core')['syncRef']
const syncRefs: typeof import('@vueuse/core')['syncRefs']
@@ -117,8 +119,10 @@ declare global {
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
const useAgent: typeof import('./composables/useAgent.js')['useAgent']
const useAgentStore: typeof import('./stores/agentStore.js')['useAgentStore']
const useAliyunCaptcha: typeof import('./composables/useAliyunCaptcha.js')['default']
const useAnimate: typeof import('@vueuse/core')['useAnimate']
const useApiFetch: typeof import('./composables/useApiFetch.js')['default']
const useAppStore: typeof import('./stores/appStore.js')['useAppStore']
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
@@ -248,6 +252,7 @@ declare global {
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
const useShare: typeof import('@vueuse/core')['useShare']
const useShareReport: typeof import('./composables/useShareReport.js')['useShareReport']
const useSlots: typeof import('vue')['useSlots']
const useSorted: typeof import('@vueuse/core')['useSorted']
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']

View File

@@ -6,17 +6,22 @@
>
成为代理
</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">
<van-field
label-width="56"
v-model="form.referrer"
label="邀请信息"
name="referrer"
placeholder="请输入邀请码/代理编码/代理手机号"
required
/>
<van-field
label-width="56"
v-model="form.region"
is-link
readonly
label="地区"
placeholder="请选择地区"
placeholder="请选择地区(可选)"
@click="showCascader = true"
/>
<van-popup v-model:show="showCascader" round position="bottom">
@@ -121,13 +126,11 @@
const router = useRouter();
const show = defineModel("show");
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 props = defineProps({
ancestor: {
type: String,
required: true,
},
isSelf: {
type: Boolean,
default: false,
@@ -137,15 +140,17 @@ const props = defineProps({
default: "",
},
});
const { ancestor, isSelf, userName } = toRefs(props);
const { isSelf, userName } = toRefs(props);
const form = ref({
referrer: "",
region: "",
mobile: "",
code: "", // 增加验证码字段
code: "", // 验证码字段
});
const showCascader = ref(false);
const cascaderValue = ref("");
const options = useCascaderAreaData();
const { runWithCaptcha } = useAliyunCaptcha();
const loadingSms = ref(false); // 控制验证码按钮的loading状态
const isCountingDown = ref(false);
const isAgreed = ref(false);
@@ -170,21 +175,20 @@ const getSmsCode = async () => {
}
loadingSms.value = true;
const { data, error } = await useApiFetch("auth/sendSms")
.post({ mobile: form.value.mobile, actionType: "agentApply" })
.json();
runWithCaptcha(
(captchaVerifyParam) => useApiFetch("auth/sendSms")
.post({ mobile: form.value.mobile, actionType: "agentApply", captchaVerifyParam })
.json(),
(result) => {
loadingSms.value = false;
if (data.value && !error.value) {
if (data.value.code === 200) {
if (result && result.code === 200) {
showToast({ message: "获取成功" });
startCountdown(); // 启动倒计时
} else {
showToast(data.value.msg);
startCountdown();
} else if (result) {
showToast(result.msg || "发送失败");
}
}
);
};
let timer = null;
@@ -207,10 +211,11 @@ onUnmounted(() => {
});
const submit = () => {
// 校验表单字段
if (!form.value.region) {
showToast({ message: "请选择地区" });
if (!form.value.referrer || !form.value.referrer.trim()) {
showToast({ message: "请输入邀请信息" });
return;
}
if (!form.value.mobile) {
showToast({ message: "请输入手机号" });
return;
@@ -231,15 +236,13 @@ const submit = () => {
showToast({ message: "请先阅读并同意用户协议及相关条款" });
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 = () => {
emit("close");
};

View File

@@ -31,7 +31,7 @@
本人在授权签署前已通过实名认证及动态验证码验证或其他身份验证手段确认本授权行为为本人真实意思表示平台已履行身份验证义务
</li>
<li>
本人在此声明已充分理解上述授权条款含义知晓并自愿承担因授权数据使用可能带来的后果包括但不限于影响个人信用评分生活行为等本人确认授权范围内的相关信息由本人提供并真实有效
本人在此声明已充分理解上述授权条款含义知晓并自愿承担因授权数据使用可能带来的后果包括但不限于影响个人风险评分生活行为等本人确认授权范围内的相关信息由本人提供并真实有效
</li>
<li>
若用户冒名签署或提供虚假信息由用户自行承担全部法律责任平台不承担任何后果
@@ -87,7 +87,7 @@
<p class="mt-4 font-bold">
签署人<span class="underline">{{
signature ? props.name : "____________"
}}</span>
}}</span>
<br />
手机号码<span class="underline">
{{ signature ? props.mobile : "____________" }}

View File

@@ -3,11 +3,18 @@ import ShareReportButton from "./ShareReportButton.vue";
import TitleBanner from "./TitleBanner.vue";
import VerificationCard from "./VerificationCard.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 { splitDWBG6A2CForTabs } from '@/ui/DWBG6A2C/utils/simpleSplitter.js';
import { splitJRZQ7F1AForTabs } from '@/ui/JRZQ7F1A/utils/simpleSplitter.js';
import { splitCJRZQ5E9FForTabs } from '@/ui/CJRZQ5E9F/utils/simpleSplitter.js';
import { splitCQYGL3F8EForTabs } from '@/ui/CQYGL3F8E/utils/simpleSplitter.js';
import { useAppStore } from "@/stores/appStore";
import { useAgentStore } from "@/stores/agentStore";
import { storeToRefs } from 'pinia';
import { showSuccessToast, showFailToast, showConfirmDialog } from 'vant';
import { checkFeatureWhitelistStatus, offlineFeature, checkOrderAgent } from '@/api/agent';
// 动态导入产品背景图片的函数
const loadProductBackground = async (productType) => {
@@ -17,7 +24,7 @@ const loadProductBackground = async (productType) => {
return (await import("@/assets/images/report/xwqy_inquire_bg.png")).default;
case 'preloanbackgroundcheck':
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;
case 'marriage':
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({
isShare: {
type: Boolean,
@@ -50,6 +59,11 @@ const props = defineProps({
type: String,
default: "",
},
queryId: {
type: String,
required: false,
default: "",
},
feature: {
type: String,
required: true,
@@ -95,8 +109,328 @@ const {
isEmpty,
isDone,
isExample,
orderId,
orderNo,
queryId,
} = 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 backgroundContainerRef = ref(null); // 背景容器的引用
@@ -150,6 +484,23 @@ onMounted(async () => {
// 监听窗口大小变化,重新计算高度
window.addEventListener('resize', handleResize);
// 检查订单是否属于当前代理推广(含分享页,以便分享页也能显示下线按钮)
if (!isExample.value && orderId.value) {
try {
const { data, error } = await checkOrderAgent({ order_id: orderId.value })
if (data.value && !error.value && data.value.code === 200) {
isAgentOrder.value = data.value.data.is_agent_order
}
} catch (err) {
console.error('检查订单代理状态失败:', err)
}
}
// 检查所有模块的下架状态(含分享页,当是代理推广的订单时)
if (isAgentOrder.value && idCard.value && !isExample.value) {
checkAllFeaturesStatus()
}
});
// 处理窗口大小变化(带防抖)
@@ -352,6 +703,7 @@ const featureMap = {
name: "谛听多维报告",
component: defineAsyncComponent(() => import("@/ui/CDWBG8B4D/index.vue")),
},
// 谛听多维报告拆分模块
DWBG8B4D_Overview: {
name: "报告概览",
@@ -373,10 +725,10 @@ const featureMap = {
name: "逾期风险综述",
component: defineAsyncComponent(() => import("@/ui/CDWBG8B4D/components/OverdueRiskSection.vue")),
},
// DWBG8B4D_CourtInfo: {
// name: "法院曝光台信息",
// component: defineAsyncComponent(() => import("@/ui/CDWBG8B4D/components/MultCourtInfoSection.vue")),
// },
DWBG8B4D_CourtInfo: {
name: "法院曝光台信息",
component: defineAsyncComponent(() => import("@/ui/CDWBG8B4D/components/MultCourtInfoSection.vue")),
},
DWBG8B4D_LoanEvaluation: {
name: "借贷评估",
component: defineAsyncComponent(() => import("@/ui/CDWBG8B4D/components/LoanEvaluationSection.vue")),
@@ -396,7 +748,7 @@ const featureMap = {
JRZQ4B6C: {
name: "信贷表现",
component: defineAsyncComponent(() => import("@/ui/JRZQ4B6C/index.vue")),
remark: '信贷表现主要为企业在背景调查过程中探查用户近期信贷表现时提供参考,帮助企业对其内部员工、外部业务进行个人信用过滤。数据来源于多个征信机构,可能存在数据延迟或不完整的情况。'
remark: '信贷表现主要为企业在背景调查过程中探查用户近期信贷表现时提供参考,帮助企业对其内部员工、外部业务进行个人风险过滤。数据来源于多个征信机构,可能存在数据延迟或不完整的情况。'
},
JRZQ09J8: {
name: "收入评估",
@@ -407,7 +759,7 @@ const featureMap = {
DWBG6A2C: {
name: "司南报告",
component: defineAsyncComponent(() => import("@/ui/DWBG6A2C/index.vue")),
remark: '司南报告提供全面的个人信用风险评估,包括身份核验、风险名单、借贷行为、履约情况等多维度分析。'
remark: '司南报告提供全面的个人风险风险评估,包括身份核验、风险名单、借贷行为、履约情况等多维度分析。'
},
// 司南报告拆分模块
// DWBG6A2C_BaseInfo: {
@@ -462,10 +814,10 @@ const featureMap = {
name: "关联风险监督",
component: defineAsyncComponent(() => import("@/ui/DWBG6A2C/components/RiskSupervisionSection.vue")),
},
// DWBG6A2C_CourtRiskInfo: {
// name: "法院风险信息",
// component: defineAsyncComponent(() => import("@/ui/DWBG6A2C/components/CourtRiskInfoSection.vue")),
// },
DWBG6A2C_CourtRiskInfo: {
name: "法院风险信息",
component: defineAsyncComponent(() => import("@/ui/DWBG6A2C/components/CourtRiskInfoSection.vue")),
},
// 贷款风险报告
JRZQ5E9F: {
name: "贷款风险评估",
@@ -549,13 +901,26 @@ const featureMap = {
component: defineAsyncComponent(() => import("@/ui/YYSY7D3E/index.vue")),
remark: '手机携号转网查询用于检测用户手机号码是否发生过携号转网操作,以及转网前后的运营商信息。携号转网可能影响用户身份验证和信用评估。'
},
// 借贷意向A
JRZQ6F2A: {
name: "借贷申请",
component: defineAsyncComponent(() => import("@/ui/JRZQ6F2A/index.vue")),
remark: '【非银行类贷款】:主要有持牌网络小贷、持牌小贷、持牌消费金融、持牌融资租赁、持牌汽车金融、其他。\n【持牌网络小贷】小额贷款公司拥有相关牌照和许可证可以通过互联网为用户提供贷款服务。贷款服务包括抵押贷款、消费贷款等。\n【持牌小贷机构】是指经国家金融管理部门批准设立并颁发许可证具备合法资质可依法在注册地所在省市开展小额贷款业务的非银行类金融机构。\n【持牌消费金融】是指经银监会批准在中华人民共和国境内设立的不吸收公众存款以小额、分散为原则为中国境内居民个人提供以消费为目的的贷款的非银行金融机构。\n【持牌融资租赁】是指持有金融牌照和许可证进行的融资租赁业务的融资租赁机构。\n【持牌汽车金融机构】是经过国家金融监督管理总局批准设立的、专门提供汽车金融服务的非银行金融机构。主要职责是为中国境内的汽车购买者及销售者提供金融服务包括但不限于购车贷款、经销商贷款、汽车融资租赁等。\n【其他】指除以上分类的其他非银行类贷款机构。'
},
// 手机在网时长
YYSY8B1C: {
name: "手机在网时长",
component: defineAsyncComponent(() => import("@/ui/YYSY8B1C/index.vue")),
remark: '手机在网时长查询用于检测用户手机号码的在网使用时长。在网时长越长,通常表示用户身份越稳定,信用风险越低。需要注意的是,如果手机号码存在携号转网的情况,那么在网时长会从转网的时候重新计算,转网前的在网时长不计入当前在网时长。建议结合手机携号转网查询结果进行综合评估。'
},
// 支付行为指数
JRZQ3C9R: {
name: "支付表现",
component: defineAsyncComponent(() => import("@/ui/JRZQ3C9R/index.vue")),
remark: '支付表现是指数基于近两年的查验记录、还款成功与失败表现以及余额不足情况,对用户支付与还款习惯进行量化评分,用于评估其支付稳定性与违约风险。'
},
};
const maskValue = computed(() => {
@@ -627,86 +992,89 @@ const maskValue = computed(() => {
// ==================== 新评分系统 ====================
// Feature 风险等级配置(权重越高表示风险越大,最终分数越高越安全)
const featureRiskLevels = {
// 🔴 高风险类 - 权重 10
'FLXG0V4B': 20, // 司法涉诉
'FLXG7E8F': 20, // 个人涉诉
'FLXG3D56': 10, // 违约失信
'FLXGDEA9': 18, // 本人不良
'JRZQ4AA8': 10, // 还款压力
// 🔴 高风险类 - 权重 60有高风险直接扣约70-80分
'FLXG0V4B': 50, // 司法涉诉
'FLXG3D56': 50, // 违约失信
'JRZQ8A2D': 50, // 特殊名单验证
'FLXG7E8F': 55, // 个人涉诉
'FLXGDEA9': 55, // 本人不良
// 🟠 中高风险类 - 权重 7
'JRZQ0A03': 7, // 借贷申请记录
'JRZQ8203': 7, // 借贷行为记录
'JRZQ4B6C': 7, // 信贷表现
'BehaviorRiskScan': 7, // 风险行为扫描
'IVYZ8I9J': 7, // 网络社交异常
'JRZQ8A2D': 9, // 特殊名单验证
'JRZQ7F1A': 8, // 全景雷达
'JRZQ7F1A_ApplyReport': 3,
'JRZQ7F1A_BehaviorReport': 3,
'JRZQ7F1A_BigDataReport': 2,
'YYSY7D3E': 5, // 手机携号转网
'YYSY8B1C': 5, // 手机在网时长
'DWBG7F3A': 8, // 多头借贷
// 🟡 中高风险类 - 权重 20-30
'JRZQ4AA8': 30, // 还款压力
'JRZQ0A03': 20, // 借贷申请记录
'JRZQ8203': 35, // 借贷行为记录
'JRZQ4B6C': 35, // 信贷表现
'JRZQ6F2A': 20, // 借贷申请
'BehaviorRiskScan': 60, // 风险行为扫描
'IVYZ8I9J': 45, // 网络社交异常
'JRZQ7F1A': 40, // 全景雷达
'JRZQ7F1A_ApplyReport': 20,
'JRZQ7F1A_BehaviorReport': 20,
'JRZQ7F1A_BigDataReport': 15,
'DWBG7F3A': 45, // 多头借贷
'YYSY7D3E': 15, // 手机携号转网
'YYSY8B1C': 15, // 手机在网时长
// 🟢 中风险类 - 权重 8-12
'QYGL3F8E': 10, // 人企关系加强版
'QCXG7A2B': 10, // 名下车辆
'JRZQ09J8': 10, // 收入评估
'JRZQ3C9R': 10, // 支付行为指数
// 🟡 中风险类 - 权重 5
'QYGL3F8E': 5, // 人企关系加强版
'QCXG7A2B': 5, // 名下车辆
'JRZQ09J8': 5, // 收入评估
// 🔵 低风险类 - 权重 3
'IVYZ5733': 3, // 婚姻状态
'IVYZ9A2B': 3, // 学历信息
'IVYZ3P9M': 3, // 学历信息查询(实时版)
// 🔵 低风险类 - 权重 3-5
'IVYZ5733': 4, // 婚姻状态
'IVYZ9A2B': 4, // 学历信息
'IVYZ3P9M': 4, // 学历信息查询(实时版)
// 📊 复合报告类 - 按子模块动态计算
'DWBG8B4D': 0, // 谛听多维报告(由子模块计算)
'DWBG6A2C': 0, // 司南报告(由子模块计算)
'JRZQ5E9F': 0, // 贷款风险评估(由子模块计算)
// 谛听多维报告子模块
'DWBG8B4D_Overview': 10,
'DWBG8B4D_ElementVerification': 4,
'DWBG8B4D_Identity': 4,
'DWBG8B4D_RiskWarning': 10,
'DWBG8B4D_OverdueRisk': 9,
'DWBG8B4D_LoanEvaluation': 7,
'DWBG8B4D_LeasingRisk': 6,
'DWBG8B4D_RiskSupervision': 8,
'DWBG8B4D_RiskWarningTab': 9,
'DWBG8B4D_Overview': 30,
'DWBG8B4D_ElementVerification': 10,
'DWBG8B4D_Identity': 10,
'DWBG8B4D_RiskWarning': 35,
'DWBG8B4D_OverdueRisk': 30,
'DWBG8B4D_LoanEvaluation': 40,
'DWBG8B4D_LeasingRisk': 18,
'DWBG8B4D_RiskSupervision': 25,
'DWBG8B4D_RiskWarningTab': 30,
'DWBG8B4D_CourtInfo':33,
// 司南报告子模块
'DWBG6A2C_StandLiveInfo': 4,
'DWBG6A2C_RiskPoint': 9,
'DWBG6A2C_SecurityInfo': 15,
'DWBG6A2C_AntiFraudInfo': 15,
'DWBG6A2C_RiskList': 12,
'DWBG6A2C_ApplicationStatistics': 7,
'DWBG6A2C_LendingStatistics': 6,
'DWBG6A2C_PerformanceStatistics': 7,
'DWBG6A2C_OverdueRecord': 9,
'DWBG6A2C_CreditDetail': 5,
'DWBG6A2C_RentalBehavior': 5,
'DWBG6A2C_RiskSupervision': 8,
'DWBG6A2C_StandLiveInfo': 10,
'DWBG6A2C_RiskPoint': 28,
'DWBG6A2C_SecurityInfo': 45,
'DWBG6A2C_AntiFraudInfo': 45,
'DWBG6A2C_RiskList': 38,
'DWBG6A2C_ApplicationStatistics': 40,
'DWBG6A2C_LendingStatistics': 35,
'DWBG6A2C_PerformanceStatistics': 22,
'DWBG6A2C_OverdueRecord': 28,
'DWBG6A2C_CreditDetail': 15,
'DWBG6A2C_RentalBehavior': 15,
'DWBG6A2C_RiskSupervision': 25,
'DWBG6A2C_CourtRiskInfo':39,
// 贷款风险评估子模块
'CJRZQ5E9F_RiskOverview': 8,
'CJRZQ5E9F_CreditScores': 7,
'CJRZQ5E9F_LoanBehaviorAnalysis': 7,
'CJRZQ5E9F_InstitutionAnalysis': 5,
'CJRZQ5E9F_TimeTrendAnalysis': 6,
'CJRZQ5E9F_RiskIndicators': 8,
'CJRZQ5E9F_RiskAdvice': 2,
'CJRZQ5E9F_RiskOverview': 25,
'CJRZQ5E9F_CreditScores': 22,
'CJRZQ5E9F_LoanBehaviorAnalysis': 40,
'CJRZQ5E9F_InstitutionAnalysis': 15,
'CJRZQ5E9F_TimeTrendAnalysis': 18,
'CJRZQ5E9F_RiskIndicators': 25,
'CJRZQ5E9F_RiskAdvice': 6,
// 人企关系加强版子模块
'CQYGL3F8E_Investment': 4,
'CQYGL3F8E_SeniorExecutive': 4,
'CQYGL3F8E_Lawsuit': 8,
'CQYGL3F8E_InvestHistory': 3,
'CQYGL3F8E_FinancingHistory': 3,
'CQYGL3F8E_Punishment': 7,
'CQYGL3F8E_Abnormal': 6,
'CQYGL3F8E_TaxRisk': 7,
'CQYGL3F8E_Investment': 12,
'CQYGL3F8E_SeniorExecutive': 12,
'CQYGL3F8E_Lawsuit': 25,
'CQYGL3F8E_InvestHistory': 8,
'CQYGL3F8E_FinancingHistory': 8,
'CQYGL3F8E_Punishment': 22,
'CQYGL3F8E_Abnormal': 18,
'CQYGL3F8E_TaxRisk': 22,
};
// 存储每个组件的 ref 引用
@@ -726,6 +1094,19 @@ defineExpose({
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 = () => {
// 收集实际存在的 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;
@@ -763,11 +1149,7 @@ const calculateScore = () => {
const componentRisk = 100 - componentScore;
// 计算该模块的风险贡献(固定分值,不按占比)
// 使用权重系数放大高风险模块的影响
// 高风险模块权重10如果风险分数是0扣20分权重10 × 系数2
// 中风险模块权重7如果风险分数是0扣14分权重7 × 系数2
// 低风险模块权重3如果风险分数是0扣6分权重3 × 系数2
const weightMultiplier = 1.5; // 权重系数,可以调整这个值来控制影响程度
const weightMultiplier = 1.2;
const riskContribution = (componentRisk / 100) * weight * weightMultiplier;
riskDetails.push({
@@ -780,22 +1162,44 @@ const calculateScore = () => {
hasStatus: key in componentRiskScores.value
});
// 累加风险分数
totalRiskScore += riskContribution;
});
// 总风险分数限制在 0-90 范围内确保最低分为10分
const finalRiskScore = Math.max(0, Math.min(90, Math.round(totalRiskScore)));
// 转换为安全分数分数越高越安全100 - 风险分数)
// 最终分数范围10-100分
// 总风险分数限制在 0-86得到安全分数 14-100
const finalRiskScore = Math.max(0, Math.min(86, Math.round(totalRiskScore)));
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();
// 将评分系统数据整理到一个对象中
@@ -852,7 +1256,12 @@ watch([reportData, componentRiskScores], () => {
</van-tab>
<van-tab v-for="(item, index) in processedReportData" :key="`${item.data.apiID}_${index}`"
:title="featureMap[item.data.apiID]?.name">
<TitleBanner :id="item.data.apiID" class="mb-4">
<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 }}
</TitleBanner>
<component :is="featureMap[item.data.apiID]?.component" :ref="el => {
@@ -879,7 +1288,7 @@ watch([reportData, componentRiskScores], () => {
1、本份报告是在取得您个人授权后我们才向合法存有您以上个人信息的机构去调取相关内容我们不会以任何形式对您的报告进行存储除您和您授权的人外不会提供给任何人和机构进行查看。
</p>
<p class="text-[#999999]">
&nbsp; &nbsp; 2本报告自生成之日起有效期 30
&nbsp; &nbsp; 2、本报告自生成之日起有效期 {{ useAppStore().queryRetentionDays || 30 }}
天,过期自动删除。如果您对本份报告存有异议,可能是合作机构数据有延迟或未能获取到您的相关数据,出于合作平台数据隐私的保护,本平台将不做任何解释。
</p>
<p class="text-[#999999]">
@@ -894,19 +1303,57 @@ watch([reportData, componentRiskScores], () => {
</div>
<div class="disclaimer">
<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="公安备案" />
<text>琼公网安备46010002000584号</text>
</div>
</div> -->
<div>
<a class="text-blue-500" href="https://beian.miit.gov.cn">
琼ICP备2024048057-2
琼ICP备2024038584-10
</a>
</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>
<style lang="scss" scoped>

View File

@@ -1,33 +1,74 @@
<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 { 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 route = useRoute();
const dialogStore = useDialogStore();
const agentStore = useAgentStore();
const userStore = useUserStore();
const { runWithCaptcha } = useAliyunCaptcha();
const phoneNumber = ref("");
const verificationCode = ref("");
const inviteCode = ref("");
const isCountingDown = ref(false);
const countdown = ref(60);
const isAgreed = ref(false);
const hasAccount = ref(false); // 是否有平台账号
let timer = null;
// 聚焦状态变量
const phoneFocused = 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(() => {
return /^1[3-9]\d{9}$/.test(phoneNumber.value);
});
const canBind = computed(() => {
const isInviteCodeValid = computed(() => {
return inviteCode.value.trim().length > 0;
});
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() {
@@ -36,25 +77,30 @@ async function sendVerificationCode() {
showToast({ message: "请输入有效的手机号" });
return;
}
const { data, error } = await useApiFetch("auth/sendSms")
.post({ mobile: phoneNumber.value, actionType: "bindMobile" })
.json();
if (data.value && !error.value) {
if (data.value.code === 200) {
if (!hasAccount.value && !isInviteCodeValid.value) {
showToast({ message: "请先输入邀请码" });
return;
}
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('verificationCode');
const verificationCodeInput = document.getElementById('registerVerificationCode');
if (verificationCodeInput) {
verificationCodeInput.focus();
}
});
} else {
showToast(data.value.msg);
} else if (result) {
showToast(result.msg || "发送失败");
}
}
);
}
function startCountdown() {
@@ -70,7 +116,7 @@ function startCountdown() {
}, 1000);
}
async function handleBind() {
async function handleRegister() {
if (!isPhoneNumberValid.value) {
showToast({ message: "请输入有效的手机号" });
return;
@@ -79,48 +125,117 @@ async function handleBind() {
showToast({ message: "请输入有效的验证码" });
return;
}
if (!hasAccount.value && !isInviteCodeValid.value) {
showToast({ message: "请输入邀请码" });
return;
}
if (!isAgreed.value) {
showToast({ message: "请先同意用户协议" });
return;
}
try {
if (hasAccount.value) {
// 已有账号模式:绑定手机号登录
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: "绑定成功" });
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');
// 发出注册成功的事件
emit('register-success');
// 延迟执行路由检查,确保状态已更新
// 检查是否是代理,如果是代理跳转到代理主页,否则跳转到首页
setTimeout(() => {
// 重新触发路由检查
const currentRoute = router.currentRoute.value;
router.replace(currentRoute.path);
}, 100);
if (agentStore.isAgent) {
router.replace("/agent");
} else {
showToast(data.value.msg);
router.replace("/");
}
}, 300);
} else {
// 检查是否是手机号已绑定其他微信的错误
if (data.value.msg && data.value.msg.includes("已绑定其他微信号")) {
showToast({ message: "该手机号已绑定其他微信号,一个微信只能绑定一个手机号" });
} else {
showToast(data.value.msg || "绑定失败,请重试");
}
}
}
} else {
// 新注册模式:通过邀请码注册成为代理
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() {
dialogStore.closeBindPhone();
dialogStore.closeRegisterAgent();
// 重置表单
phoneNumber.value = "";
verificationCode.value = "";
inviteCode.value = "";
isAgreed.value = false;
hasAccount.value = false;
if (timer) {
clearInterval(timer);
}
@@ -138,17 +253,14 @@ function toPrivacyPolicy() {
</script>
<template>
<div v-if="dialogStore.showBindPhone">
<van-popup v-model:show="dialogStore.showBindPhone" round position="bottom" :style="{ height: '80%' }"
<div v-if="dialogStore.showRegisterAgent">
<van-popup v-model:show="dialogStore.showRegisterAgent" round position="bottom" :style="{ height: '85%' }"
@close="closeDialog">
<div class="bind-phone-dialog">
<div class="register-agent-dialog">
<div class="title-bar">
<div class="font-bold">绑定手机号码</div>
<div class="font-bold">注册成为代理</div>
<div class="text-sm text-gray-500 mt-1">
为使用完整功能请绑定手机号码
</div>
<div class="text-sm text-gray-500 mt-1">
如该微信号之前已绑定过手机号请输入已绑定的手机号
{{ hasAccount ? '绑定手机号登录已有账号' : '请输入手机号和邀请码完成代理注册' }}
</div>
<van-icon name="cross" class="close-icon" @click="closeDialog" />
</div>
@@ -163,6 +275,48 @@ function toPrivacyPolicy() {
</div>
<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="[
'input-container bg-blue-300/20',
@@ -178,13 +332,13 @@ function toPrivacyPolicy() {
'input-container bg-blue-300/20',
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"
@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
:class="isCountingDown || !isPhoneNumberValid || (!hasAccount && !isInviteCodeValid)
? 'cursor-not-allowed bg-gray-300 text-gray-500'
: 'bg-blue-500 text-white hover:bg-blue-600'
" @click="sendVerificationCode">
@@ -200,7 +354,7 @@ function toPrivacyPolicy() {
<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>
@@ -214,8 +368,8 @@ function toPrivacyPolicy() {
<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">
确认绑定
:class="{ 'opacity-50 cursor-not-allowed': !canRegister }" @click="handleRegister">
{{ hasAccount ? '绑定手机号登录' : '注册成为代理' }}
</button>
</div>
</div>
@@ -224,7 +378,7 @@ function toPrivacyPolicy() {
</template>
<style scoped>
.bind-phone-dialog {
.register-agent-dialog {
background: url("@/assets/images/login_bg.png") no-repeat;
background-position: center;
background-size: cover;

View 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>

View File

@@ -47,8 +47,8 @@ const canvasWidth = 300
const canvasHeight = 180
const bgImgUrl = '/image/clickCaptcha.jpg' // 可替换为任意背景图
const allChars = ['大', '数', '据', '全', '能', '查', '风', '险', '报', '告']
const targetChars = ref(['', '', '查']) // 目标点击顺序固定
const allChars = ['大', '数', '据', '', '查', '风', '险', '报', '告']
const targetChars = ref(['', '', '查']) // 目标点击顺序固定
const charPositions = ref([]) // [{char, x, y, w, h}]
const clickedIndex = ref(0)
const errorMessage = ref('')
@@ -134,7 +134,7 @@ function generateCaptcha() {
;[chars[i], chars[j]] = [chars[j], chars[i]]
}
currentChars = chars
targetChars.value = ['', '', '查']
targetChars.value = ['', '', '查']
clickedIndex.value = 0
errorMessage.value = ''
successMessage.value = ''
@@ -239,9 +239,9 @@ watch(
padding: 1rem;
backdrop-filter: blur(4px);
animation: fadeIn 0.2s ease-out;
}
}
@keyframes fadeIn {
@keyframes fadeIn {
from {
opacity: 0;
}
@@ -249,9 +249,9 @@ watch(
to {
opacity: 1;
}
}
}
.captcha-modal {
.captcha-modal {
background: #fff;
border-radius: 1rem;
width: 100%;
@@ -259,9 +259,9 @@ watch(
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 {
@keyframes slideUp {
from {
transform: translateY(20px);
opacity: 0;
@@ -271,18 +271,18 @@ watch(
transform: translateY(0);
opacity: 1;
}
}
}
.captcha-header {
.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 {
.captcha-title {
font-size: 1.125rem;
font-weight: 600;
color: var(--color-text-primary, #323233);
@@ -291,9 +291,9 @@ watch(
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
}
.close-btn {
.close-btn {
background: rgba(0, 0, 0, 0.05);
border: none;
font-size: 1.5rem;
@@ -308,20 +308,20 @@ watch(
justify-content: center;
transition: all 0.2s ease;
line-height: 1;
}
}
.close-btn:hover {
.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;
background: #ffffff;
}
}
.captcha-canvas {
.captcha-canvas {
width: 100%;
border-radius: 0.75rem;
background: var(--color-bg-tertiary, #f8f8f8);
@@ -330,26 +330,26 @@ watch(
border: 2px solid var(--color-border-primary, #ebedf0);
transition: all 0.2s ease;
cursor: pointer;
}
}
.captcha-canvas:hover {
.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 {
.captcha-instruction {
margin: 1.25rem 0 0.75rem 0;
text-align: center;
}
}
.captcha-instruction p {
.captcha-instruction p {
font-size: 0.95rem;
color: var(--color-text-secondary, #646566);
margin: 0;
line-height: 1.5;
}
}
.target-list {
.target-list {
color: var(--color-primary, #8CC6F7);
font-weight: 600;
font-size: 1.05rem;
@@ -357,15 +357,15 @@ watch(
background: var(--color-primary-light, rgba(140, 198, 247, 0.1));
border-radius: 0.375rem;
display: inline-block;
}
}
.captcha-status {
.captcha-status {
text-align: center;
min-height: 1.75rem;
margin-top: 0.5rem;
}
}
.error-message {
.error-message {
color: var(--color-danger, #ee0a24);
font-size: 0.875rem;
margin: 0;
@@ -373,9 +373,9 @@ watch(
background: rgba(238, 10, 36, 0.1);
border-radius: 0.5rem;
animation: shake 0.3s ease-in-out;
}
}
@keyframes shake {
@keyframes shake {
0%,
100% {
@@ -389,9 +389,9 @@ watch(
75% {
transform: translateX(5px);
}
}
}
.success-message {
.success-message {
color: var(--color-success, #07c160);
font-size: 0.875rem;
margin: 0;
@@ -399,9 +399,9 @@ watch(
background: rgba(7, 193, 96, 0.1);
border-radius: 0.5rem;
animation: bounce 0.4s ease-out;
}
}
@keyframes bounce {
@keyframes bounce {
0%,
100% {
@@ -411,24 +411,24 @@ watch(
50% {
transform: translateY(-3px);
}
}
}
.status-text {
.status-text {
color: var(--color-text-tertiary, #969799);
font-size: 0.875rem;
margin: 0;
}
}
.captcha-footer {
.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 {
.refresh-btn {
width: 100%;
padding: 0.875rem;
background: var(--color-primary, #8CC6F7);
@@ -440,25 +440,25 @@ watch(
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(140, 198, 247, 0.3);
}
}
.refresh-btn:hover:not(:disabled) {
.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) {
.refresh-btn:active:not(:disabled) {
transform: translateY(0);
}
}
.refresh-btn:disabled {
.refresh-btn:disabled {
background: var(--color-text-disabled, #c8c9cc);
cursor: not-allowed;
box-shadow: none;
}
}
.confirm-btn {
.confirm-btn {
width: 100%;
padding: 0.875rem;
background: var(--color-success, #07c160);
@@ -470,25 +470,25 @@ watch(
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(7, 193, 96, 0.3);
}
}
.confirm-btn:hover:not(:disabled) {
.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) {
.confirm-btn:active:not(:disabled) {
transform: translateY(0);
}
}
.confirm-btn:disabled {
.confirm-btn:disabled {
background: var(--color-text-disabled, #c8c9cc);
cursor: not-allowed;
box-shadow: none;
}
}
@media (max-width: 480px) {
@media (max-width: 480px) {
.captcha-overlay {
padding: 0.75rem;
}

View File

@@ -4,9 +4,9 @@
分析指数是根据网络行为大数据出具的分析评估参考分数分数越高越好该指数仅对本报告有效不代表对报告查询人的综合定性评价
</div>
<div ref="chartRef" :style="{ width: '100%', height: '200px' }"></div>
<div class="risk-description">
<!-- <div class="risk-description">
{{ riskDescription }}
</div>
</div> -->
<div class="risk-legend mt-6">
<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 }">
@@ -29,10 +29,10 @@ const props = defineProps({
},
});
// 根据分数计算风险等级和颜色(分数越高越安全)
// 根据分数计算风险等级和颜色(分数范围 10-9020-80 基础分 ±10分数越高越安全)
const riskLevel = computed(() => {
const score = props.score;
if (score >= 75 && score <= 100) {
if (score >= 70 && score <= 90) {
return {
level: "无任何风险",
color: "#52c41a",
@@ -41,7 +41,7 @@ const riskLevel = computed(() => {
{ offset: 1, color: "#7fdb42" }
]
};
} else if (score >= 50 && score < 75) {
} else if (score >= 50 && score < 70) {
return {
level: "风险指数较低",
color: "#faad14",
@@ -50,7 +50,7 @@ const riskLevel = computed(() => {
{ offset: 1, color: "#ffc53d" }
]
};
} else if (score >= 25 && score < 50) {
} else if (score >= 30 && score < 50) {
return {
level: "风险指数较高",
color: "#fa8c16",
@@ -71,14 +71,14 @@ const riskLevel = computed(() => {
}
});
// 评分解释文本(分数越高越安全)
// 评分解释文本(分数范围 10-90分数越高越安全)
const riskDescription = computed(() => {
const score = props.score;
if (score >= 75 && score <= 100) {
if (score >= 70 && score <= 90) {
return "根据综合分析,当前报告未检测到明显风险因素,各项指标表现正常,总体状况良好。";
} else if (score >= 50 && score < 75) {
} else if (score >= 50 && score < 70) {
return "根据综合分析,当前报告存在少量风险信号,建议关注相关指标变化,保持警惕。";
} else if (score >= 25 && score < 50) {
} else if (score >= 30 && score < 50) {
return "根据综合分析,当前报告风险指数较高,多项指标显示异常,建议进一步核实相关情况。";
} else {
return "根据综合分析,当前报告显示高度风险状态,多项重要指标严重异常,请立即采取相应措施。";
@@ -89,10 +89,10 @@ const chartRef = ref(null);
let chartInstance = null;
const legendItems = [
{ level: "高风险", color: "#f5222d", range: "0-24", textColor: "#ffffff" },
{ level: "一般", color: "#fa8c16", range: "25-49", textColor: "#ffffff" },
{ level: "良好", color: "#faad14", range: "50-74", textColor: "#ffffff" },
{ level: "优秀", color: "#52c41a", range: "75-100", textColor: "#ffffff" }
{ level: "高风险", color: "#f5222d", range: "0-29", textColor: "#ffffff" },
{ level: "一般", color: "#fa8c16", range: "30-49", textColor: "#ffffff" },
{ level: "良好", color: "#faad14", range: "50-69", textColor: "#ffffff" },
{ level: "优秀", color: "#52c41a", range: "70-100", textColor: "#ffffff" }
];
const initChart = () => {
@@ -116,8 +116,8 @@ const updateChart = () => {
type: "gauge",
startAngle: 180,
endAngle: 0,
min: 0,
max: 100,
min: 10,
max: 90,
radius: "100%",
center: ["50%", "80%"],
itemStyle: {
@@ -153,7 +153,7 @@ const updateChart = () => {
show: true,
distance: -30,
length: 6,
splitNumber: 10, // 每1分一个小刻度
splitNumber: 8, // 刻度数量10-90 共 80 分)
lineStyle: {
color: risk.color,
width: 1,
@@ -164,7 +164,7 @@ const updateChart = () => {
show: true,
distance: -36,
length: 12,
splitNumber: 9, // 9个大刻度100分分成9个区间
splitNumber: 8, // 8个大刻度10-90 分成 8 个区间
lineStyle: {
color: risk.color,
width: 2,
@@ -195,7 +195,7 @@ const updateChart = () => {
color: risk.color,
offsetCenter: [0, "-25%"],
formatter: function (value) {
return `{value|${value}分}\n{level|${risk.level}}`;
return `{value|${value}分}`;
},
rich: {
value: {
@@ -269,6 +269,7 @@ onUnmounted(() => {
line-height: 1.5;
text-align: center;
}
.risk-legend {
display: flex;
justify-content: center;

View File

@@ -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"
@click="handleSubmit">
<span>{{ buttonText }}</span>
<span class="ml-4">¥{{ featureData.sell_price }}</span>
<span v-if="!isWeChat" class="ml-4">¥{{ featureData.sell_price }}</span>
</button>
<!-- <div class="text-gray-500 leading-relaxed mt-8" v-html="featureData.description">
</div> -->
<!-- 免责声明 -->
<div class="text-xs text-center text-gray-500 leading-relaxed mt-2">
为保证用户的隐私及数据安全查询结果生成30天后将自动删除
为保证用户的隐私及数据安全查询结果生成{{ appStore.queryRetentionDays || 30 }}天后将自动删除
</div>
</div>
<!-- 报告包含内容 -->
<div class="card mt-3" v-if="featureData.features && featureData.features.length > 0">
<ReportFeatures :features="featureData.features" :title-style="{ color: 'var(--van-text-color)' }" />
<div class="card mt-3" v-if="showReportFeatures">
<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="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);">
@@ -95,7 +97,7 @@
<div class="mb-4 text-xl font-bold" style="color: var(--van-text-color);">
{{ featureData.product_name }}
</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>
<div class="text-2xl font-semibold text-danger">
@@ -108,14 +110,14 @@
v-html="featureData.description">
</div>
<div class="mb-2 text-xs italic text-danger">
为保证用户的隐私以及数据安全查询的结果生成30天之后将自动清除
为保证用户的隐私以及数据安全查询的结果生成{{ appStore.queryRetentionDays || 30 }}天之后将自动清除
</div>
</div>
</div>
<!-- 支付组件 -->
<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"
@@ -126,18 +128,24 @@
</template>
<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 { useRoute, useRouter } from "vue-router";
import { useUserStore } from "@/stores/userStore";
import { useDialogStore } from "@/stores/dialogStore";
import { useEnv } from "@/composables/useEnv";
import { showConfirmDialog } from "vant";
import { useAliyunCaptcha } from "@/composables/useAliyunCaptcha";
import Payment from "@/components/Payment.vue";
import BindPhoneDialog from "@/components/BindPhoneDialog.vue";
import BindPhoneOnlyDialog from "@/components/BindPhoneOnlyDialog.vue";
import SectionTitle from "@/components/SectionTitle.vue";
import ReportFeatures from "@/components/ReportFeatures.vue";
import { useAppStore } from "@/stores/appStore";
import {
isRiskAssessmentProduct,
resolveInquireProductEn,
} from "@/constants/riskAssessmentReportFeatures";
// Props
const props = defineProps({
@@ -174,7 +182,7 @@ const loadProductBackground = async (productType) => {
return (await import("@/assets/images/report/xwqy_inquire_bg.png")).default;
case 'preloanbackgroundcheck':
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;
case 'marriage':
return (await import("@/assets/images/report/marriage_inquire_bg.png")).default;
@@ -198,6 +206,8 @@ const router = useRouter();
const dialogStore = useDialogStore();
const userStore = useUserStore();
const { isWeChat } = useEnv();
const appStore = useAppStore();
const { runWithCaptcha } = useAliyunCaptcha();
// 响应式数据
const showPayment = ref(false);
@@ -211,6 +221,17 @@ const countdown = ref(60);
// 使用传入的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({
name: "",
@@ -251,6 +272,9 @@ const backgroundStyle = computed(() => {
// 动态加载牌匾背景图片
const loadTrapezoidBackground = async () => {
if (!props.feature) {
return;
}
try {
let bgModule;
if (props.feature === 'marriage') {
@@ -318,8 +342,7 @@ function handleBindSuccess() {
// 处理输入框点击事件
const handleInputClick = async () => {
if (!isLoggedIn.value) {
// 非微信浏览器环境:未登录用户提示跳转到登录页
if (!isWeChat.value) {
if (!isWeChat.value && props.type !== 'promotion') {
try {
await showConfirmDialog({
title: '提示',
@@ -333,16 +356,14 @@ const handleInputClick = async () => {
}
}
} else {
// 微信浏览器环境:已登录但检查是否需要绑定手机号
if (isWeChat.value && !userStore.mobile) {
if (isWeChat.value && !userStore.mobile && props.type !== 'promotion') {
dialogStore.openBindPhone();
}
}
};
function handleSubmit() {
// 非微信浏览器环境:检查登录状态
if (!isWeChat.value && !isLoggedIn.value) {
if (!isWeChat.value && !isLoggedIn.value && props.type !== 'promotion') {
router.push('/login');
return;
}
@@ -378,7 +399,7 @@ function handleSubmit() {
}
// 检查是否需要绑定手机号
if (!userStore.mobile) {
if (!userStore.mobile && props.type !== 'promotion') {
pendingPayment.value = true;
dialogStore.openBindPhone();
} else {
@@ -421,6 +442,9 @@ async function submitRequest() {
localStorage.setItem("token", data.value.data.accessToken);
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);
}
showPayment.value = true;
@@ -435,11 +459,14 @@ async function sendVerificationCode() {
return;
}
const { data, error } = await useApiFetch("/auth/sendSms")
.post({ mobile: formData.mobile, actionType: "query" })
.json();
if (!error.value && data.value.code === 200) {
// 使用阿里云滑块验证码保护发送短信接口
runWithCaptcha(
(captchaVerifyParam) => useApiFetch("/auth/sendSms")
.post({ mobile: formData.mobile, actionType: "query", captchaVerifyParam })
.json(),
(result) => {
// result 已经是解包后的响应数据data.value
if (result && result.code === 200) {
showToast({ message: "验证码发送成功", type: "success" });
startCountdown();
nextTick(() => {
@@ -448,9 +475,11 @@ async function sendVerificationCode() {
verificationCodeInput.focus();
}
});
} else {
showToast({ message: "验证码发送失败,请重试" });
} else if (result) {
showToast({ message: result.msg || "验证码发送失败,请重试" });
}
}
);
}
let timer = null;
@@ -488,18 +517,29 @@ const toHistory = () => {
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 () => {
await loadBackgroundImage();
await loadTrapezoidBackground();
});
// 加载背景图片
const loadBackgroundImage = async () => {
const background = await loadProductBackground(props.feature);
productBackground.value = background || '';
};
onUnmounted(() => {
if (timer) {
clearInterval(timer);

View File

@@ -1,10 +1,15 @@
<template>
<van-popup
v-model:show="show"
position="bottom"
class="flex flex-col justify-between p-6"
:style="{ height: '50%' }"
>
<van-popup v-model:show="show" position="bottom" class="flex flex-col justify-between p-6"
:style="{ height: '50%' }">
<!-- Logo和应用名 -->
<div class="flex items-center justify-center mb-2">
<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">
<h3 class="text-lg font-bold">支付</h3>
</div>
@@ -12,11 +17,8 @@
<div class="font-bold text-xl">{{ data.product_name }}</div>
<div class="text-3xl text-red-500 font-bold">
<!-- 显示原价和折扣价格 -->
<div
v-if="discountPrice"
class="line-through text-gray-500 mt-4"
:class="{ 'text-2xl': discountPrice }"
>
<div v-if="discountPrice" class="line-through text-gray-500 mt-4"
:class="{ 'text-2xl': discountPrice }">
¥ {{ data.sell_price }}
</div>
<div>
@@ -36,63 +38,46 @@
<!-- 支付方式选择 -->
<div class="">
<van-cell-group inset>
<van-cell
v-if="isWeChat"
title="微信支付"
clickable
@click="selectedPaymentMethod = 'wechat'"
>
<!-- 开发环境测试支付选项 -->
<van-cell v-if="isDevMode" title="测试支付(开发环境)" clickable @click="selectedPaymentMethod = 'test'">
<template #icon>
<van-icon
size="24"
name="wechat-pay"
color="#1AAD19"
class="mr-2"
/>
<van-icon size="24" name="setting" color="#FF9800" class="mr-2" />
</template>
<template #right-icon>
<van-radio
v-model="selectedPaymentMethod"
name="wechat"
/>
<van-radio v-model="selectedPaymentMethod" name="test" />
</template>
</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>
</van-cell>
<!-- 支付宝支付 -->
<van-cell
v-else
title="支付宝支付"
clickable
@click="selectedPaymentMethod = 'alipay'"
>
<van-cell v-else title="支付宝支付" clickable @click="selectedPaymentMethod = 'alipay'">
<template #icon>
<van-icon
size="24"
name="alipay"
color="#00A1E9"
class="mr-2"
/>
<van-icon size="24" name="alipay" color="#00A1E9" class="mr-2" />
</template>
<template #right-icon>
<van-radio
v-model="selectedPaymentMethod"
name="alipay"
/>
<van-radio v-model="selectedPaymentMethod" name="alipay" />
</template>
</van-cell>
</van-cell-group>
</div>
<!-- 确认按钮 -->
<div class="">
<van-button class="w-full" round type="primary" @click="getPayment"
>确认支付</van-button
>
<van-button class="w-full" round type="primary" @click="getPayment">确认支付</van-button>
</div>
</van-popup>
</template>
<script setup>
import { ref, defineProps } from "vue";
import { ref, computed, onMounted, onUnmounted } from "vue";
import { useRouter } from "vue-router";
const { isWeChat } = useEnv();
const props = defineProps({
@@ -108,26 +93,70 @@ const props = defineProps({
type: String,
required: true,
},
returnUrl: {
type: String,
required: false,
default: "",
},
});
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(() => {
if (!isDevMode.value) {
// 非开发环境,根据平台选择支付方式
if (isWeChat.value) {
selectedPaymentMethod.value = "wechat";
} else {
selectedPaymentMethod.value = "alipay";
}
}
// 初始化日期时间并启动定时器
updateDateTime();
dateTimeTimer = setInterval(updateDateTime, 1000);
});
onUnmounted(() => {
// 清理定时器
if (dateTimeTimer) {
clearInterval(dateTimeTimer);
dateTimeTimer = null;
}
});
const orderNo = ref("");
const router = useRouter();
const discountPrice = ref(false); // 是否应用折扣
onMounted(() => {
if (isWeChat.value) {
selectedPaymentMethod.value = "wechat";
} else {
selectedPaymentMethod.value = "alipay";
}
});
// 当前日期时间
const currentDateTime = ref("");
let dateTimeTimer = null;
// 格式化日期时间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() {
const { data, error } = await useApiFetch("/pay/payment")
@@ -139,7 +168,18 @@ async function getPayment() {
.json();
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;
// 存储订单ID以便支付宝返回时获取
const prepayUrl = data.value.data.prepay_id;
@@ -157,9 +197,13 @@ async function getPayment() {
function (res) {
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({
path: "/payment/result",
query: { orderNo: data.value.data.order_no },
query: queryParams,
});
}
}

View File

@@ -16,15 +16,20 @@
</div>
<div class="flex items-center justify-between mt-2">
<div>推广收益为<span class="text-orange-500"> {{ promotionRevenue }} </span></div>
<div>我的成本为<span class="text-orange-500"> {{ 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 class="card m-4">
<div class="text-lg mb-2">收益与成本说明</div>
<div>推广收益 = 客户查询价 - 我的成本</div>
<div>我的成本 = 提价成本 + 价成本</div>
<div class="mt-1">提价成本超过平台标准定价部分平台会收取部分成本价</div>
<div>我的成本 = 实际底价 + 价成本</div>
<div class="mt-1">提价成本设置{{ productConfig.price_threshold }}元以上的部分收取{{
rateFormat(productConfig.price_fee_rate)
}}的提价成本</div>
<div class="">设定范围<span class="text-orange-500">{{
productConfig.price_range_min }}</span> - <span class="text-orange-500">{{
productConfig.price_range_max }}</span></div>
@@ -60,24 +65,43 @@ watch(show, () => {
const costPrice = computed(() => {
if (!productConfig.value) return 0.00
// 平台定价成本
let platformPricing = 0
platformPricing += productConfig.value.cost_price
if (price.value > productConfig.value.p_pricing_standard) {
platformPricing += (price.value - productConfig.value.p_pricing_standard) * productConfig.value.p_overpricing_ratio
// 新代理系统:成本价 = 实际底价actual_base_price+ 提价成本
const actualBasePrice = Number(productConfig.value.actual_base_price) || 0;
const priceNum = Number(price.value) || 0;
const priceThreshold = Number(productConfig.value.price_threshold) || 0;
const priceFeeRate = Number(productConfig.value.price_fee_rate) || 0;
// 计算提价成本
let priceCost = 0;
if (priceNum > priceThreshold) {
priceCost = (priceNum - priceThreshold) * priceFeeRate;
}
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) {
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
}
}
}
// 总成本 = 实际底价 + 提价成本
const totalCost = actualBasePrice + priceCost;
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(() => {

View File

@@ -1,74 +1,55 @@
<template>
<van-popup v-model:show="show" round position="bottom" :style="{ maxHeight: '95vh' }">
<div class="qrcode-popup-container">
<div class="qrcode-content">
<van-swipe class="poster-swiper rounded-lg sm:rounded-xl shadow" indicator-color="white"
@change="onSwipeChange">
<van-swipe-item v-for="(_, index) in posterImages" :key="index">
<!-- 网格布局显示4张海报 -->
<div class="poster-grid">
<div v-for="(_, index) in posterImages" :key="index"
:class="['poster-item', { 'poster-item-selected': currentIndex === index }]"
@click="selectPoster(index)">
<canvas :ref="(el) => (posterCanvasRefs[index] = el)"
class="poster-canvas rounded-lg sm:rounded-xl m-auto"></canvas>
</van-swipe-item>
</van-swipe>
class="poster-canvas rounded-lg sm:rounded-xl"></canvas>
<!-- 单张海报加载占位 -->
<div v-if="posterLoading[index]" class="poster-skeleton">
<van-loading size="20px" vertical>生成中</van-loading>
</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">
<span class="swipe-icon"></span> 左右滑动切换海报
<span class="swipe-icon"></span>
</div>
</div>
<!-- 整体加载状态提示 -->
<div v-if="isGeneratingPosters" class="poster-loading-overlay">
<van-loading size="24px" vertical>海报生成中...</van-loading>
</div>
</div>
<div class="fixed bottom-0 left-0 right-0 bg-white">
<van-divider class="my-2 sm:my-3">分享到好友</van-divider>
<div class="flex items-center justify-around pb-3 sm:pb-4 px-4">
<!-- 微信环境显示分享保存和复制按钮 -->
<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">
<img src="@/assets/images/icon_share_img.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 class="text-center mt-1 text-gray-600 text-xs">保存图片</div>
</div>
<div class="flex flex-col items-center justify-center cursor-pointer" @click="copyUrl">
<img src="@/assets/images/icon_share_url.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 class="text-center mt-1 text-gray-600 text-xs">复制链接</div>
</div>
</template>
<!-- 非微信环境显示保存和复制按钮 -->
<template v-else>
<div class="flex flex-col items-center justify-center cursor-pointer" @click="savePoster">
<img src="@/assets/images/icon_share_img.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 class="text-center mt-1 text-gray-600 text-xs">保存图片</div>
</div>
<div class="flex flex-col items-center justify-center cursor-pointer" @click="copyUrl">
<img src="@/assets/images/icon_share_url.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 class="text-center mt-1 text-gray-600 text-xs">复制链接</div>
</div>
</template>
</div>
</div>
</van-popup>
</div>
<!-- 图片保存指引遮罩层 -->
<ImageSaveGuide :show="showImageGuide" :image-url="currentImageUrl" :title="imageGuideTitle"
@@ -85,18 +66,36 @@ import ImageSaveGuide from "./ImageSaveGuide.vue";
const props = defineProps({
linkIdentifier: {
type: String,
required: true,
required: false, // 推广链接模式下需要
},
fullLink: {
type: String,
required: false, // 完整的推广链接(后端返回)
},
inviteLink: {
type: String,
required: false, // 邀请链接模式下需要
},
qrCodeUrl: {
type: String,
required: false, // 邀请链接模式下提供,直接使用后端返回的二维码
},
mode: {
type: String,
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 currentIndex = ref(0); // 当前显示的海报索引
const currentIndex = ref(0); // 当前选中的海报索引
const postersGenerated = ref([]); // 标记海报是否已经生成过将在onMounted中初始化
const show = defineModel("show");
const posterLoading = ref([]); // 单张海报加载状态
const isGeneratingPosters = ref(true); // 是否正在生成所有海报
const generatedCount = ref(0); // 已生成的海报数量
// 微信环境检测
const isWeChat = computed(() => {
@@ -111,35 +110,90 @@ const showImageGuide = ref(false);
const currentImageUrl = ref('');
const imageGuideTitle = ref('');
const url = computed(() => {
const baseUrl = window.location.origin; // 获取当前站点的域名
return mode.value === "promote"
? `${baseUrl}/agent/promotionInquire/` // 使用动态的域名
: `${baseUrl}/agent/invitationAgentApply/`;
if (mode.value === "invitation" && inviteLink.value) {
// 邀请模式:使用完整的邀请链接(已包含域名)
return inviteLink.value;
}
// 推广链接模式:使用后端返回的完整短链
if (mode.value === "promote" && fullLink.value) {
return fullLink.value;
}
// 如果没有完整链接,返回空字符串(不应该发生,因为后端应该总是返回 full_link
console.warn("推广链接模式但未提供 fullLink");
return "";
});
// 海报图片数组
const posterImages = ref([]);
// QR码位置配置为每个海报单独配置
const qrCodePositions = ref({
promote: [
{ x: 180, y: 1440, size: 300 }, // tg_qrcode_1.png
{ x: 525, y: 1955, size: 500 }, // tg_qrcode_2.jpg
{ x: 525, y: 1955, size: 500 }, // tg_qrcode_3.jpg
{ x: 525, y: 1955, size: 500 }, // tg_qrcode_4.jpg
{ x: 525, y: 1955, size: 500 }, // tg_qrcode_5.jpg
{ x: 525, y: 1955, size: 500 }, // tg_qrcode_6.jpg
{ x: 255, y: 940, size: 250 }, // tg_qrcode_7.jpg
{ x: 255, y: 940, size: 250 }, // tg_qrcode_8.jpg
],
// invitation模式的配置 (yq_qrcode)
invitation: [
{ x: 360, y: -1370, size: 360 }, // yq_qrcode_1.png
],
});
// 产品类型到海报目录的映射
const productTypeToDirMap = {
'riskassessment': 'riskassessment',
'companyinfo': 'companyinfo',
'preloanbackgroundcheck': 'backgroundcheck',
'marriage': 'marriage',
'homeservice': 'homeservice',
'backgroundcheck': 'backgroundcheck',
'consumerFinanceReport': 'consumerFinanceReport',
'invitation': 'invitation'
};
// 处理轮播图切换事件
const onSwipeChange = (index) => {
// QR码位置配置为每个产品类型的每张海报单独配置
// 格式:{ 产品类型: [{ 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;
if (!postersGenerated.value[index]) {
generatePoster(index);
@@ -149,45 +203,46 @@ const onSwipeChange = (index) => {
// 加载海报图片
const loadPosterImages = async () => {
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) {
// 尝试加载 .png 文件
try {
const module = await import(
`@/assets/images/${basePrefix}${i}.png`
);
images.push(module.default);
continue; // 如果成功加载了 png则跳过后续的 jpg 尝试
} catch (error) {
console.warn(
`Image ${basePrefix}${i}.png not found, trying jpg...`
);
}
const imagePath = `/image/poster/${posterDir}/${posterDir}_${String(i).padStart(2, '0')}.jpg`;
// 使用 new Image() 预加载图片,确保图片存在
const img = new Image();
img.crossOrigin = 'anonymous'; // 如果需要跨域
// 如果 .png 不存在,尝试加载 .jpg 文件
try {
const module = await import(
`@/assets/images/${basePrefix}${i}.jpg`
);
images.push(module.default);
await new Promise((resolve, reject) => {
img.onload = () => {
// 图片加载成功,使用完整路径
images.push(imagePath);
resolve();
};
img.onerror = () => {
console.warn(`Failed to load poster: ${imagePath}`);
// 如果加载失败,仍然添加路径,让 canvas 处理错误
images.push(imagePath);
resolve(); // 继续加载其他图片
};
img.src = imagePath;
});
} catch (error) {
console.warn(
`Image ${basePrefix}${i}.jpg not found either, using fallback.`
);
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]);
}
console.error(`Error loading poster ${i}:`, error);
// 即使出错也添加路径
images.push(imagePath);
}
}
@@ -198,6 +253,15 @@ onMounted(async () => {
posterImages.value = await loadPosterImages();
// 根据加载的图片数量初始化postersGenerated数组
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,23 +289,30 @@ const generatePoster = async (index) => {
// 2. 绘制海报图片
ctx.drawImage(posterImg, 0, 0);
// 3. 生成二维码
QRCode.toDataURL(
generalUrl(),
{ width: 150, margin: 0 },
(err, qrCodeUrl) => {
if (err) {
console.error(err);
return;
}
// 3. 生成或加载二维码
const loadQRCode = (qrCodeDataUrl) => {
// 4. 加载二维码图片
const qrCodeImg = new Image();
qrCodeImg.src = qrCodeUrl;
qrCodeImg.src = qrCodeDataUrl;
qrCodeImg.onload = () => {
// 获取当前海报的二维码位置配置
const positions = qrCodePositions.value[mode.value];
const position = positions[index] || positions[0]; // 如果没有对应索引的配置,则使用第一个配置
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 };
}
const position = positionConfig;
// 计算Y坐标负值表示从底部算起的位置
const qrY =
@@ -260,16 +331,56 @@ const generatePoster = async (index) => {
// 标记海报已生成
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(show, (newVal) => {
if (newVal && !postersGenerated.value[currentIndex.value]) {
generatePoster(currentIndex.value); // 当弹窗显示且当前海报未生成时生成海报
watch(url, (newVal) => {
if (newVal && posterImages.value.length > 0) {
for (let i = 0; i < posterImages.value.length; i++) {
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,
imgUrl: "https://www.quannengcha.com/logo.png"
imgUrl: "https://www.onecha.cn/logo.png"
};
configWeixinShare(shareConfig);
@@ -314,7 +425,7 @@ const shareToTimeline = () => {
? "扫码查看一查查推广信息"
: "扫码申请一查查代理权限",
link: shareUrl,
imgUrl: "https://www.quannengcha.com/logo.png"
imgUrl: "https://www.onecha.cn/logo.png"
};
configWeixinShare(shareConfig);
@@ -484,7 +595,10 @@ const tryShareAPI = async (dataURL) => {
};
const generalUrl = () => {
return url.value + encodeURIComponent(linkIdentifier.value);
// 直接使用 url computed 属性,它已经处理了所有情况
// 推广模式:优先使用 fullLink否则使用 linkIdentifier 构建
// 邀请模式:优先使用 inviteLink否则使用 linkIdentifier 构建
return url.value;
};
const copyUrl = () => {
@@ -525,82 +639,109 @@ const copyToClipboard = (text) => {
.qrcode-popup-container {
display: flex;
flex-direction: column;
max-height: 95vh;
height: calc(100vh - 46px);
max-height: calc(100vh - 46px);
overflow: hidden;
width: 100%;
max-width: 100vw;
}
.qrcode-content {
position: relative;
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) {
.qrcode-content {
padding: 0.5rem;
padding: 0.25rem;
}
}
/* 中等及以上屏幕 */
@media (min-width: 640px) {
.qrcode-content {
padding: 1rem;
padding: 0.75rem;
}
}
.poster-swiper {
height: calc(95vh - 180px);
min-height: 300px;
max-height: 500px;
/* 网格布局2x2显示4张海报 */
.poster-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.3rem;
width: 100%;
max-width: 100%;
align-items: start;
justify-items: center;
box-sizing: border-box;
}
/* 小屏设备:更小的海报高度 */
/* 小屏设备优化 */
@media (max-width: 375px) {
.poster-swiper {
height: calc(95vh - 160px);
min-height: 280px;
max-height: 400px;
.poster-grid {
gap: 0.25rem;
}
}
/* 中等屏幕 */
@media (min-width: 640px) and (max-width: 767px) {
.poster-swiper {
height: calc(95vh - 190px);
max-height: 520px;
/* 中等及以上屏幕 */
@media (min-width: 640px) {
.poster-grid {
gap: 0.75rem;
}
}
/* 大屏幕 */
@media (min-width: 768px) {
.poster-swiper {
height: calc(95vh - 200px);
max-height: 600px;
}
.poster-item {
position: relative;
width: 100%;
/* 海报比例 1889:3425 ≈ 0.551 */
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 {
width: 100%;
height: 100%;
object-fit: contain;
}
.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;
}
border-radius: 0.25rem;
display: block;
}
.share-icon {

View File

@@ -1,11 +1,19 @@
<script setup>
import { ref, computed } from "vue";
import { useRouter } from "vue-router";
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 dialogStore = useDialogStore();
const agentStore = useAgentStore();
const userStore = useUserStore();
import { showToast } from "vant";
const { runWithCaptcha } = useAliyunCaptcha();
// 表单数据
const realName = ref("");
const idCard = ref("");
@@ -47,25 +55,26 @@ const canSubmit = computed(() => {
);
});
// 发送验证码
// 发送验证码(使用阿里云滑块验证码防刷)
async function sendVerificationCode() {
if (isCountingDown.value || !isPhoneNumberValid.value) return;
if (!isPhoneNumberValid.value) {
showToast({ message: "请输入有效的手机号" });
return;
}
const { data, error } = await useApiFetch("auth/sendSms")
.post({ mobile: phoneNumber.value, actionType: "realName" })
.json();
if (data.value && !error.value) {
if (data.value.code === 200) {
runWithCaptcha(
(captchaVerifyParam) => useApiFetch("auth/sendSms")
.post({ mobile: phoneNumber.value, actionType: "realName", captchaVerifyParam })
.json(),
(result) => {
if (result && result.code === 200) {
showToast({ message: "获取成功" });
startCountdown();
} else {
showToast(data.value.msg);
} else if (result) {
showToast(result.msg || "发送失败");
}
}
);
}
function startCountdown() {
@@ -104,14 +113,12 @@ async function handleSubmit() {
return;
}
const { data, error } = await useApiFetch("/agent/real_name")
.post({
const { data, error } = await realNameAuth({
name: realName.value,
id_card: idCard.value,
mobile: phoneNumber.value,
code: verificationCode.value,
})
.json();
});
if (data.value && !error.value) {
if (data.value.code === 200) {

View File

@@ -1,11 +1,15 @@
<template>
<div class=" m-4">
<div class="flex items-center">
<img src="@/assets/images/report/wxts_icon.png" alt="温馨提示" class="tips-icon" />
<span class="tips-title">温馨提示!</span>
<img src="@/assets/images/report/wxts_icon.png" :alt="title" class="tips-icon" />
<span class="tips-title">{{ title }}</span>
</div>
<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>
</template>
@@ -17,6 +21,14 @@ const props = defineProps({
content: {
type: String,
required: true
},
title: {
type: String,
default: '温馨提示!'
},
defaultExpanded: {
type: Boolean,
default: false
}
});
@@ -43,7 +55,7 @@ const isExpanded = ref(false);
}
.tips-content {
font-size: 14px;
font-size: 16px;
color: #333;
}
@@ -77,8 +89,7 @@ const isExpanded = ref(false);
}
:deep(.van-text-ellipsis) {
font-size: 12px;
font-size: 14px;
color: #999999;
}
</style>

View File

@@ -1,5 +1,5 @@
<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="w-1 h-5 rounded-full mr-2"
style="background: linear-gradient(to bottom, var(--van-theme-primary), var(--van-theme-primary-dark));">
@@ -7,9 +7,27 @@
报告包含内容
</div>
<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个独立的案件类型 -->
<template v-if="feature.api_id === 'FLXG0V4B'">
<template v-else-if="feature.api_id === 'FLXG0V4B'">
<div v-for="(caseType, caseIndex) in [
{ name: '管辖案件', icon: 'beijianguanrenyuan.svg' },
{ name: '刑事案件', icon: 'xingshi.svg' },
@@ -181,12 +199,29 @@
</template>
<script setup>
import { computed } from 'vue';
import {
isRiskAssessmentProduct,
resolveInquireProductEn,
riskAssessmentReportModules,
} from '@/constants/riskAssessmentReportFeatures';
// Props
const props = defineProps({
features: {
type: Array,
default: () => []
},
/** 查询/推广产品标识(路由 feature 或推广链接 product_en */
inquireProduct: {
type: String,
default: ''
},
/** 推广查询等场景下的完整产品数据,用于 fallback product_en */
inquireFeatureData: {
type: Object,
default: () => ({})
},
containerClass: {
type: String,
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 iconMap = {

View File

@@ -1,6 +1,5 @@
<script setup>
import { ref } from "vue";
import { showToast, showDialog } from "vant";
import { useShareReport } from "@/composables/useShareReport";
const props = defineProps({
orderId: {
@@ -21,104 +20,16 @@ const props = defineProps({
},
});
const isLoading = ref(false);
const { isLoading, handleShare } = useShareReport();
const copyToClipboard = async (text) => {
await navigator.clipboard.writeText(text);
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;
}
const onShare = () => {
handleShare(props.orderId, props.orderNo, props.isExample, props.disabled);
};
</script>
<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"
: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" />
<span class="text-white text-sm font-medium">
{{ isLoading ? "生成中..." : (isExample ? "分享示例" : "分享报告") }}

View 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>

View File

@@ -1,14 +1,50 @@
<template>
<div class="title-banner-wrapper">
<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>
</template>
<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>
<style scoped>
.title-banner-wrapper {
@apply flex items-center justify-center gap-2 relative;
}
.title-banner {
@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);
@@ -20,4 +56,17 @@
position: relative;
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>

View File

@@ -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>

View 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>

View File

@@ -1,10 +1,14 @@
<template>
<div v-if="isWeChat" class="wechat-overlay">
<img src="@/assets/images/ysjjt.png" alt="Arrow" class="wechat-image" />
<div class="wechat-content">
<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>
<img src="@/assets/images/llqdk.jpg" alt="In WeChat" class="wechat-image" />
</div>
</div>
</template>
@@ -37,36 +41,54 @@ onMounted(() => {
bottom: 0;
background: rgba(0, 0, 0, 0.7);
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 {
text-align: center;
position: absolute;
top: 20%;
left: 50%;
transform: translate(-50%, -50%);
text-align: left;
color: white;
font-size: 16px;
}
/* 图片样式 */
.wechat-image {
/* position: absolute;
bottom: 0;
left: 0; */
margin-top: 20px;
width: 100%;
width: 80%;
max-width: 400px;
}
/* 提示信息的样式 */
.wechat-message {
font-size: 24px;
font-size: 18px;
line-height: 1.8;
margin: 0 0 20px 0;
}
/* 图标样式 */
.icon-more-vert {
font-size: 20px;
/* 三个点特殊样式 */
.dots {
font-size: 36px;
font-weight: bold;
margin-left: 5px;
}
/* 手机浏览器特殊颜色 */
.browser-text {
color: #ffd700;
font-weight: bold;
}
/* 额外提示信息样式 */
.tip-message {
font-size: 14px;
color: white;
line-height: 1.6;
margin: 0;
}
</style>

View 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>

View File

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

View File

@@ -6,7 +6,9 @@ import { useAgentStore } from "@/stores/agentStore";
// 创建全局的 fetch 实例
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: {
async beforeFetch({ url, options }) {
showLoadingToast({
@@ -28,13 +30,24 @@ const useApiFetch = createFetch({
if (isWechat) {
platform = "wxh5";
}
options.headers['X-Platform'] = platform
options.headers["X-Platform"] = platform;
if (token) {
options.headers = {
...options.headers,
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 };
},
@@ -44,30 +57,41 @@ const useApiFetch = createFetch({
if (response.status === 401) {
// 清除本地存储的 token
localStorage.removeItem("token");
localStorage.removeItem('refreshAfter')
localStorage.removeItem('accessExpire')
localStorage.removeItem("refreshAfter");
localStorage.removeItem("accessExpire");
// 跳转到登录页
router.replace("/login");
}
if (data.code !== 200) {
if (data.code === 100009) {
// 用户未找到:清除所有数据并刷新页面
// 改进的存储管理
localStorage.removeItem('token')
localStorage.removeItem('refreshAfter')
localStorage.removeItem('accessExpire')
localStorage.removeItem('userInfo')
localStorage.removeItem('agentInfo')
localStorage.removeItem("token");
localStorage.removeItem("refreshAfter");
localStorage.removeItem("accessExpire");
localStorage.removeItem("userInfo");
localStorage.removeItem("agentInfo");
localStorage.removeItem("tokenVersion");
// 重置状态
const userStore = useUserStore();
const agentStore = useAgentStore();
userStore.resetUser()
agentStore.resetAgent()
location.reload()
userStore.resetUser();
agentStore.resetAgent();
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 });
}
}
@@ -76,11 +100,11 @@ const useApiFetch = createFetch({
async onFetchError({ error, response }) {
console.log("error", error);
closeToast();
if (response.status === 401) {
if (response && response.status === 401) {
// 清除本地存储的 token
localStorage.removeItem("token");
localStorage.removeItem('refreshAfter')
localStorage.removeItem('accessExpire')
localStorage.removeItem("refreshAfter");
localStorage.removeItem("accessExpire");
// 跳转到登录页
router.replace("/login");
} else {

View File

@@ -1,197 +1,213 @@
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import { ref, watch } from "vue";
import { useRoute } from "vue-router";
export function useSEO() {
const route = useRoute()
const route = useRoute();
// 默认SEO信息
const defaultSEO = {
title: '一查查|大数据风险报告查询与代理平台,支持个人和企业多场景风控应用',
description: '一查查,专业大数据风险报告查询与代理平台,支持个人信用查询、小微企业风控、贷前风险背调等多场景报告应用,免费开通代理权限,助力高效识别信用与风险。',
keywords: '大数据风险报告查询、大数据风险评估、大数据分析报告、个人大数据风险查询、小微企业风、贷前风险背调、代理管理平台、免费开通代理、风险管控平台、信用风险分析、企业风险报告、贷前信用审核、失信人名单查询、被执行人信息、信用黑名单查询',
url: 'https://www.zhinengcha.cn'
}
title: "一查查|大数据风险报告查询与代理平台,支持个人和企业多场景风控应用",
description:
"一查查,专业大数据风险报告查询与代理平台,支持个人风险查询、小微企业风、贷前风险背调等多场景报告应用,免费开通代理权限,助力高效识别信用与风险。",
keywords:
"大数据风险报告查询、大数据风险评估、大数据分析报告、个人大数据风险查询、小微企业风险、贷前风险背调、代理管理平台、免费开通代理、风险管控平台、信用风险分析、企业风险报告、贷前信用审核、失信人名单查询、被执行人信息、信用黑名单查询",
url: "https://www.zhinengcha.cn",
};
// 页面SEO配置
const pageSEO = ref({
title: '',
description: '',
keywords: '',
url: ''
})
title: "",
description: "",
keywords: "",
url: "",
});
// 更新页面SEO信息
const updateSEO = (seoConfig) => {
const config = { ...defaultSEO, ...seoConfig }
const config = { ...defaultSEO, ...seoConfig };
// 更新页面标题
document.title = config.title
document.title = config.title;
// 更新meta描述
let metaDescription = document.querySelector('meta[name="description"]')
let metaDescription = document.querySelector(
'meta[name="description"]'
);
if (!metaDescription) {
metaDescription = document.createElement('meta')
metaDescription.name = 'description'
document.head.appendChild(metaDescription)
metaDescription = document.createElement("meta");
metaDescription.name = "description";
document.head.appendChild(metaDescription);
}
metaDescription.content = config.description
metaDescription.content = config.description;
// 更新meta关键词
let metaKeywords = document.querySelector('meta[name="keywords"]')
let metaKeywords = document.querySelector('meta[name="keywords"]');
if (!metaKeywords) {
metaKeywords = document.createElement('meta')
metaKeywords.name = 'keywords'
document.head.appendChild(metaKeywords)
metaKeywords = document.createElement("meta");
metaKeywords.name = "keywords";
document.head.appendChild(metaKeywords);
}
metaKeywords.content = config.keywords
metaKeywords.content = config.keywords;
// 更新Open Graph标签
updateOpenGraph(config)
updateOpenGraph(config);
// 更新Twitter Cards
updateTwitterCards(config)
updateTwitterCards(config);
// 更新canonical URL
updateCanonicalURL(config.url)
updateCanonicalURL(config.url);
// 更新结构化数据
updateStructuredData(config)
}
updateStructuredData(config);
};
// 更新Open Graph标签
const updateOpenGraph = (config) => {
const ogTags = {
'og:title': config.title,
'og:description': config.description,
'og:url': config.url,
'og:type': 'website',
'og:site_name': '一查查',
'og:locale': 'zh_CN'
}
"og:title": config.title,
"og:description": config.description,
"og:url": config.url,
"og:type": "website",
"og:site_name": "一查查",
"og:locale": "zh_CN",
};
Object.entries(ogTags).forEach(([property, content]) => {
let meta = document.querySelector(`meta[property="${property}"]`)
let meta = document.querySelector(`meta[property="${property}"]`);
if (!meta) {
meta = document.createElement('meta')
meta.setAttribute('property', property)
document.head.appendChild(meta)
}
meta.content = content
})
meta = document.createElement("meta");
meta.setAttribute("property", property);
document.head.appendChild(meta);
}
meta.content = content;
});
};
// 更新Twitter Cards
const updateTwitterCards = (config) => {
const twitterTags = {
'twitter:card': 'summary',
'twitter:title': config.title,
'twitter:description': config.description,
'twitter:url': config.url
}
"twitter:card": "summary",
"twitter:title": config.title,
"twitter:description": config.description,
"twitter:url": config.url,
};
Object.entries(twitterTags).forEach(([name, content]) => {
let meta = document.querySelector(`meta[name="${name}"]`)
let meta = document.querySelector(`meta[name="${name}"]`);
if (!meta) {
meta = document.createElement('meta')
meta.name = name
document.head.appendChild(meta)
}
meta.content = content
})
meta = document.createElement("meta");
meta.name = name;
document.head.appendChild(meta);
}
meta.content = content;
});
};
// 更新canonical URL
const updateCanonicalURL = (url) => {
let canonical = document.querySelector('link[rel="canonical"]')
let canonical = document.querySelector('link[rel="canonical"]');
if (!canonical) {
canonical = document.createElement('link')
canonical.rel = 'canonical'
document.head.appendChild(canonical)
}
canonical.href = url
canonical = document.createElement("link");
canonical.rel = "canonical";
document.head.appendChild(canonical);
}
canonical.href = url;
};
// 更新结构化数据
const updateStructuredData = (config) => {
// 移除现有的结构化数据
const existingScripts = document.querySelectorAll('script[type="application/ld+json"]')
existingScripts.forEach(script => {
const existingScripts = document.querySelectorAll(
'script[type="application/ld+json"]'
);
existingScripts.forEach((script) => {
if (script.textContent.includes('"@type":"WebPage"')) {
script.remove()
script.remove();
}
})
});
// 添加新的结构化数据
const structuredData = {
"@context": "https://schema.org",
"@type": "WebPage",
"name": config.title,
"description": config.description,
"url": config.url,
"mainEntity": {
name: config.title,
description: config.description,
url: config.url,
mainEntity: {
"@type": "Organization",
"name": "一查查",
"url": "https://www.zhinengcha.cn/",
"description": "专业大数据风险报告查询与代理平台,支持个人和企业多场景风控应用"
}
}
name: "一查查",
url: "https://www.zhinengcha.cn/",
description:
"专业大数据风险报告查询与代理平台,支持个人和企业多场景风控应用",
},
};
const script = document.createElement('script')
script.type = 'application/ld+json'
script.textContent = JSON.stringify(structuredData)
document.head.appendChild(script)
}
const script = document.createElement("script");
script.type = "application/ld+json";
script.textContent = JSON.stringify(structuredData);
document.head.appendChild(script);
};
// 根据路由自动更新SEO
const updateSEOByRoute = () => {
const routeConfigs = {
'/': {
title: '一查查|大数据风险报告查询与代理平台,支持个人和企业多场景风控应用',
description: '一查查,专业大数据风险报告查询与代理平台,支持个人信用查询、小微企业风控、贷前风险背调等多场景报告应用,免费开通代理权限,助力高效识别信用与风险。',
keywords: '大数据风险报告查询、大数据风险评估、大数据分析报告、个人大数据风险查询、小微企业风、贷前风险背调、代理管理平台、免费开通代理、风险管控平台、信用风险分析、企业风险报告、贷前信用审核、失信人名单查询、被执行人信息、信用黑名单查询'
"/": {
title: "一查查|大数据风险报告查询与代理平台,支持个人和企业多场景风控应用",
description:
"一查查,专业大数据风险报告查询与代理平台,支持个人风险查询、小微企业风、贷前风险背调等多场景报告应用,免费开通代理权限,助力高效识别信用风险。",
keywords:
"大数据风险报告查询、大数据风险评估、大数据分析报告、个人大数据风险查询、小微企业风险、贷前风险背调、代理管理平台、免费开通代理、风险管控平台、信用风险分析、企业风险报告、贷前信用审核、失信人名单查询、被执行人信息、信用黑名单查询",
},
'/agent': {
title: '一查查代理 - 免费开通代理权限 | 大数据风险报告代理',
description: '一查查代理平台,免费开通代理权限,享受大数据风险报告查询服务代理收益。专业的大数据风险报告、婚姻查询、个人信用评估等服务的代理合作。',
keywords: '一查查代理, 免费代理, 大数据风险报告代理, 代理权限, 代理收益'
"/agent": {
title: "一查查代理 - 免费开通代理权限 | 大数据风险报告代理",
description:
"一查查代理平台,免费开通代理权限,享受大数据风险报告查询服务代理收益。专业的大数据风险报告、婚姻查询、个人风险评估等服务的代理合作。",
keywords:
"一查查代理, 免费代理, 大数据风险报告代理, 代理权限, 代理收益",
},
'/help': {
title: '帮助中心 - 一查查使用指南 | 常见问题解答',
description: '一查查帮助中心,提供详细的使用指南、常见问题解答、操作教程等,帮助用户更好地使用大数据风险报告查询服务。',
keywords: '一查查帮助, 使用指南, 常见问题, 操作教程, 客服支持'
"/help": {
title: "帮助中心 - 一查查使用指南 | 常见问题解答",
description:
"一查查帮助中心,提供详细的使用指南常见问题解答、操作教程等,帮助用户更好地使用大数据风险报告查询服务。",
keywords: "一查查帮助, 使用指南, 常见问题, 操作教程, 客服支持",
},
'/help/guide': {
title: '使用指南 - 一查查操作教程 | 功能说明',
description: '一查查详细使用指南,包含各功能模块的操作教程、功能说明、注意事项等,让用户快速上手使用。',
keywords: '使用指南, 操作教程, 功能说明, 快速上手, 一查查教程'
"/help/guide": {
title: "使用指南 - 一查查操作教程 | 功能说明",
description:
"一查查详细使用指南,包含各功能模块的操作教程功能说明、注意事项等,让用户快速上手使用。",
keywords: "使用指南, 操作教程, 功能说明, 快速上手, 一查查教程",
},
'/example': {
title: '示例报告 - 一查查报告展示 | 大数据风险报告样例',
description: '一查查示例报告展示,包含大数据风险报告、婚姻状况查询、个人信用评估等服务的报告样例,让用户了解报告内容和格式。',
keywords: '示例报告, 报告展示, 报告样例, 大数据风险报告, 婚姻查询报告'
"/example": {
title: "示例报告 - 一查查报告展示 | 大数据风险报告样例",
description:
"一查查示例报告展示,包含大数据风险报告、婚姻状况查询、个人风险评估等服务的报告样例,让用户了解报告内容和格式。",
keywords:
"示例报告, 报告展示, 报告样例, 大数据风险报告, 婚姻查询报告",
},
'/service': {
title: '客服中心 - 一查查在线客服 | 技术支持',
description: '一查查客服中心,提供在线客服支持、技术咨询、问题反馈等服务,确保用户获得及时有效的帮助。',
keywords: '客服中心, 在线客服, 技术支持, 问题反馈, 一查查客服'
}
}
"/service": {
title: "客服中心 - 一查查在线客服 | 技术支持",
description:
"一查查客服中心,提供在线客服支持、技术咨询、问题反馈等服务,确保用户获得及时有效的帮助。",
keywords: "客服中心, 在线客服, 技术支持, 问题反馈, 一查查客服",
},
};
const currentPath = route?.path || '/'
const config = routeConfigs[currentPath] || defaultSEO
const currentPath = route?.path || "/";
const config = routeConfigs[currentPath] || defaultSEO;
updateSEO({
...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 {
updateSEO,
updateSEOByRoute,
pageSEO
}
pageSEO,
};
}

View 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,
};
}

View 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 || '';
}

View File

@@ -20,17 +20,17 @@
</div>
<div class="disclaimer">
<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="公安备案" />
<text>琼公网安备46010002000584号</text>
</div>
</div> -->
<div>
<a class="text-blue-500" href="https://beian.miit.gov.cn">
琼ICP备2024048057-2
琼ICP备2024038584-10
</a>
</div>
</div>
<div>海南省学宇思网络科技有限公司版权所有</div>
<div>海南海宇大数据有限公司版权所有</div>
</div>
</div>
</template>
@@ -43,8 +43,8 @@ const router = useRouter();
const route = useRoute();
const tabbar = ref("promote");
const menu = reactive([
{ title: "推广", icon: "cluster-o", name: "promote" },
{ title: "资产", icon: "gold-coin-o", name: "agent" },
{ title: "首页", icon: "home-o", name: "promote" },
{ title: "数据", icon: "bar-chart-o", name: "agent" },
{ title: "我的", icon: "user-o", name: "me" }
]);
// 根据当前路由设置 Tabbar 的高亮项
@@ -83,7 +83,7 @@ const tabChange = (name, a, b, c) => {
// 跳转到投诉页面
const toComplaint = () => {
window.location.href =
"https://work.weixin.qq.com/kfid/kfc8a32720024833f57"; // 跳转到客服页面
"https://work.weixin.qq.com/kfid/kfc82d4424e4b19e5f3"; // 跳转到客服页面
// router.push({ name: 'complaint' }); // 使用 Vue Router 进行跳转
};
</script>

View File

@@ -1,17 +1,17 @@
import { createRouter, createWebHistory } from 'vue-router'
import NProgress from 'nprogress'
import GlobalLayout from '@/layouts/GlobalLayout.vue'
import HomeLayout from '@/layouts/HomeLayout.vue'
import PageLayout from '@/layouts/PageLayout.vue'
import index from '@/views/index.vue'
import Promote from '@/views/Promote.vue'
import PromotePage from '@/views/PromotePage.vue'
import { useAgentStore } from '@/stores/agentStore'
import { useUserStore } from '@/stores/userStore'
import { useDialogStore } from '@/stores/dialogStore'
import { useEnv } from '@/composables/useEnv'
import { storeToRefs } from 'pinia'
import { useSEO } from '@/composables/useSEO'
import { createRouter, createWebHistory } from "vue-router";
import NProgress from "nprogress";
import GlobalLayout from "@/layouts/GlobalLayout.vue";
import HomeLayout from "@/layouts/HomeLayout.vue";
import PageLayout from "@/layouts/PageLayout.vue";
import index from "@/views/index.vue";
import Promote from "@/views/Promote.vue";
import PromotePage from "@/views/PromotePage.vue";
import { useAgentStore } from "@/stores/agentStore";
import { useUserStore } from "@/stores/userStore";
import { useDialogStore } from "@/stores/dialogStore";
import { useEnv } from "@/composables/useEnv";
import { storeToRefs } from "pinia";
import { useSEO } from "@/composables/useSEO";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@@ -27,262 +27,309 @@ const router = createRouter({
},
routes: [
{
path: '/',
path: "/",
component: GlobalLayout, // 使用 Layout 作为父组件
children: [
{
path: '',
path: "",
component: HomeLayout, // 使用 Layout 作为父组件
children: [
{
path: '',
name: 'promote',
path: "",
name: "promote",
component: PromotePage,
},
{
path: 'index',
name: 'index',
path: "index",
name: "index",
component: index,
},
{
path: '/agent',
name: 'agent',
component: () => import('@/views/Agent.vue'),
path: "/agent",
name: "agent",
component: () => import("@/views/Agent.vue"),
meta: { title: "代理主页", requiresAuth: true },
},
{
path: 'me',
name: 'me',
component: () => import('@/views/Me.vue'),
path: "me",
name: "me",
component: () => import("@/views/Me.vue"),
},
],
},
{
path: '',
path: "",
component: PageLayout,
children: [
{
path: '/historyQuery',
name: 'history',
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'),
path: "/historyQuery",
name: "history",
component: () => import("@/views/HistoryQuery.vue"),
meta: {
title: '报告结果', requiresAuth: true, notNeedBindPhone: true
title: "历史报告",
requiresAuth: true,
notNeedBindPhone: true,
},
},
{
path: '/example',
name: 'example',
component: () => import('@/views/Example.vue'),
meta: { title: '示例报告', notNeedBindPhone: true },
path: "/help",
name: "help",
component: () => import("@/views/Help.vue"),
meta: { title: "帮助中心" },
},
{
path: '/vant-theme-test',
name: 'vantThemeTest',
component: () => import('@/views/VantThemeTest.vue'),
meta: { title: 'Vant主题色测试' },
path: "/help/detail",
name: "helpDetail",
component: () => import("@/views/HelpDetail.vue"),
meta: { title: "帮助中心" },
},
{
path: '/privacyPolicy',
name: 'privacyPolicy',
component: () => import('@/views/PrivacyPolicy.vue'),
meta: { title: '隐私政策' },
path: "/help/guide",
name: "helpGuide",
component: () => import("@/views/HelpGuide.vue"),
meta: { title: "引导指南" },
},
{
path: '/userAgreement',
name: 'userAgreement',
component: () => import('@/views/UserAgreement.vue'),
meta: { title: '用户协议' },
path: "/agent/system-guide",
name: "agentSystemGuide",
component: () =>
import("@/views/AgentSystemGuide.vue"),
meta: { title: "代理系统指南" },
},
{
path: '/agentManageAgreement',
name: 'agentManageAgreement',
component: () => import('@/views/AgentManageAgreement.vue'),
meta: { title: '代理管理协议' },
path: "/withdraw",
name: "withdraw",
component: () => import("@/views/Withdraw.vue"),
meta: { title: "提现", requiresAuth: true },
},
{
path: '/agentSerivceAgreement',
name: 'agentSerivceAgreement',
component: () => import('@/views/AgentServiceAgreement.vue'),
meta: { title: '信息技术服务合同' },
path: "/service",
name: "service",
component: () => import("@/views/Service.vue"),
meta: { title: "客服" },
},
{
path: '/inquire/:feature',
name: 'inquire',
component: () => import('@/views/Inquire.vue'),
meta: { title: '查询报告' },
path: "/complaint",
name: "complaint",
component: () => import("@/views/Complaint.vue"),
meta: { title: "投诉" },
},
{
path: '/authorization',
name: 'authorization',
component: () => import('@/views/Authorization.vue'),
meta: { title: '授权书' },
path: "/report",
name: "report",
component: () => import("@/views/Report.vue"),
meta: {
title: "报告结果",
requiresAuth: true,
notNeedBindPhone: true,
},
},
{
path: '/payment/result',
name: 'paymentResult',
component: () => import('@/views/PaymentResult.vue'),
meta: { title: '支付结果', requiresAuth: true },
path: "/example",
name: "example",
component: () => import("@/views/Example.vue"),
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,
children: [
{
path: '/agent/promote',
name: 'agentPromote',
path: "/agent/promote",
name: "agentPromote",
component: Promote,
meta: {
title: '推广报告',
title: "推广报告",
requiresAuth: true,
requiresAgent: true,
},
},
{
path: 'promoteDetails',
name: 'promoteDetails',
component: () => import('@/views/AgentPromoteDetails.vue'),
path: "/agent/promote/poster",
name: "agentPromotePoster",
component: () => import("@/views/PromotePoster.vue"),
meta: {
title: '直推报告收益明细',
title: "推广海报",
requiresAuth: true,
requiresAgent: true,
},
},
{
path: 'rewardsDetails',
name: 'rewardsDetails',
component: () => import('@/views/AgentRewardsDetails.vue'),
path: "/agent/promotion/query/list",
name: "agentPromotionQueryList",
component: () =>
import("@/views/AgentPromotionHistory.vue"),
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,
requiresAgent: true,
},
},
{
path: 'invitation',
name: 'invitation',
component: () => import('@/views/Invitation.vue'),
path: "invitation",
name: "invitation",
component: () =>
import("@/views/InvitationPage.vue"),
meta: {
title: '邀请下级',
title: "邀请下级",
requiresAuth: true,
requiresAgent: true,
},
},
{
path: 'agentVip',
name: 'agentVip',
component: () => import('@/views/AgentVip.vue'),
path: "withdraw",
name: "withdraw",
component: () => import("@/views/Withdraw.vue"),
meta: {
title: '代理会员',
title: "提现",
requiresAuth: true,
requiresAgent: true,
},
},
{
path: 'vipApply',
name: 'agentVipApply',
component: () => import('@/views/AgentVipApply.vue'),
path: "withdrawDetails",
name: "withdrawDetails",
component: () =>
import("@/views/WithdrawDetails.vue"),
meta: {
title: 'VIP代理申请',
title: "提现记录",
requiresAuth: true,
requiresAgent: true,
},
},
{
path: 'vipConfig',
name: 'agentVipConfig',
component: () => import('@/views/AgentVipConfig.vue'),
path: "teamList",
name: "teamList",
component: () => import("@/views/TeamList.vue"),
meta: {
title: '代理会员报告配置',
title: "我的团队",
requiresAuth: true,
requiresAgent: true,
},
},
{
path: 'withdraw',
name: 'withdraw',
component: () => import('@/views/Withdraw.vue'),
path: "upgradeSubordinate",
name: "upgradeSubordinate",
component: () =>
import("@/views/UpgradeSubordinate.vue"),
meta: {
title: '提现',
title: "调整下级级别",
requiresAuth: true,
requiresAgent: true,
},
},
{
path: 'withdrawDetails',
name: 'withdrawDetails',
component: () => import('@/views/WithdrawDetails.vue'),
path: "upgrade",
name: "agentUpgrade",
component: () => import("@/views/AgentUpgrade.vue"),
meta: {
title: '提现记录',
title: "升级代理",
requiresAuth: true,
requiresAgent: true,
},
},
{
path: 'invitationAgentApply/self',
name: 'invitationAgentApplySelf',
component: () => import('@/views/InvitationAgentApply.vue'),
meta: { title: '代理申请', requiresAuth: true },
},
{
path: 'subordinateList',
name: 'subordinateList',
component: () => import('@/views/SubordinateList.vue'),
path: "subordinateList",
name: "subordinateList",
component: () =>
import("@/views/SubordinateList.vue"),
meta: {
title: '我的下级',
title: "我的下级",
requiresAuth: true,
requiresAgent: true,
},
},
{
path: 'subordinateDetail/:id',
name: 'subordinateDetail',
component: () => import('@/views/SubordinateDetail.vue'),
path: "subordinateDetail/:id",
name: "subordinateDetail",
component: () =>
import("@/views/SubordinateDetail.vue"),
meta: {
title: '下级贡献详情',
title: "下级贡献详情",
requiresAuth: true,
requiresAgent: true,
},
@@ -290,37 +337,42 @@ const router = createRouter({
],
},
{
path: 'app',
path: "app",
children: [
{
path: 'authorization',
name: 'appAuthorization',
component: () => import('@/views/Authorization.vue'),
meta: { title: '授权书' },
path: "authorization",
name: "appAuthorization",
component: () =>
import("@/views/Authorization.vue"),
meta: { title: "授权书" },
},
{
path: 'privacyPolicy',
name: 'appPrivacyPolicy',
component: () => import('@/views/PrivacyPolicy.vue'),
meta: { title: '隐私政策' },
path: "privacyPolicy",
name: "appPrivacyPolicy",
component: () =>
import("@/views/PrivacyPolicy.vue"),
meta: { title: "隐私政策" },
},
{
path: 'userAgreement',
name: 'appUserAgreement',
component: () => import('@/views/UserAgreement.vue'),
meta: { title: '用户协议' },
path: "userAgreement",
name: "appUserAgreement",
component: () =>
import("@/views/UserAgreement.vue"),
meta: { title: "用户协议" },
},
{
path: 'agentManageAgreement',
name: 'appAgentManageAgreement',
component: () => import('@/views/AgentManageAgreement.vue'),
meta: { title: '代理管理协议' },
path: "agentManageAgreement",
name: "appAgentManageAgreement",
component: () =>
import("@/views/AgentManageAgreement.vue"),
meta: { title: "代理管理协议" },
},
{
path: 'agentSerivceAgreement',
name: 'appAgentSerivceAgreement',
component: () => import('@/views/AgentServiceAgreement.vue'),
meta: { title: '信息技术服务合同' },
path: "agentSerivceAgreement",
name: "appAgentSerivceAgreement",
component: () =>
import("@/views/AgentServiceAgreement.vue"),
meta: { title: "信息技术服务合同" },
},
],
},
@@ -328,45 +380,50 @@ const router = createRouter({
},
{
path: '/login',
name: 'login',
component: () => import('@/views/Login.vue'),
path: "/login",
name: "login",
component: () => import("@/views/Login.vue"),
},
{
path: '/agent/promotionInquire/:linkIdentifier',
name: 'promotionInquire',
component: () => import('@/views/PromotionInquire.vue'),
path: "/register",
name: "register",
component: () => import("@/views/Register.vue"),
},
{
path: "/agent/promotionInquire/:linkIdentifier",
name: "promotionInquire",
component: () => import("@/views/PromotionInquire.vue"),
meta: { notNeedBindPhone: true },
},
{
path: '/agent/invitationAgentApply/:linkIdentifier',
name: 'invitationAgentApply',
component: () => import('@/views/InvitationAgentApply.vue'),
meta: { title: '代理申请' },
path: "/agent/invitationAgentApply/:linkIdentifier",
name: "invitationAgentApply",
component: () => import("@/views/InvitationAgentApply.vue"),
meta: { title: "代理申请" },
},
{
path: '/report/share/:linkIdentifier',
name: 'reportShare',
component: () => import('@/views/ReportShare.vue'),
path: "/report/share/:linkIdentifier",
name: "reportShare",
component: () => import("@/views/ReportShare.vue"),
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/NotFound.vue'),
path: "/:pathMatch(.*)*",
name: "NotFound",
component: () => import("@/views/NotFound.vue"),
},
],
})
});
NProgress.configure({
easing: 'ease', // 动画方式
easing: "ease", // 动画方式
speed: 500, // 递增进度条的速度(毫秒)
showSpinner: false, // 是否显示加载的圆圈
trickleSpeed: 200, // 自动递增间隔
minimum: 0.3, // 初始化最小百分比
})
});
// 路由导航守卫
router.beforeEach(async (to, from, next) => {
NProgress.start(); // 启动进度条
NProgress.start();
const isAuthenticated = localStorage.getItem("token");
const agentStore = useAgentStore();
const userStore = useUserStore();
@@ -375,93 +432,89 @@ router.beforeEach(async (to, from, next) => {
const { isWeChat } = useEnv();
const { isAgent, isLoaded } = storeToRefs(agentStore);
const { mobile, isLoggedIn } = storeToRefs(userStore);
const { isWeixinAuthing, weixinAuthComplete } = storeToRefs(authStore);
// 微信环境下,如果正在进行授权,等待授权完成
if (isWeChat.value && isWeixinAuthing.value && !weixinAuthComplete.value) {
// 等待授权完成,使用响应式监听
await new Promise((resolve) => {
const stopWatcher = watch(
[isWeixinAuthing, weixinAuthComplete],
([authing, complete]) => {
if (!authing || complete) {
stopWatcher();
resolve();
// 检查 token 是否过期
const accessExpire = localStorage.getItem("accessExpire");
const now = Date.now();
let isTokenExpired = false;
if (accessExpire) {
isTokenExpired = now > parseInt(accessExpire) * 1000;
}
},
{ immediate: true }
);
});
// ============================================================
// 场景 2: 需要登录的页面 + 无 token 或 token 过期 → 跳转登录
// ============================================================
if (to.meta.requiresAuth && (!isAuthenticated || isTokenExpired)) {
const loginQuery = {
redirect: to.fullPath,
};
if (from && from.name === "promotionInquire") {
loginQuery.from = "promotionInquire";
}
// 处理需要登录的页面
if (to.meta.requiresAuth && !isAuthenticated) {
if (isWeChat.value) {
// 微信环境下,如果授权失败或超时,重定向到首页
if (!weixinAuthComplete.value) {
next("/");
location.reload();
} else {
// 授权完成但仍无token可能是授权失败
next("/");
location.reload();
}
} else {
next("/login");
}
next({ path: "/login", query: loginQuery });
return;
}
// 已登录状态下的处理
if (isAuthenticated) {
// ============================================================
// 场景 3: 已登录状态下的处理
// ============================================================
if (isAuthenticated && !isTokenExpired) {
// 确保用户信息已加载
if (!isLoggedIn.value) {
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;
}
// 检查代理权限
// 检查代理权限(仅在 requiresAgent 为 true 时)
if (to.meta.requiresAgent) {
if (!isLoaded.value) {
await agentStore.fetchAgentStatus();
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) {
next("/agent/invitationAgentApply/self");
next("/register");
return;
}
}
}
// ============================================================
// 其他情况正常通过
// ============================================================
next();
})
});
router.afterEach((to) => {
NProgress.done() // 结束进度条
NProgress.done(); // 结束进度条
// SEO优化更新页面标题和meta信息
const { updateSEO } = useSEO()
const { updateSEO } = useSEO();
// 根据路由meta信息更新SEO
if (to.meta.title) {
const seoConfig = {
title: `${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;

View 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();

View File

@@ -1,30 +1,60 @@
import { defineStore } from 'pinia'
import { getAgentInfo } from '@/api/agent'
export const useAgentStore = defineStore('agent', {
state: () => ({
isLoaded: false,
level: '',
status: 3, // 0=待审核1=审核通过2=审核未通过3=未申请
level: 0, // 1=普通2=黄金3=钻石
levelName: '', // 等级名称
isAgent: false,
ancestorID: null,
agentID: null,
agentCode: 0,
mobile: '',
ExpiryTime: '',
region: '',
wechatId: '',
teamLeaderId: null,
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: {
async fetchAgentStatus() {
const { data, error } = await useApiFetch('/agent/info').get().json()
const { data, error } = await getAgentInfo()
if (data.value && !error.value) {
if (data.value.code === 200) {
this.level = data.value.data.level
this.isAgent = data.value.data.is_agent // 判断是否是代理
this.status = data.value.data.status // 获取代理状态 0=待审核1=审核通过2=审核未通过3=未申请
this.agentID = data.value.data.agent_id
this.mobile = data.value.data.mobile
this.ExpiryTime = data.value.data.expiry_time
this.isRealName = data.value.data.is_real_name
const agentData = data.value.data
// 如果 agent_id 为 0说明不是代理
if (agentData.agent_id === 0) {
this.resetAgent()
} else {
this.level = agentData.level || 0
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.setItem(
@@ -32,18 +62,38 @@ export const useAgentStore = defineStore('agent', {
JSON.stringify({
isAgent: this.isAgent,
level: this.level,
status: this.status,
levelName: this.levelName,
agentID: this.agentID,
agentCode: this.agentCode,
mobile: this.mobile,
ExpiryTime: this.ExpiryTime,
region: this.region,
wechatId: this.wechatId,
teamLeaderId: this.teamLeaderId,
isRealName: this.isRealName,
})
)
}
} else {
// 检查是否是临时用户需要绑定手机号的错误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
},
@@ -51,25 +101,30 @@ export const useAgentStore = defineStore('agent', {
updateAgentInfo(agentInfo) {
if (agentInfo) {
this.isAgent = agentInfo.isAgent || false
this.level = agentInfo.level || ''
this.status = agentInfo.status || 3
this.level = agentInfo.level || 0
this.levelName = agentInfo.levelName || ''
this.agentID = agentInfo.agentID || null
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.isLoaded = true
}
},
// 重置代理信息
resetAgent() {
this.isLoaded = false
this.level = ''
this.status = 3
this.level = 0
this.levelName = ''
this.isAgent = false
this.ancestorID = null
this.agentID = null
this.agentCode = 0
this.mobile = ''
this.region = ''
this.wechatId = ''
this.teamLeaderId = null
this.isRealName = false
},
},

21
src/stores/appStore.js Normal file
View 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
},
},
})

View File

@@ -6,39 +6,63 @@ export const useAuthStore = defineStore("auth", {
isWeixinAuthing: false, // 是否正在进行微信授权
weixinAuthComplete: false, // 微信授权是否完成
pendingRoute: null, // 等待授权完成后跳转的路由
authStartTime: 0, // 授权开始时间,用于防止超时重复授权
}),
actions: {
// 开始微信授权
startWeixinAuth(targetRoute = null) {
// 如果已经在授权过程中,不再重复启动
if (this.isWeixinAuthing) {
console.warn("WeChat auth already in progress");
return;
}
this.isWeixinAuthing = true;
this.weixinAuthComplete = false;
this.pendingRoute = targetRoute;
this.authStartTime = Date.now();
// 保存到localStorage防止页面刷新后状态丢失
localStorage.setItem("weixinAuthing", "true");
localStorage.setItem(
"authStartTime",
this.authStartTime.toString()
);
if (targetRoute) {
const routeData =
typeof targetRoute === "string"
? 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(targetRoute)
JSON.stringify(sanitized)
);
}
}
},
// 微信授权完成
completeWeixinAuth() {
this.isWeixinAuthing = false;
this.weixinAuthComplete = true;
this.authStartTime = 0;
// 清除localStorage中的授权状态
localStorage.removeItem("weixinAuthing");
localStorage.removeItem("pendingRoute");
},
// 清除待处理路由
clearPendingRoute() {
this.pendingRoute = null;
localStorage.removeItem("pendingRoute");
localStorage.removeItem("authStartTime");
},
// 重置授权状态
@@ -46,22 +70,47 @@ export const useAuthStore = defineStore("auth", {
this.isWeixinAuthing = false;
this.weixinAuthComplete = false;
this.pendingRoute = null;
this.authStartTime = 0;
localStorage.removeItem("weixinAuthing");
localStorage.removeItem("pendingRoute");
localStorage.removeItem("authStartTime");
},
// 检查授权是否超时超过30秒视为超时
isAuthTimeout() {
if (!this.authStartTime) return false;
const elapsed = Date.now() - this.authStartTime;
return elapsed > 30000; // 30秒超时
},
// 从localStorage恢复状态页面刷新后调用
restoreFromStorage() {
const isAuthing = localStorage.getItem("weixinAuthing") === "true";
const pendingRouteStr = localStorage.getItem("pendingRoute");
const authStartTime = localStorage.getItem("authStartTime");
if (isAuthing) {
console.log("🔄 Restoring WeChat auth state from storage");
this.isWeixinAuthing = true;
this.weixinAuthComplete = false;
this.authStartTime = authStartTime
? parseInt(authStartTime)
: 0;
// 检查是否超时,如果超时则重置
if (this.isAuthTimeout()) {
console.warn("WeChat auth timeout, resetting state");
this.resetAuthState();
return;
}
if (pendingRouteStr) {
try {
this.pendingRoute = JSON.parse(pendingRouteStr);
console.log(
"✅ Restored pendingRoute from storage:",
this.pendingRoute
);
} catch (e) {
console.error("Failed to parse pending route:", e);
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");
},
},
});

View File

@@ -2,7 +2,8 @@ import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useDialogStore = defineStore('dialog', () => {
const showBindPhone = ref(false)
const showBindPhone = ref(false) // 推广页面专用的绑定手机号(不要求邀请码)
const showRegisterAgent = ref(false) // 注册成为代理(带邀请码)
const showRealNameAuth = ref(false)
function openBindPhone() {
@@ -13,6 +14,14 @@ export const useDialogStore = defineStore('dialog', () => {
showBindPhone.value = false
}
function openRegisterAgent() {
showRegisterAgent.value = true
}
function closeRegisterAgent() {
showRegisterAgent.value = false
}
function openRealNameAuth() {
showRealNameAuth.value = true
}
@@ -25,6 +34,9 @@ export const useDialogStore = defineStore('dialog', () => {
showBindPhone,
openBindPhone,
closeBindPhone,
showRegisterAgent,
openRegisterAgent,
closeRegisterAgent,
showRealNameAuth,
openRealNameAuth,
closeRealNameAuth,

View File

@@ -1,59 +1,76 @@
import { defineStore } from 'pinia'
import { defineStore } from "pinia";
export const useUserStore = defineStore('user', {
export const useUserStore = defineStore("user", {
state: () => ({
userName: '',
mobile: '',
userAvatar: '',
userName: "",
mobile: "",
userAvatar: "",
isLoggedIn: false,
}),
actions: {
async fetchUserInfo() {
const { data, error } = await useApiFetch('/user/detail').get().json()
const { data, error } = await useApiFetch("/user/detail")
.get()
.json();
if (data.value && !error.value) {
if (data.value.code === 200) {
const userinfo = data.value.data.userInfo
this.userName = userinfo.mobile || ''
this.mobile = userinfo.mobile || ''
this.userAvatar = userinfo.userAvatar
this.isLoggedIn = true
const userinfo = data.value.data.userInfo;
this.userName = userinfo.mobile || "";
this.mobile = userinfo.mobile || "";
this.userAvatar = userinfo.userAvatar;
this.isLoggedIn = true;
// 保存到localStorage
localStorage.setItem(
'userInfo',
"userInfo",
JSON.stringify({
nickName: this.userName,
avatar: this.userAvatar,
})
)
);
} else if (data.value.code === 100009) {
localStorage.removeItem('token')
localStorage.removeItem('refreshAfter')
localStorage.removeItem('accessExpire')
localStorage.removeItem('userInfo')
localStorage.removeItem('agentInfo')
// Token 无效或用户不存在,清除数据但不 reload
// reload 会导致无限循环
console.warn(
"User not found or token invalid (100009), clearing auth data"
);
localStorage.removeItem("token");
localStorage.removeItem("refreshAfter");
localStorage.removeItem("accessExpire");
localStorage.removeItem("userInfo");
localStorage.removeItem("agentInfo");
this.resetUser()
window.location.reload()
this.resetUser();
// 不要 reload让调用者处理错误
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
this.userName = userInfo.mobile || userInfo.nickName || "";
this.userAvatar = userInfo.avatar || "";
this.isLoggedIn = true;
}
},
// 重置用户信息
resetUser() {
this.userName = ''
this.userAvatar = ''
this.isLoggedIn = false
this.userName = "";
this.userAvatar = "";
this.isLoggedIn = false;
},
},
})
});

View File

@@ -338,7 +338,7 @@
<!-- 温馨提示 -->
<LRemark
content="法院曝光台信息展示申请人的各类案件信息,包括涉案公告、执行案件、失信案件和限高案件。数据来源于全国法院执行信息公开网等权威司法数据库。案件状态包括审理中、执行中、已结案等,执行金额和已还款金额直接反映债务履行情况。失信和限高记录对个人信用影响较大,建议重点关注。数据更新频率依赖于司法系统,可能存在延迟。" />
content="法院曝光台信息展示申请人的各类案件信息,包括涉案公告、执行案件、失信案件和限高案件。数据来源于全国法院执行信息公开网等权威司法数据库。案件状态包括审理中、执行中、已结案等,执行金额和已还款金额直接反映债务履行情况。失信和限高记录对个人风险影响较大,建议重点关注。数据更新频率依赖于司法系统,可能存在延迟。" />
</template>
<script setup>

View File

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

View File

@@ -465,12 +465,12 @@ const riskScore = computed(() => {
const lowRiskCount = summaryData.value.byRiskLevel.find(item => item.id === 'low')?.triggered || 0;
// 计算风险分数
// 高风险项:每个扣 30 分
// 中风险项:每个扣 15
// 低风险项:每个扣 5 分
// 高风险项(无法收回):每个扣 40 分
// 中风险项(严重逾期):每个扣 20
// 低风险项(短期逾期):每个扣 5 分
let score = 100;
score -= highRiskCount * 30;
score -= mediumRiskCount * 15;
score -= highRiskCount * 40;
score -= mediumRiskCount * 20;
score -= lowRiskCount * 5;
return Math.max(0, Math.min(100, score));

View File

@@ -57,7 +57,7 @@
<!-- 所有风险类型列表 -->
<div class="space-y-3">
<!-- 正常人员 -->
<!-- 人员状态 -->
<div class="rounded-lg p-4 border-2 relative" :class="getRiskItemClass('0')">
<div
:class="['absolute top-0 right-0 px-1.5 py-0.5 text-sm font-bold text-white rounded-bl-lg rounded-tr-lg', getRiskBadgeClass('0')]">
@@ -65,13 +65,15 @@
</div>
<div class="flex items-center pr-12">
<div class="w-8 h-8 mr-3 flex-shrink-0 flex items-center justify-center">
<img :src="getRiskItemIcon('0')" alt="正常人员" class="w-8 h-8 object-contain" />
<img :src="getRiskItemIcon('0')" :alt="isNormalPerson ? '正常人员' : '存在风险'"
class="w-8 h-8 object-contain" />
</div>
<div class="flex-1">
<div class="font-bold text-sm" :class="getRiskItemTextColor('0')">
{{ getRiskTypeInfo('0').text }}
{{ isNormalPerson ? '正常人员' : '人员状态' }}
</div>
<div class="text-sm text-[#999999] mt-0.5">{{ getRiskTypeInfo('0').description }}</div>
<div class="text-sm text-[#999999] mt-0.5">{{ isNormalPerson ? '无不良记录,属于正常人员' : '存在不良记录风险'
}}</div>
</div>
</div>
</div>

View File

@@ -62,16 +62,17 @@ const currentStatus = !actualData
:class="`status-label rounded-full px-6 py-3 text-center font-bold shadow-md ${currentStatus.bgClass} ${currentStatus.textClass}`">
{{ currentStatus.text }}
</div>
<div v-if="currentStatus.opDate" class="op-date-container mt-4 px-4 py-2 bg-blue-50 rounded-lg border border-blue-200">
<p class="op-date text-sm font-medium text-blue-700">
登记日期{{ currentStatus.opDate }}
</p>
</div>
<p v-html="currentStatus.description" class="status-description mt-3 text-sm text-gray-600"></p>
</div>
</div>
</template>
<!-- <div v-if="currentStatus.opDate" class="op-date-container mt-4 px-4 py-2 bg-blue-50 rounded-lg border border-blue-200">
<p class="op-date text-sm font-medium text-blue-700">
登记日期{{ currentStatus.opDate }}
</p>
</div> -->
<style lang="scss" scoped>
.status-info {

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