This commit is contained in:
2025-11-27 13:19:45 +08:00
commit c85b46c18e
612 changed files with 83497 additions and 0 deletions

View File

@@ -0,0 +1,263 @@
<template>
<van-popup v-model:show="show" destroy-on-close round position="bottom">
<div
class="h-12 flex items-center justify-center font-semibold"
style="background-color: var(--van-theme-primary-light); color: var(--van-theme-primary);"
>
成为代理
</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.region"
is-link
readonly
label="地区"
placeholder="请选择地区"
@click="showCascader = true"
/>
<van-popup v-model:show="showCascader" round position="bottom">
<van-cascader
v-model="cascaderValue"
title="请选择所在地区"
:options="options"
@close="showCascader = false"
@finish="onFinish"
/>
</van-popup>
<van-field
label-width="56"
v-model="form.mobile"
label="手机号"
name="mobile"
placeholder="请输入手机号"
:readonly="isSelf"
:disabled="isSelf"
/>
<!-- 获取验证码按钮 -->
<div class="flex items-center justify-between">
<van-field
label-width="56"
v-model="form.code"
label="验证码"
name="code"
placeholder="请输入验证码"
/>
<button
class="px-2 py-1 text-sm font-bold flex-shrink-0 rounded-lg transition duration-300"
:class="
isCountingDown || !isPhoneNumberValid
? 'cursor-not-allowed bg-gray-300 text-gray-500'
: 'text-white hover:opacity-90'
"
:style="isCountingDown || !isPhoneNumberValid
? ''
: 'background-color: var(--van-theme-primary);'"
@click="getSmsCode"
:disabled="isCountingDown || !isPhoneNumberValid"
>
{{
isCountingDown ? `${countdown}s重新获取` : "获取验证码"
}}
</button>
</div>
<!-- 同意条款的复选框 -->
<div class="p-4">
<div class="flex items-start">
<van-checkbox
v-model="isAgreed"
name="agree"
icon-size="16px"
class="flex-shrink-0 mr-2"
>
</van-checkbox>
<div class="text-xs leading-tight" style="color: var(--van-text-color-2);">
我已阅读并同意
<a
class="cursor-pointer hover:underline"
style="color: var(--van-theme-primary);"
@click="toUserAgreement"
>用户协议</a
><a
class="cursor-pointer hover:underline"
style="color: var(--van-theme-primary);"
@click="toServiceAgreement"
>信息技术服务合同</a
><a
class="cursor-pointer hover:underline"
style="color: var(--van-theme-primary);"
@click="toAgentManageAgreement"
>推广方管理制度协议</a
>
<div class="text-xs mt-1" style="color: var(--van-text-color-2);">
点击勾选即代表您同意上述法律文书的相关条款并签署上述法律文书
</div>
<div class="text-xs mt-1" style="color: var(--van-text-color-2);">
手机号未在本平台注册账号则申请后将自动生成账号
</div>
</div>
</div>
</div>
<div class="mt-4">
<van-button type="primary" round block @click="submit"
>提交申请</van-button
>
</div>
<div class="mt-2">
<van-button type="default" round block @click="closePopup"
>取消</van-button
>
</div>
</div>
</van-popup>
</template>
<script setup>
const router = useRouter();
const show = defineModel("show");
import { useCascaderAreaData } from "@vant/area-data";
import { showToast } from "vant"; // 引入 showToast 方法
const emit = defineEmits(); // 确保 emit 可以正确使用
const props = defineProps({
ancestor: {
type: String,
required: true,
},
isSelf: {
type: Boolean,
default: false,
},
userName: {
type: String,
default: "",
},
});
const { ancestor, isSelf, userName } = toRefs(props);
const form = ref({
region: "",
mobile: "",
code: "", // 增加验证码字段
});
const showCascader = ref(false);
const cascaderValue = ref("");
const options = useCascaderAreaData();
const loadingSms = ref(false); // 控制验证码按钮的loading状态
const isCountingDown = ref(false);
const isAgreed = ref(false);
const countdown = ref(60);
const onFinish = ({ selectedOptions }) => {
showCascader.value = false;
form.value.region = selectedOptions.map((option) => option.text).join("/");
};
const isPhoneNumberValid = computed(() => {
return /^1[3-9]\d{9}$/.test(form.value.mobile);
});
const getSmsCode = async () => {
if (!form.value.mobile) {
showToast({ message: "请输入手机号" });
return;
}
if (!isPhoneNumberValid.value) {
showToast({ message: "手机号格式不正确" });
return;
}
loadingSms.value = true;
const { data, error } = await useApiFetch("auth/sendSms")
.post({ mobile: form.value.mobile, actionType: "agentApply" })
.json();
loadingSms.value = false;
if (data.value && !error.value) {
if (data.value.code === 200) {
showToast({ message: "获取成功" });
startCountdown(); // 启动倒计时
} else {
showToast(data.value.msg);
}
}
};
let timer = null;
function startCountdown() {
isCountingDown.value = true;
countdown.value = 60;
timer = setInterval(() => {
if (countdown.value > 0) {
countdown.value--;
} else {
clearInterval(timer);
isCountingDown.value = false;
}
}, 1000);
}
onUnmounted(() => {
if (timer) {
clearInterval(timer);
}
});
const submit = () => {
// 校验表单字段
if (!form.value.region) {
showToast({ message: "请选择地区" });
return;
}
if (!form.value.mobile) {
showToast({ message: "请输入手机号" });
return;
}
if (!isPhoneNumberValid.value) {
showToast({ message: "手机号格式不正确" });
return;
}
// 如果不是自己申请,则需要验证码
if (!isSelf.value && !form.value.code) {
showToast({ message: "请输入验证码" });
return;
}
if (!isAgreed.value) {
showToast({ message: "请先阅读并同意用户协议及相关条款" });
return;
}
console.log("form", form.value);
// 触发父组件提交申请
emit("submit", form.value);
};
const maskName = computed(() => {
return (name) => {
return name.substring(0, 3) + "****" + name.substring(7);
};
});
const closePopup = () => {
emit("close");
};
const toUserAgreement = () => {
router.push({ name: "userAgreement" });
};
const toServiceAgreement = () => {
router.push({ name: "agentSerivceAgreement" });
};
const toAgentManageAgreement = () => {
router.push({ name: "agentManageAgreement" });
};
// 如果是自己申请,则预填并锁定手机号
onMounted(() => {
if (isSelf.value && userName.value) {
form.value.mobile = userName.value;
}
});
</script>

View File

@@ -0,0 +1,177 @@
<template>
<div class=" bg-gray-100 flex flex-col p-4">
<!-- 标题 -->
<div class="text-center text-2xl font-bold mb-4">授权书</div>
<!-- 授权书滚动区域 -->
<div class="card flex-1 overflow-y-auto" ref="agreementBox" @scroll="handleScroll">
<p class="my-2">{{ companyName }}</p>
<p class="indent-[2em]">
本人<span class="font-bold">
{{ signature ? props.name : "____________" }}</span>
拟向贵司申请大数据分析报告查询业务贵司需要了解本人相关状况用于查询大数据分析报告因此本人同意向贵司提供本人的姓名和手机号等个人信息并同意贵司向第三方传送上述信息第三方将使用上述信息核实信息真实情况查询信用记录并生成报告
</p>
<p class="mt-2 font-bold">授权内容如下</p>
<ol class="list-decimal pl-6">
<li>
贵司向依法成立的第三方服务商根据本人提交的信息进行核实并有权通过前述第三方服务机构查询使用本人的身份信息设备信息运营商信息等查询本人信息包括但不限于学历婚姻资产状况及对信息主体产生负面影响的不良信息出具相关报告
</li>
<li>
依法成立的第三方服务商查询或核实搜集保存处理共享使用含合法业务应用本人相关数据且不再另行告知本人但法律法规监管政策禁止的除外
</li>
<!-- <li>本人授权本业务推广方 可浏览本人大数据报告</li> -->
<li>
本人授权有效期为自授权之日起
1个月本授权为不可撤销授权但法律法规另有规定的除外
</li>
</ol>
<p class="mt-2 font-bold">用户声明与承诺</p>
<ul class="list-decimal pl-6">
<li>
本人在授权签署前已通过实名认证及动态验证码验证或其他身份验证手段确认本授权行为为本人真实意思表示平台已履行身份验证义务
</li>
<li>
本人在此声明已充分理解上述授权条款含义知晓并自愿承担因授权数据使用可能带来的后果包括但不限于影响个人信用评分生活行为等本人确认授权范围内的相关信息由本人提供并真实有效
</li>
<li>
若用户冒名签署或提供虚假信息由用户自行承担全部法律责任平台不承担任何后果
</li>
</ul>
<p class="mt-2 font-bold">特别提示</p>
<ul class="list-decimal pl-6">
<li>
本产品所有数据均来自第三方可能部分数据未公开数据更新延迟或信息受到限制贵司不对数据的准确性真实性完整性做任何承诺用户需根据实际情况结合报告内容自行判断与决策
</li>
<li>
本产品仅供用户本人查询或被授权查询除非用户取得合法授权用户不得利用本产品查询他人信息用户因未获得合法授权而擅自查询他人信息所产生的任何后果由用户自行承担责任
</li>
<li>
本授权书涉及对本人敏感信息包括但不限于婚姻状态资产状况等的查询与使用本人已充分知晓相关信息的敏感性并明确同意贵司及其合作方依据授权范围使用相关信息
</li>
<li>
平台声明本授权书涉及的信息核实及查询结果由第三方服务商提供平台不对数据的准确性完整性实时性承担责任用户根据报告所作决策的风险由用户自行承担平台对此不承担法律责任
</li>
<li>
本授权书中涉及的数据查询和报告生成由依法成立的第三方服务商提供若因第三方行为导致数据错误或损失用户应向第三方主张权利平台不承担相关责任
</li>
</ul>
<p class="mt-2 font-bold">附加说明</p>
<ul class="list-decimal pl-6">
<li>
本人在授权的相关数据将依据法律法规及贵司内部数据管理规范妥善存储存储期限为法律要求的最短必要时间超过存储期限或在数据使用目的达成后贵司将对相关数据进行销毁或匿名化处理
</li>
<li>
本人有权随时撤回本授权书中的授权但撤回前的授权行为及其法律后果仍具有法律效力若需撤回授权本人可通过贵司官方渠道提交书面申请贵司将在收到申请后依法停止对本人数据的使用
</li>
<li>
你通过一查查自愿支付相应费用用于购买{{ companyName
}}的大数据报告产品如若对产品内容存在异议可通过邮箱admin@iieeii.com或APP联系客服按钮进行反馈贵司将在收到异议之日起20日内进行核查和处理并将结果答复
</li>
<li>
你向{{ companyName }}的支付方式为{{ companyName }}及其经官方授权的相关企业的支付宝账户
</li>
</ul>
<p class="mt-2 font-bold">争议解决机制</p>
<ul>
<li>
若因本授权书引发争议双方应友好协商解决协商不成的双方同意将争议提交至授权书签署地海南省有管辖权的人民法院解决
</li>
</ul>
<p class="mt-2 font-bold">签署方式的法律效力声明</p>
<ul>
<li>
本授权书通过用户在线勾选电子签名或其他网络签署方式完成与手写签名具有同等法律效力平台已通过技术手段保存签署过程的完整记录作为用户真实意思表示的证据
</li>
</ul>
<p class="mt-2">本授权书于 {{ signTime }}生效</p>
<p class="mt-4 font-bold">
签署人<span class="underline">{{
signature ? props.name : "____________"
}}</span>
<br />
手机号码<span class="underline">
{{ signature ? props.mobile : "____________" }}
</span>
<br />
签署时间<span class="underline">{{ signTime }}</span>
</p>
</div>
<!-- 操作按钮 -->
<div class="mt-4 flex justify-between">
<button class="flex-shrink-0 bg-red-500 text-white px-4 py-2 rounded-lg" @click="cancel">
取消
</button>
<div class="mt-2 px-2 text-center text-sm text-gray-500">
{{ scrollMessage }}
</div>
<button class="flex-shrink-0 bg-blue-500 text-white px-4 py-2 rounded-lg active:bg-blue-600" :class="!canAgree &&
'bg-gray-300 cursor-not-allowed active:bg-gray-300'
" :disabled="!canAgree" @click="agree">
{{ signature ? "同意" : "签署" }}
</button>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
const companyName = import.meta.env.VITE_COMPANY_NAME
const emit = defineEmits(['agreed', 'cancel']); // 定义事件
const props = defineProps({
name: {
type: String,
required: true,
},
idCard: {
type: String,
required: true,
},
mobile: {
type: String,
required: true,
},
})
const signature = ref(false);
const formatDate = (date) => {
const options = { year: "numeric", month: "long", day: "numeric" };
return new Intl.DateTimeFormat("zh-CN", options).format(date);
};
const signTime = ref(formatDate(new Date()));
const canAgree = ref(false); // 同意按钮状态
const scrollMessage = ref("请滑动并阅读完整授权书以继续");
// 滚动事件处理
let timeout = null;
const handleScroll = (event) => {
clearTimeout(timeout);
timeout = setTimeout(() => {
const element = event.target;
if (
Math.abs(
element.scrollHeight - element.scrollTop - element.clientHeight
) <= 50
) {
canAgree.value = true;
scrollMessage.value = "您已阅读完整授权书,可以继续";
}
}, 100);
};
// 用户同意
const agree = () => {
if (signature.value) {
emit("agreed")
return
}
signature.value = true
};
// 用户取消
const cancel = () => {
emit("cancel")
};
</script>
<style scoped></style>

View File

@@ -0,0 +1,940 @@
<script setup>
import ShareReportButton from "./ShareReportButton.vue";
import TitleBanner from "./TitleBanner.vue";
import VerificationCard from "./VerificationCard.vue";
import StyledTabs from "./StyledTabs.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';
// 动态导入产品背景图片的函数
const loadProductBackground = async (productType) => {
try {
switch (productType) {
case 'companyinfo':
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':
return (await import("@/assets/images/report/grdsj_inquire_bg.png")).default;
case 'marriage':
return (await import("@/assets/images/report/marriage_inquire_bg.png")).default;
case 'homeservice':
return (await import("@/assets/images/report/homeservice_inquire_bg.png")).default;
case 'backgroundcheck':
return (await import("@/assets/images/report/backgroundcheck_inquire_bg.png")).default;
case 'consumerFinanceReport':
return (await import("@/assets/images/report/xjbg_inquire_bg.png")).default;
default:
return null;
}
} catch (error) {
console.warn(`Failed to load background image for ${productType}:`, error);
return null;
}
};
const props = defineProps({
isShare: {
type: Boolean,
default: false,
},
orderId: {
type: String,
required: false,
default: "",
},
orderNo: {
type: String,
default: "",
},
feature: {
type: String,
required: true,
},
reportData: {
type: Array,
required: true,
},
reportParams: {
type: Object,
required: true,
},
reportName: {
type: String,
required: true,
},
reportDateTime: {
type: [String, null],
required: false,
default: null,
},
isEmpty: {
type: Boolean,
required: true,
},
isDone: {
type: Boolean,
required: true,
},
isExample: {
type: Boolean,
default: false,
},
});
// 使用toRefs将props转换为组件内的ref
const {
feature,
reportData,
reportParams,
reportName,
reportDateTime,
isEmpty,
isDone,
isExample,
} = toRefs(props);
const active = ref(null);
const backgroundContainerRef = ref(null); // 背景容器的引用
const reportScore = ref(0); // 默认分数
const productBackground = ref('');
const backgroundHeight = ref(0);
const imageAspectRatio = ref(0); // 缓存图片宽高比
const MAX_BACKGROUND_HEIGHT = 211; // 最大背景高度,防止图片过高变形
const trapezoidBgImage = ref(''); // 牌匾背景图片
// 计算背景高度
const calculateBackgroundHeight = () => {
if (imageAspectRatio.value > 0) {
// 获取容器的实际宽度,而不是整个窗口宽度
const containerWidth = backgroundContainerRef.value
? backgroundContainerRef.value.offsetWidth
: window.innerWidth;
const calculatedHeight = containerWidth * imageAspectRatio.value;
// 限制最大高度,防止图片过高
backgroundHeight.value = Math.min(calculatedHeight, MAX_BACKGROUND_HEIGHT);
}
};
// 加载背景图片并计算高度
const loadBackgroundImage = async () => {
const background = await loadProductBackground(feature.value);
productBackground.value = background || '';
// 加载图片后计算高度
if (background) {
const img = new Image();
img.onload = () => {
// 缓存图片宽高比
imageAspectRatio.value = img.height / img.width;
// 图片加载完成后,等待下一帧再计算高度,确保容器已渲染
nextTick(() => {
calculateBackgroundHeight();
});
};
img.src = background;
}
};
// 防抖定时器
let resizeTimer = null;
// 在组件挂载时加载背景图
onMounted(async () => {
await loadBackgroundImage();
await loadTrapezoidBackground();
// 监听窗口大小变化,重新计算高度
window.addEventListener('resize', handleResize);
});
// 处理窗口大小变化(带防抖)
const handleResize = () => {
if (resizeTimer) {
clearTimeout(resizeTimer);
}
resizeTimer = setTimeout(() => {
calculateBackgroundHeight();
}, 100); // 100ms 防抖延迟
};
// 组件卸载时移除监听器
onUnmounted(() => {
if (resizeTimer) {
clearTimeout(resizeTimer);
}
window.removeEventListener('resize', handleResize);
});
// 处理数据拆分支持DWBG8B4D、DWBG6A2C、CJRZQ5E9F和CQYGL3F8E
const processedReportData = computed(() => {
let data = reportData.value;
// 拆分DWBG8B4D数据
data = splitDWBG8B4DForTabs(data);
// 拆分DWBG6A2C数据
data = splitDWBG6A2CForTabs(data);
// 拆分JRZQ7F1A数据
data = splitJRZQ7F1AForTabs(data);
// // 拆分CJRZQ5E9F数据
// data = splitCJRZQ5E9FForTabs(data);
// 拆分CQYGL3F8E数据
data = splitCQYGL3F8EForTabs(data);
// 过滤掉在featureMap中没有对应的项
return data.filter(item => featureMap[item.data.apiID]);
});
// 获取产品背景图片
const getProductBackground = computed(() => productBackground.value);
// 背景图片容器样式
const backgroundContainerStyle = computed(() => {
if (backgroundHeight.value > 0) {
return {
height: `${backgroundHeight.value}px`,
};
}
return {
height: '180px', // 默认高度
};
});
// 背景图片样式
const backgroundImageStyle = computed(() => {
if (getProductBackground.value) {
return {
backgroundImage: `url(${getProductBackground.value})`,
backgroundSize: '100% auto', // 宽度100%,高度自动保持比例
backgroundPosition: 'center -40px', // 向上偏移20px
backgroundRepeat: 'no-repeat',
overflow: 'hidden', // 超出部分裁剪
};
}
return {};
});
// 动态加载牌匾背景图片
const loadTrapezoidBackground = async () => {
try {
const bgModule = await import("@/assets/images/report/title_inquire_bg.png");
trapezoidBgImage.value = bgModule.default;
} catch (error) {
console.warn(`Failed to load trapezoid background image:`, error);
}
};
// 牌匾背景图片样式
const trapezoidBgStyle = computed(() => {
if (trapezoidBgImage.value) {
return {
backgroundImage: `url(${trapezoidBgImage.value})`,
};
}
return {};
});
const featureMap = {
IVYZ5733: {
name: "婚姻状态",
component: defineAsyncComponent(() => import("@/ui/CIVYZ5733.vue")),
remark: '查询结果为"未婚或尚未登记结婚"时,表示婚姻登记处暂无相关的登记记录。婚姻状态信息由婚姻登记处逐级上报,可能存在数据遗漏或更新滞后。当前可查询的婚姻状态包括:未婚或尚未登记结婚、已婚、离异。如您对查询结果有疑问,请联系客服反馈。',
},
IVYZ81NC: {
name: "婚姻状态",
component: defineAsyncComponent(() => import("@/ui/CIVYZ81NC.vue")),
remark: '查询结果为"未婚或尚未登记结婚"时,表示婚姻登记处暂无相关的登记记录。婚姻状态信息由婚姻登记处逐级上报,可能存在数据遗漏或更新滞后。当前可查询的婚姻状态包括:未婚或尚未登记结婚、已婚、离异。如您对查询结果有疑问,请联系客服反馈。',
},
JRZQ0A03: {
name: "借贷申请记录",
component: defineAsyncComponent(() =>
import("@/ui/CJRZQ0A03.vue")
),
},
JRZQ8203: {
name: "借贷行为记录",
component: defineAsyncComponent(() =>
import("@/ui/CJRZQ8203.vue")
),
},
FLXG3D56: {
name: "违约失信",
component: defineAsyncComponent(() => import("@/ui/CFLXG3D56.vue")),
},
FLXG0V4B: {
name: "司法涉诉",
component: defineAsyncComponent(() =>
import("@/ui/CFLXG0V4B/index.vue")
),
},
QYGL3F8E: {
name: "人企关系加强版",
component: defineAsyncComponent(() =>
import("@/ui/CQYGL3F8E/index.vue")
),
remark: '人企关系加强版提供全面的企业关联分析,包括投资企业记录、高管任职记录和涉诉风险等多维度信息。'
},
// 人企关系加强版拆分模块
CQYGL3F8E_Investment: {
name: "投资企业记录",
component: defineAsyncComponent(() => import("@/ui/CQYGL3F8E/components/Investment.vue")),
},
CQYGL3F8E_SeniorExecutive: {
name: "高管任职记录",
component: defineAsyncComponent(() => import("@/ui/CQYGL3F8E/components/SeniorExecutive.vue")),
},
CQYGL3F8E_Lawsuit: {
name: "涉诉风险",
component: defineAsyncComponent(() => import("@/ui/CQYGL3F8E/components/Lawsuit.vue")),
},
CQYGL3F8E_InvestHistory: {
name: "对外投资历史",
component: defineAsyncComponent(() => import("@/ui/CQYGL3F8E/components/InvestHistory.vue")),
},
CQYGL3F8E_FinancingHistory: {
name: "融资历史",
component: defineAsyncComponent(() => import("@/ui/CQYGL3F8E/components/FinancingHistory.vue")),
},
CQYGL3F8E_Punishment: {
name: "行政处罚",
component: defineAsyncComponent(() => import("@/ui/CQYGL3F8E/components/Punishment.vue")),
},
CQYGL3F8E_Abnormal: {
name: "经营异常",
component: defineAsyncComponent(() => import("@/ui/CQYGL3F8E/components/Abnormal.vue")),
},
CQYGL3F8E_TaxRisk: {
name: "税务风险",
component: defineAsyncComponent(() => import("@/ui/CQYGL3F8E/components/TaxRisk/index.vue")),
},
QCXG7A2B: {
name: "名下车辆",
component: defineAsyncComponent(() => import("@/ui/CQCXG7A2B.vue")),
},
QCXG9P1C: {
name: "名下车辆",
component: defineAsyncComponent(() => import("@/ui/CQCXG9P1C.vue")),
},
BehaviorRiskScan: {
name: "风险行为扫描",
component: defineAsyncComponent(() =>
import("@/ui/CBehaviorRiskScan.vue")
),
},
JRZQ4AA8: {
name: "还款压力",
component: defineAsyncComponent(() => import("@/ui/CJRZQ4AA8.vue")),
},
IVYZ9A2B: {
name: "学历信息查询",
component: defineAsyncComponent(() => import("@/ui/CIVYZ9A2B.vue")),
},
IVYZ7F3A: {
name: "学历信息",
component: defineAsyncComponent(() => import("@/ui/CIVYZ7F3A.vue")),
},
IVYZ3P9M: {
name: "学历信息",
component: defineAsyncComponent(() => import("@/ui/IVYZ3P9M.vue")),
remark: '学历信息展示学生姓名、身份证号、学校、专业、入学与毕业时间、学历层次以及学习形式等字段,可结合字典编码了解具体含义。',
},
IVYZ8I9J: {
name: "网络社交异常",
component: defineAsyncComponent(() => import("@/ui/IVYZ8I9J.vue")),
},
DWBG8B4D: {
name: "谛听多维报告",
component: defineAsyncComponent(() => import("@/ui/CDWBG8B4D/index.vue")),
},
// 谛听多维报告拆分模块
DWBG8B4D_Overview: {
name: "报告概览",
component: defineAsyncComponent(() => import("@/ui/CDWBG8B4D/components/ReportOverview.vue")),
},
DWBG8B4D_ElementVerification: {
name: "要素核查",
component: defineAsyncComponent(() => import("@/ui/CDWBG8B4D/components/ElementVerification.vue")),
},
DWBG8B4D_Identity: {
name: "运营商核验",
component: defineAsyncComponent(() => import("@/ui/CDWBG8B4D/components/Identity.vue")),
},
DWBG8B4D_RiskWarning: {
name: "公安重点人员检验",
component: defineAsyncComponent(() => import("@/ui/CDWBG8B4D/components/RiskWarning.vue")),
},
DWBG8B4D_OverdueRisk: {
name: "逾期风险综述",
component: defineAsyncComponent(() => import("@/ui/CDWBG8B4D/components/OverdueRiskSection.vue")),
},
// DWBG8B4D_CourtInfo: {
// name: "法院曝光台信息",
// component: defineAsyncComponent(() => import("@/ui/CDWBG8B4D/components/MultCourtInfoSection.vue")),
// },
DWBG8B4D_LoanEvaluation: {
name: "借贷评估",
component: defineAsyncComponent(() => import("@/ui/CDWBG8B4D/components/LoanEvaluationSection.vue")),
},
DWBG8B4D_LeasingRisk: {
name: "租赁风险评估",
component: defineAsyncComponent(() => import("@/ui/CDWBG8B4D/components/LeasingRiskSection.vue")),
},
DWBG8B4D_RiskSupervision: {
name: "关联风险监督",
component: defineAsyncComponent(() => import("@/ui/CDWBG8B4D/components/RiskSupervisionSection.vue")),
},
DWBG8B4D_RiskWarningTab: {
name: "规则风险提示",
component: defineAsyncComponent(() => import("@/ui/CDWBG8B4D/components/RiskWarningTab.vue")),
},
JRZQ4B6C: {
name: "信贷表现",
component: defineAsyncComponent(() => import("@/ui/JRZQ4B6C/index.vue")),
remark: '信贷表现主要为企业在背景调查过程中探查用户近期信贷表现时提供参考,帮助企业对其内部员工、外部业务进行个人信用过滤。数据来源于多个征信机构,可能存在数据延迟或不完整的情况。'
},
JRZQ09J8: {
name: "收入评估",
component: defineAsyncComponent(() => import("@/ui/JRZQ09J8/index.vue")),
remark: '基于全国社会保险信息系统的缴费基数数据进行收入水平评估。评级反映相对收入水平,实际收入可能因地区差异而有所不同,建议结合其他收入证明材料进行综合评估。'
},
// 司南报告
DWBG6A2C: {
name: "司南报告",
component: defineAsyncComponent(() => import("@/ui/DWBG6A2C/index.vue")),
remark: '司南报告提供全面的个人信用风险评估,包括身份核验、风险名单、借贷行为、履约情况等多维度分析。'
},
// 司南报告拆分模块
// DWBG6A2C_BaseInfo: {
// name: "基本信息",
// component: defineAsyncComponent(() => import("@/ui/DWBG6A2C/components/BaseInfoSection.vue")),
// },
DWBG6A2C_StandLiveInfo: {
name: "身份信息核验",
component: defineAsyncComponent(() => import("@/ui/DWBG6A2C/components/StandLiveInfoSection.vue")),
},
DWBG6A2C_RiskPoint: {
name: "命中风险标注",
component: defineAsyncComponent(() => import("@/ui/DWBG6A2C/components/RiskPointSection.vue")),
},
DWBG6A2C_SecurityInfo: {
name: "公安重点人员核验",
component: defineAsyncComponent(() => import("@/ui/DWBG6A2C/components/SecurityInfoSection.vue")),
},
DWBG6A2C_AntiFraudInfo: {
name: "涉赌涉诈人员核验",
component: defineAsyncComponent(() => import("@/ui/DWBG6A2C/components/AntiFraudInfoSection.vue")),
},
DWBG6A2C_RiskList: {
name: "风险名单",
component: defineAsyncComponent(() => import("@/ui/DWBG6A2C/components/RiskListSection.vue")),
},
DWBG6A2C_ApplicationStatistics: {
name: "历史借贷行为",
component: defineAsyncComponent(() => import("@/ui/DWBG6A2C/components/ApplicationStatisticsSection.vue")),
},
DWBG6A2C_LendingStatistics: {
name: "近24个月放款情况",
component: defineAsyncComponent(() => import("@/ui/DWBG6A2C/components/LendingStatisticsSection.vue")),
},
DWBG6A2C_PerformanceStatistics: {
name: "履约情况",
component: defineAsyncComponent(() => import("@/ui/DWBG6A2C/components/PerformanceStatisticsSection.vue")),
},
DWBG6A2C_OverdueRecord: {
name: "历史逾期记录",
component: defineAsyncComponent(() => import("@/ui/DWBG6A2C/components/OverdueRecordSection.vue")),
},
DWBG6A2C_CreditDetail: {
name: "授信详情",
component: defineAsyncComponent(() => import("@/ui/DWBG6A2C/components/CreditDetailSection.vue")),
},
DWBG6A2C_RentalBehavior: {
name: "租赁行为",
component: defineAsyncComponent(() => import("@/ui/DWBG6A2C/components/RentalBehaviorSection.vue")),
},
DWBG6A2C_RiskSupervision: {
name: "关联风险监督",
component: defineAsyncComponent(() => import("@/ui/DWBG6A2C/components/RiskSupervisionSection.vue")),
},
// DWBG6A2C_CourtRiskInfo: {
// name: "法院风险信息",
// component: defineAsyncComponent(() => import("@/ui/DWBG6A2C/components/CourtRiskInfoSection.vue")),
// },
// 贷款风险报告
JRZQ5E9F: {
name: "贷款风险评估",
component: defineAsyncComponent(() => import("@/ui/CJRZQ5E9F/index.vue")),
remark: '贷款风险评估提供全面的个人贷款风险分析,包括风险概览、信用评分、贷款行为分析、机构分析等多维度评估。'
},
// 贷款风险报告拆分模块
CJRZQ5E9F_RiskOverview: {
name: "风险概览",
component: defineAsyncComponent(() => import("@/ui/CJRZQ5E9F/components/RiskOverview.vue")),
},
CJRZQ5E9F_CreditScores: {
name: "信用评分",
component: defineAsyncComponent(() => import("@/ui/CJRZQ5E9F/components/CreditScores.vue")),
},
CJRZQ5E9F_LoanBehaviorAnalysis: {
name: "贷款行为分析",
component: defineAsyncComponent(() => import("@/ui/CJRZQ5E9F/components/LoanBehaviorAnalysis.vue")),
},
CJRZQ5E9F_InstitutionAnalysis: {
name: "机构分析",
component: defineAsyncComponent(() => import("@/ui/CJRZQ5E9F/components/InstitutionAnalysis.vue")),
},
CJRZQ5E9F_TimeTrendAnalysis: {
name: "时间趋势分析",
component: defineAsyncComponent(() => import("@/ui/CJRZQ5E9F/components/TimeTrendAnalysis.vue")),
},
CJRZQ5E9F_RiskIndicators: {
name: "风险指标详情",
component: defineAsyncComponent(() => import("@/ui/CJRZQ5E9F/components/RiskIndicators.vue")),
},
CJRZQ5E9F_RiskAdvice: {
name: "专业建议",
component: defineAsyncComponent(() => import("@/ui/CJRZQ5E9F/components/RiskAdvice.vue")),
},
FLXG7E8F: {
name: "司法涉诉",
component: defineAsyncComponent(() =>
import("@/ui/FLXG7E8F/index.vue")
),
remark: '司法涉诉风险展示申请人相关的诉讼情况,包括民事诉讼、刑事诉讼、行政诉讼、执行案件、失信被执行人、限制消费等。数据来源于各级法院的公开判决书和法官网等权威渠道。'
},
FLXGDEA9: {
name: "本人不良",
component: defineAsyncComponent(() => import("@/ui/CFLXGDEA9.vue")),
remark: '本人不良记录查询结果来源于公安部门等权威机构,包括各类违法犯罪前科记录。查询结果仅供参考,具体信息以相关部门官方记录为准。'
},
// 多头借贷
DWBG7F3A: {
name: "多头借贷",
component: defineAsyncComponent(() => import("@/ui/DWBG7F3A/index.vue")),
remark: '多头借贷提供全面的多头借贷风险评估,包括多头共债子分、多头申请、多头逾期、圈团风险和可疑欺诈风险等多维度分析。'
},
// 违约失信
JRZQ8A2D: {
name: "违约失信",
component: defineAsyncComponent(() => import("@/ui/JRZQ8A2D.vue")),
remark: '违约失信用于验证个人在各类金融机构和法院系统的信用状况,包括法院失信、银行风险、非银机构风险等多个维度。'
},
// 全景雷达
JRZQ7F1A: {
name: "全景雷达",
component: defineAsyncComponent(() => import("@/ui/JRZQ7F1A/index.vue")),
remark: '全景雷达提供全面的信用评估,包括申请行为详情、放款还款详情和大数据详情。通过多维度的数据分析,全面展示申请人的信用状况和借贷行为。'
},
JRZQ7F1A_ApplyReport: {
name: "申请行为详情",
component: defineAsyncComponent(() => import("@/ui/JRZQ7F1A/components/ApplyReportSection.vue")),
},
JRZQ7F1A_BehaviorReport: {
name: "放款还款详情",
component: defineAsyncComponent(() => import("@/ui/JRZQ7F1A/components/BehaviorReportSection.vue")),
},
JRZQ7F1A_BigDataReport: {
name: "大数据详情",
component: defineAsyncComponent(() => import("@/ui/JRZQ7F1A/components/BigDataReportSection.vue")),
},
// 手机携号转网
YYSY7D3E: {
name: "手机携号转网",
component: defineAsyncComponent(() => import("@/ui/YYSY7D3E/index.vue")),
remark: '手机携号转网查询用于检测用户手机号码是否发生过携号转网操作,以及转网前后的运营商信息。携号转网可能影响用户身份验证和信用评估。'
},
// 手机在网时长
YYSY8B1C: {
name: "手机在网时长",
component: defineAsyncComponent(() => import("@/ui/YYSY8B1C/index.vue")),
remark: '手机在网时长查询用于检测用户手机号码的在网使用时长。在网时长越长,通常表示用户身份越稳定,信用风险越低。需要注意的是,如果手机号码存在携号转网的情况,那么在网时长会从转网的时候重新计算,转网前的在网时长不计入当前在网时长。建议结合手机携号转网查询结果进行综合评估。'
},
};
const maskValue = computed(() => {
return (type, value) => {
if (!value) return value;
if (type === "name") {
// 姓名脱敏(保留首位)
if (value.length === 1) {
return "*"; // 只保留一个字,返回 "*"
} else if (value.length === 2) {
return value[0] + "*"; // 两个字,保留姓氏,第二个字用 "*" 替代
} else {
return (
value[0] +
"*".repeat(value.length - 2) +
value[value.length - 1]
); // 两个字以上,保留第一个和最后一个字,其余的用 "*" 替代
}
} else if (type === "id_card") {
// 身份证号脱敏保留前6位和最后4位
return value.replace(/^(.{6})(?:\d+)(.{4})$/, "$1****$2");
} else if (type === "mobile") {
if (value.length === 11) {
return value.substring(0, 3) + "****" + value.substring(7);
}
return value; // 如果手机号不合法或长度不为 11 位,直接返回原手机号
} else if (type === "bank_card") {
// 银行卡号脱敏保留前6位和后4位
return value.replace(/^(.{6})(?:\d+)(.{4})$/, "$1****$2");
} else if (type === "ent_name") {
// 企业名称脱敏保留前3个字符和后3个字符中间部分用 "*" 替代)
if (value.length <= 6) {
return value[0] + "*".repeat(value.length - 1); // 少于6个字符时只保留第一个字符其他用 * 替代
} else {
return (
value.slice(0, 3) +
"*".repeat(value.length - 6) +
value.slice(-3)
); // 多于6个字符时保留前3和后3
}
} else if (type === "ent_code") {
// 企业代码脱敏保留前4个字符和后4个字符中间部分用 "*" 替代)
if (value.length <= 8) {
return value.slice(0, 4) + "*".repeat(value.length - 4); // 长度不超过8时保留前4个字符其他用 * 替代
} else {
return (
value.slice(0, 4) +
"*".repeat(value.length - 8) +
value.slice(-4)
); // 长度超过8时保留前4个字符和后4个字符
}
} else if (type === "car_license") {
// 车牌号脱敏保留前2个字符后2个字符其他部分用 "*" 替代)
if (value.length <= 4) {
return value[0] + "*".repeat(value.length - 1); // 如果车牌号长度小于等于4只保留首字符
} else {
// 如果车牌号较长保留前2个字符后2个字符其余部分用 "*" 替代
return (
value.slice(0, 2) +
"*".repeat(value.length - 4) +
value.slice(-2)
);
}
}
return value;
};
});
// ==================== 新评分系统 ====================
// Feature 风险等级配置(权重越高表示风险越大,最终分数越高越安全)
const featureRiskLevels = {
// 🔴 高风险类 - 权重 10
'FLXG0V4B': 20, // 司法涉诉
'FLXG7E8F': 20, // 个人涉诉
'FLXG3D56': 10, // 违约失信
'FLXGDEA9': 18, // 本人不良
'JRZQ4AA8': 10, // 还款压力
// 🟠 中高风险类 - 权重 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, // 多头借贷
// 🟡 中风险类 - 权重 5
'QYGL3F8E': 5, // 人企关系加强版
'QCXG7A2B': 5, // 名下车辆
'JRZQ09J8': 5, // 收入评估
// 🔵 低风险类 - 权重 3
'IVYZ5733': 3, // 婚姻状态
'IVYZ9A2B': 3, // 学历信息
'IVYZ3P9M': 3, // 学历信息查询(实时版)
// 📊 复合报告类 - 按子模块动态计算
'DWBG8B4D': 0, // 谛听多维报告(由子模块计算)
'DWBG6A2C': 0, // 司南报告(由子模块计算)
'JRZQ5E9F': 0, // 贷款风险评估(由子模块计算)
// 谛听多维报告子模块
'DWBG8B4D_Overview': 10,
'DWBG8B4D_ElementVerification': 4,
'DWBG8B4D_Identity': 4,
'DWBG8B4D_RiskWarning': 10,
'DWBG8B4D_OverdueRisk': 9,
'DWBG8B4D_LoanEvaluation': 7,
'DWBG8B4D_LeasingRisk': 6,
'DWBG8B4D_RiskSupervision': 8,
'DWBG8B4D_RiskWarningTab': 9,
// 司南报告子模块
'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,
// 贷款风险评估子模块
'CJRZQ5E9F_RiskOverview': 8,
'CJRZQ5E9F_CreditScores': 7,
'CJRZQ5E9F_LoanBehaviorAnalysis': 7,
'CJRZQ5E9F_InstitutionAnalysis': 5,
'CJRZQ5E9F_TimeTrendAnalysis': 6,
'CJRZQ5E9F_RiskIndicators': 8,
'CJRZQ5E9F_RiskAdvice': 2,
// 人企关系加强版子模块
'CQYGL3F8E_Investment': 4,
'CQYGL3F8E_SeniorExecutive': 4,
'CQYGL3F8E_Lawsuit': 8,
'CQYGL3F8E_InvestHistory': 3,
'CQYGL3F8E_FinancingHistory': 3,
'CQYGL3F8E_Punishment': 7,
'CQYGL3F8E_Abnormal': 6,
'CQYGL3F8E_TaxRisk': 7,
};
// 存储每个组件的 ref 引用
const componentRefs = ref({});
// 存储每个组件的风险评分(由组件主动通知)
const componentRiskScores = ref({});
// 提供方法让子组件通知自己的风险评分0-100分分数越高越安全
const notifyRiskStatus = (apiID, index, riskScore) => {
const key = `${apiID}_${index}`;
componentRiskScores.value[key] = riskScore;
};
// 暴露给子组件
defineExpose({
notifyRiskStatus
});
// 计算综合评分的函数(分数越高越安全)
const calculateScore = () => {
// 收集实际存在的 features 及其风险权重
const presentFeatures = [];
processedReportData.value.forEach((item, index) => {
const apiID = item.data?.apiID;
if (!apiID) return;
// 获取风险权重(如果不在配置中,默认为 3
const weight = featureRiskLevels[apiID] ?? 3;
// 跳过权重为 0 的复合报告主模块(它们由子模块计算)
if (weight === 0) return;
presentFeatures.push({
apiID,
index,
weight
});
});
if (presentFeatures.length === 0) return 100; // 无有效特征时返回满分(最安全)
// 累计总风险分数
let totalRiskScore = 0;
const riskDetails = []; // 用于调试
presentFeatures.forEach(({ apiID, index, weight }) => {
// 从组件风险评分中获取评分0-100分分数越高越安全
const key = `${apiID}_${index}`;
const componentScore = componentRiskScores.value[key] ?? 100; // 默认100分最安全
// 将组件评分转换为风险分数0-100 -> 100-0
const componentRisk = 100 - componentScore;
// 计算该模块的风险贡献(固定分值,不按占比)
// 使用权重系数放大高风险模块的影响
// 高风险模块权重10如果风险分数是0扣20分权重10 × 系数2
// 中风险模块权重7如果风险分数是0扣14分权重7 × 系数2
// 低风险模块权重3如果风险分数是0扣6分权重3 × 系数2
const weightMultiplier = 1.5; // 权重系数,可以调整这个值来控制影响程度
const riskContribution = (componentRisk / 100) * weight * weightMultiplier;
riskDetails.push({
apiID,
index,
weight,
componentScore,
componentRisk,
riskContribution,
hasStatus: key in componentRiskScores.value
});
// 累加风险分数
totalRiskScore += riskContribution;
});
// 将总风险分数限制在 0-90 范围内确保最低分为10分
const finalRiskScore = Math.max(0, Math.min(90, Math.round(totalRiskScore)));
// 转换为安全分数分数越高越安全100 - 风险分数)
// 最终分数范围10-100分
const safetyScore = 100 - finalRiskScore;
return safetyScore;
};
// 监听 reportData 和 componentRiskScores 变化并计算评分
watch([reportData, componentRiskScores], () => {
reportScore.value = calculateScore();
// 将评分系统数据整理到一个对象中
const scoreData = {
timestamp: new Date().toISOString(),
finalScore: reportScore.value,
reportModules: processedReportData.value.map((item, index) => ({
apiID: item.data.apiID,
name: featureMap[item.data.apiID]?.name || '未知',
index: index,
riskScore: componentRiskScores.value[`${item.data.apiID}_${index}`] ?? '未上报',
weight: featureRiskLevels[item.data.apiID] ?? 0
})),
componentScores: componentRiskScores.value,
riskLevels: featureRiskLevels
};
}, { immediate: true, deep: true });
</script>
<template>
<div class="min-h-full bg-[#D6E6FF]">
<template v-if="isDone">
<!-- 背景图容器 -->
<div ref="backgroundContainerRef" class="w-full" :style="[backgroundContainerStyle, backgroundImageStyle]">
</div>
<!-- Tabs 区域 -->
<StyledTabs v-model:active="active" scrollspy sticky :offset-top="46">
<div class="flex flex-col gap-y-4 p-4">
<LEmpty v-if="isEmpty" />
<van-tab title="分析指数">
<div class="relative mb-4">
<!-- 产品卡片牌匾效果 - 使用背景图片 -->
<div class="absolute -top-[12px] left-1/2 transform -translate-x-1/2 w-[140px]">
<div class="trapezoid-bg-image flex items-center justify-center"
:style="trapezoidBgStyle">
<div class="text-xl whitespace-nowrap text-white">分析指数</div>
</div>
</div>
<div class="card mb-4 mt-6 !pt-10">
<div class="my-4">
<GaugeChart :score="reportScore" />
</div>
</div>
</div>
</van-tab>
<van-tab title="基本信息">
<TitleBanner id="basic" class="mb-4">基本信息</TitleBanner>
<VerificationCard :report-params="reportParams" :report-date-time="reportDateTime"
:report-name="reportName" :is-empty="isEmpty" :is-share="isShare" :order-id="orderId"
:order-no="orderNo" :isExample="isExample" />
<LRemark content="如查询的姓名/身份证与运营商提供的不一致,可能会存在报告内容不匹配的情况" />
</van-tab>
<van-tab v-for="(item, index) in processedReportData" :key="`${item.data.apiID}_${index}`"
:title="featureMap[item.data.apiID]?.name">
<TitleBanner :id="item.data.apiID" class="mb-4">
{{ featureMap[item.data.apiID]?.name }}
</TitleBanner>
<component :is="featureMap[item.data.apiID]?.component" :ref="el => {
if (el) {
const refKey = `${item.data.apiID}_${index}`;
componentRefs[refKey] = el;
}
}" :data="item.data.data" :params="reportParams" :api-id="item.data.apiID" :index="index"
:notify-risk-status="notifyRiskStatus">
</component>
<LRemark v-if="featureMap[item.data.apiID]?.remark"
:content="featureMap[item.data.apiID]?.remark" />
</van-tab>
<ShareReportButton v-if="!isShare" class="h-12 text-3xl mt-8" :order-id="orderId"
:order-no="orderNo" :isExample="isExample" />
<span class="mb-4 text-center text-sm text-gray-500">分享当前{{ isExample ? '示例' : '报告' }}链接</span>
<div class="card">
<div>
<div class="text-bold text-center text-[#333333] mb-2">
免责声明
</div>
<p class="text-[#999999]">
&nbsp; &nbsp;
1本份报告是在取得您个人授权后我们才向合法存有您以上个人信息的机构去调取相关内容我们不会以任何形式对您的报告进行存储除您和您授权的人外不会提供给任何人和机构进行查看
</p>
<p class="text-[#999999]">
&nbsp; &nbsp; 2本报告自生成之日起有效期 30
过期自动删除如果您对本份报告存有异议可能是合作机构数据有延迟或未能获取到您的相关数据出于合作平台数据隐私的保护本平台将不做任何解释
</p>
<p class="text-[#999999]">
&nbsp; &nbsp; 3若以上数据有错误请联系平台客服
</p>
</div>
</div>
</div>
</StyledTabs>
</template>
</div>
<div class="disclaimer">
<div class="flex flex-col 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>
<a class="text-blue-500" href="https://beian.miit.gov.cn">
琼ICP备2024048057号-2
</a>
</div>
</div>
<div>海南省学宇思网络科技有限公司版权所有</div>
</div>
</template>
<style lang="scss" scoped>
.a {
color: #e03131;
}
.disclaimer {
/* margin-top: 24px; */
padding: 10px;
font-size: 12px;
color: #999;
text-align: center;
border-top: 1px solid #e0e0e0;
padding-bottom: 60px;
background: #ffffff;
}
:deep(.card) {
@apply p-3;
box-shadow: 0px 0px 24px 0px #3F3F3F0F;
}
/* 梯形背景图片样式 */
.trapezoid-bg-image {
background-size: contain;
background-repeat: no-repeat;
background-position: center;
height: 44px;
}
</style>

View File

@@ -0,0 +1,266 @@
<script setup>
import { ref, computed, nextTick } from "vue";
import { useDialogStore } from "@/stores/dialogStore";
const emit = defineEmits(['bind-success'])
const router = useRouter();
const dialogStore = useDialogStore();
const agentStore = useAgentStore();
const userStore = useUserStore();
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;
}
const { data, error } = await useApiFetch("auth/sendSms")
.post({ mobile: phoneNumber.value, actionType: "bindMobile" })
.json();
if (data.value && !error.value) {
if (data.value.code === 200) {
showToast({ message: "获取成功" });
startCountdown();
// 聚焦到验证码输入框
nextTick(() => {
const verificationCodeInput = document.getElementById('verificationCode');
if (verificationCodeInput) {
verificationCodeInput.focus();
}
});
} else {
showToast(data.value.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="verificationCode" 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>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,521 @@
<template>
<div v-if="visible" class="captcha-overlay">
<div class="captcha-modal">
<div class="captcha-header">
<h3 class="captcha-title">安全验证</h3>
<button class="close-btn" @click="handleClose">×</button>
</div>
<div class="captcha-content">
<canvas ref="canvasRef" :width="canvasWidth" :height="canvasHeight" class="captcha-canvas"
@click="handleCanvasClick"></canvas>
<div class="captcha-instruction">
<p>
请依次点击 <span class="target-list">{{ targetChars.join('、') }}</span>
</p>
</div>
<div class="captcha-status">
<p v-if="errorMessage" class="error-message">{{ errorMessage }}</p>
<p v-else-if="successMessage" class="success-message">{{ successMessage }}</p>
<p v-else class="status-text">点击图片中的目标文字</p>
</div>
</div>
<div class="captcha-footer">
<button class="refresh-btn" @click="refreshCaptcha" :disabled="isRefreshing">
{{ isRefreshing ? '刷新中...' : '刷新验证' }}
</button>
<button class="confirm-btn" :disabled="clickedList.length < 3 || !!successMessage" @click="handleConfirm">
确认
</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch, nextTick } from 'vue'
const props = defineProps({
visible: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(['success', 'close'])
const canvasRef = ref(null)
const canvasWidth = 300
const canvasHeight = 180
const bgImgUrl = '/image/clickCaptcha.jpg' // 可替换为任意背景图
const allChars = ['大', '数', '据', '全', '能', '查', '风', '险', '报', '告']
const targetChars = ref(['全', '能', '查']) // 目标点击顺序固定
const charPositions = ref([]) // [{char, x, y, w, h}]
const clickedIndex = ref(0)
const errorMessage = ref('')
const successMessage = ref('')
const isRefreshing = ref(false)
const clickedList = ref([]) // [{char, idx, ...}]
let currentChars = [] // 当前乱序后的字顺序
function randomChars(count, except = []) {
const pool = allChars.filter(c => !except.includes(c))
const arr = []
while (arr.length < count) {
const c = pool[Math.floor(Math.random() * pool.length)]
if (!arr.includes(c)) arr.push(c)
}
return arr
}
function randomColor() {
return (
'#' +
Math.floor(Math.random() * 0xffffff)
.toString(16)
.padStart(6, '0')
)
}
function drawCaptcha() {
const canvas = canvasRef.value
if (!canvas) return
const ctx = canvas.getContext('2d')
ctx.clearRect(0, 0, canvasWidth, canvasHeight)
// 绘制背景图
const bg = new window.Image()
bg.src = bgImgUrl
bg.onload = () => {
ctx.drawImage(bg, 0, 0, canvasWidth, canvasHeight)
// 绘制乱序文字
charPositions.value.forEach(pos => {
ctx.save()
ctx.translate(pos.x + pos.w / 2, pos.y + pos.h / 2)
ctx.rotate(pos.angle)
ctx.font = 'bold 28px sans-serif'
ctx.fillStyle = pos.color
ctx.shadowColor = '#333'
ctx.shadowBlur = 4
ctx.fillText(pos.char, -pos.w / 2, pos.h / 2 - 8)
ctx.restore()
})
// 绘制点击顺序标签
clickedList.value.forEach((item, i) => {
const pos = charPositions.value[item.idx]
if (!pos) return
ctx.save()
ctx.beginPath()
ctx.arc(pos.x + pos.w / 2, pos.y + 6, 12, 0, 2 * Math.PI)
ctx.fillStyle = '#8CC6F7'
ctx.fill()
ctx.font = 'bold 16px sans-serif'
ctx.fillStyle = '#fff'
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText((i + 1).toString(), pos.x + pos.w / 2, pos.y + 6)
ctx.restore()
})
}
}
function refreshCaptcha() {
isRefreshing.value = true
setTimeout(() => {
generateCaptcha()
isRefreshing.value = false
}, 500)
}
function generateCaptcha() {
// 乱序排列7个字
const chars = [...allChars]
for (let i = chars.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
;[chars[i], chars[j]] = [chars[j], chars[i]]
}
currentChars = chars
targetChars.value = ['全', '能', '查']
clickedIndex.value = 0
errorMessage.value = ''
successMessage.value = ''
clickedList.value = []
// 生成每个字的坐标、角度、颜色
charPositions.value = []
const used = []
chars.forEach((char, idx) => {
let x,
y,
w = 36,
h = 36,
tryCount = 0
do {
x = Math.random() * (canvasWidth - w - 10) + 5
y = Math.random() * (canvasHeight - h - 10) + 5
tryCount++
} while (used.some(pos => Math.abs(pos.x - x) < w && Math.abs(pos.y - y) < h) && tryCount < 20)
used.push({ x, y })
const angle = (Math.random() - 0.5) * 0.7
const color = randomColor()
charPositions.value.push({ char, x, y, w, h, idx, angle, color })
})
nextTick(drawCaptcha)
}
function handleCanvasClick(e) {
if (successMessage.value) return
// 适配缩放
const rect = canvasRef.value.getBoundingClientRect()
const scaleX = canvasWidth / rect.width
const scaleY = canvasHeight / rect.height
const x = (e.clientX - rect.left) * scaleX
const y = (e.clientY - rect.top) * scaleY
// 找到被点中的字
const posIdx = charPositions.value.findIndex(
pos => x >= pos.x && x <= pos.x + pos.w && y >= pos.y && y <= pos.y + pos.h
)
if (posIdx === -1) {
errorMessage.value = '请点击目标文字'
setTimeout(() => (errorMessage.value = ''), 1200)
return
}
// 已经点过不能重复点
if (clickedList.value.some(item => item.idx === posIdx)) return
if (clickedList.value.length >= charPositions.value.length) return
clickedList.value.push({ char: charPositions.value[posIdx].char, idx: posIdx })
drawCaptcha()
}
function handleConfirm() {
if (clickedList.value.length < 3) {
errorMessage.value = '请依次点击3个字'
setTimeout(() => (errorMessage.value = ''), 1200)
return
}
const userSeq = clickedList.value
.slice(0, 3)
.map(item => item.char)
.join('')
if (userSeq === '一查查') {
successMessage.value = '验证成功!'
setTimeout(() => emit('success'), 600)
} else {
errorMessage.value = '校验错误,请重试'
setTimeout(() => {
errorMessage.value = ''
clickedList.value = []
// 失败时重新打乱
generateCaptcha()
}, 1200)
}
}
function handleClose() {
emit('close')
}
onMounted(() => {
if (props.visible) generateCaptcha()
})
watch(
() => props.visible,
v => {
if (v) generateCaptcha()
}
)
</script>
<style scoped>
.captcha-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
padding: 1rem;
backdrop-filter: blur(4px);
animation: fadeIn 0.2s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.captcha-modal {
background: #fff;
border-radius: 1rem;
width: 100%;
max-width: 360px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05);
overflow: hidden;
animation: slideUp 0.3s ease-out;
}
@keyframes slideUp {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.captcha-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.25rem 1.5rem;
border-bottom: 1px solid var(--color-border-primary, #ebedf0);
background: linear-gradient(135deg, var(--color-primary-light, rgba(140, 198, 247, 0.1)), #ffffff);
}
.captcha-title {
font-size: 1.125rem;
font-weight: 600;
color: var(--color-text-primary, #323233);
margin: 0;
background: linear-gradient(135deg, var(--color-primary, #8CC6F7), var(--color-primary-600, #709ec6));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.close-btn {
background: rgba(0, 0, 0, 0.05);
border: none;
font-size: 1.5rem;
color: var(--color-text-secondary, #646566);
cursor: pointer;
padding: 0.375rem;
width: 32px;
height: 32px;
border-radius: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
line-height: 1;
}
.close-btn:hover {
color: var(--color-text-primary, #323233);
background: rgba(0, 0, 0, 0.1);
transform: rotate(90deg);
}
.captcha-content {
padding: 1.5rem 1.5rem 1rem 1.5rem;
background: #ffffff;
}
.captcha-canvas {
width: 100%;
border-radius: 0.75rem;
background: var(--color-bg-tertiary, #f8f8f8);
display: block;
margin: 0 auto;
border: 2px solid var(--color-border-primary, #ebedf0);
transition: all 0.2s ease;
cursor: pointer;
}
.captcha-canvas:hover {
border-color: var(--color-primary, #8CC6F7);
box-shadow: 0 0 0 3px var(--color-primary-light, rgba(140, 198, 247, 0.1));
}
.captcha-instruction {
margin: 1.25rem 0 0.75rem 0;
text-align: center;
}
.captcha-instruction p {
font-size: 0.95rem;
color: var(--color-text-secondary, #646566);
margin: 0;
line-height: 1.5;
}
.target-list {
color: var(--color-primary, #8CC6F7);
font-weight: 600;
font-size: 1.05rem;
padding: 0.25rem 0.5rem;
background: var(--color-primary-light, rgba(140, 198, 247, 0.1));
border-radius: 0.375rem;
display: inline-block;
}
.captcha-status {
text-align: center;
min-height: 1.75rem;
margin-top: 0.5rem;
}
.error-message {
color: var(--color-danger, #ee0a24);
font-size: 0.875rem;
margin: 0;
padding: 0.5rem;
background: rgba(238, 10, 36, 0.1);
border-radius: 0.5rem;
animation: shake 0.3s ease-in-out;
}
@keyframes shake {
0%,
100% {
transform: translateX(0);
}
25% {
transform: translateX(-5px);
}
75% {
transform: translateX(5px);
}
}
.success-message {
color: var(--color-success, #07c160);
font-size: 0.875rem;
margin: 0;
padding: 0.5rem;
background: rgba(7, 193, 96, 0.1);
border-radius: 0.5rem;
animation: bounce 0.4s ease-out;
}
@keyframes bounce {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-3px);
}
}
.status-text {
color: var(--color-text-tertiary, #969799);
font-size: 0.875rem;
margin: 0;
}
.captcha-footer {
padding: 1.25rem 1.5rem;
border-top: 1px solid var(--color-border-primary, #ebedf0);
background: var(--color-bg-secondary, #fafafa);
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.refresh-btn {
width: 100%;
padding: 0.875rem;
background: var(--color-primary, #8CC6F7);
color: white;
border: none;
border-radius: 0.625rem;
font-size: 0.95rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(140, 198, 247, 0.3);
}
.refresh-btn:hover:not(:disabled) {
background: var(--color-primary-dark, rgba(140, 198, 247, 0.8));
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(140, 198, 247, 0.4);
}
.refresh-btn:active:not(:disabled) {
transform: translateY(0);
}
.refresh-btn:disabled {
background: var(--color-text-disabled, #c8c9cc);
cursor: not-allowed;
box-shadow: none;
}
.confirm-btn {
width: 100%;
padding: 0.875rem;
background: var(--color-success, #07c160);
color: white;
border: none;
border-radius: 0.625rem;
font-size: 0.95rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(7, 193, 96, 0.3);
}
.confirm-btn:hover:not(:disabled) {
background: rgba(7, 193, 96, 0.9);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(7, 193, 96, 0.4);
}
.confirm-btn:active:not(:disabled) {
transform: translateY(0);
}
.confirm-btn:disabled {
background: var(--color-text-disabled, #c8c9cc);
cursor: not-allowed;
box-shadow: none;
}
@media (max-width: 480px) {
.captcha-overlay {
padding: 0.75rem;
}
.captcha-modal {
max-width: 100%;
border-radius: 0.875rem;
}
.captcha-header {
padding: 1rem 1.25rem;
}
.captcha-content {
padding: 1.25rem 1.25rem 0.875rem 1.25rem;
}
.captcha-canvas {
min-height: 140px;
}
.captcha-footer {
padding: 1rem 1.25rem;
}
.captcha-instruction p {
font-size: 0.875rem;
}
}
</style>

View File

@@ -0,0 +1,303 @@
<template>
<div>
<div class="px-4 text-sm text-gray-500">
分析指数是根据网络行为大数据出具的分析评估参考分数分数越高越好该指数仅对本报告有效不代表对报告查询人的综合定性评价
</div>
<div ref="chartRef" :style="{ width: '100%', height: '200px' }"></div>
<div class="risk-description">
{{ riskDescription }}
</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 }">
{{ item.range }}
</span>
<span class="risk-legend__text">{{ item.level }}</span>
</div>
</div>
</div>
</template>
<script setup>
import * as echarts from "echarts";
import { ref, onMounted, onUnmounted, watch, computed } from "vue";
const props = defineProps({
score: {
type: Number,
required: true,
},
});
// 根据分数计算风险等级和颜色(分数越高越安全)
const riskLevel = computed(() => {
const score = props.score;
if (score >= 75 && score <= 100) {
return {
level: "无任何风险",
color: "#52c41a",
gradient: [
{ offset: 0, color: "#52c41a" },
{ offset: 1, color: "#7fdb42" }
]
};
} else if (score >= 50 && score < 75) {
return {
level: "风险指数较低",
color: "#faad14",
gradient: [
{ offset: 0, color: "#faad14" },
{ offset: 1, color: "#ffc53d" }
]
};
} else if (score >= 25 && score < 50) {
return {
level: "风险指数较高",
color: "#fa8c16",
gradient: [
{ offset: 0, color: "#fa8c16" },
{ offset: 1, color: "#ffa940" }
]
};
} else {
return {
level: "高风险警告",
color: "#f5222d",
gradient: [
{ offset: 0, color: "#f5222d" },
{ offset: 1, color: "#ff4d4f" }
]
};
}
});
// 评分解释文本(分数越高越安全)
const riskDescription = computed(() => {
const score = props.score;
if (score >= 75 && score <= 100) {
return "根据综合分析,当前报告未检测到明显风险因素,各项指标表现正常,总体状况良好。";
} else if (score >= 50 && score < 75) {
return "根据综合分析,当前报告存在少量风险信号,建议关注相关指标变化,保持警惕。";
} else if (score >= 25 && score < 50) {
return "根据综合分析,当前报告风险指数较高,多项指标显示异常,建议进一步核实相关情况。";
} else {
return "根据综合分析,当前报告显示高度风险状态,多项重要指标严重异常,请立即采取相应措施。";
}
});
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" }
];
const initChart = () => {
if (!chartRef.value) return;
// 初始化ECharts实例
chartInstance = echarts.init(chartRef.value);
updateChart();
};
const updateChart = () => {
if (!chartInstance) return;
// 获取当前风险等级信息
const risk = riskLevel.value;
// 配置项
const option = {
series: [
{
type: "gauge",
startAngle: 180,
endAngle: 0,
min: 0,
max: 100,
radius: "100%",
center: ["50%", "80%"],
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, risk.gradient),
shadowBlur: 6,
shadowColor: risk.color,
},
progress: {
show: true,
width: 20,
roundCap: true,
clip: false
},
axisLine: {
roundCap: true,
lineStyle: {
width: 20,
color: [
[1, new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{
offset: 0,
color: risk.color + "30" // 使用风险颜色透明度20%
},
{
offset: 1,
color: risk.color + "25" // 使用风险颜色透明度10%
}
])]
]
}
},
axisTick: {
show: true,
distance: -30,
length: 6,
splitNumber: 10, // 每1分一个小刻度
lineStyle: {
color: risk.color,
width: 1,
opacity: 0.5
}
},
splitLine: {
show: true,
distance: -36,
length: 12,
splitNumber: 9, // 9个大刻度100分分成9个区间
lineStyle: {
color: risk.color,
width: 2,
opacity: 0.5
}
},
axisLabel: {
show: false,
},
anchor: {
show: false
},
pointer: {
icon: "triangle",
iconStyle: {
color: risk.color,
borderColor: risk.color,
borderWidth: 1
},
offsetCenter: ["7%", "-67%"],
length: "10%",
width: 15
},
detail: {
valueAnimation: true,
fontSize: 30,
fontWeight: "bold",
color: risk.color,
offsetCenter: [0, "-25%"],
formatter: function (value) {
return `{value|${value}分}\n{level|${risk.level}}`;
},
rich: {
value: {
fontSize: 30,
fontWeight: 'bold',
color: risk.color,
padding: [0, 0, 5, 0]
},
level: {
fontSize: 14,
fontWeight: 'normal',
color: risk.color,
padding: [5, 0, 0, 0]
}
}
},
data: [
{
value: props.score
}
],
title: {
fontSize: 14,
color: risk.color,
offsetCenter: [0, "10%"],
formatter: risk.level
}
}
]
};
// 使用配置项设置图表
chartInstance.setOption(option);
};
// 监听分数变化
watch(
() => props.score,
() => {
updateChart();
}
);
onMounted(() => {
initChart();
// 处理窗口大小变化
window.addEventListener("resize", () => {
if (chartInstance) {
chartInstance.resize();
}
});
});
// 在组件销毁前清理
onUnmounted(() => {
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
window.removeEventListener("resize", chartInstance?.resize);
});
</script>
<style scoped>
.risk-description {
margin-bottom: 4px;
padding: 0 12px;
color: #666666;
font-size: 12px;
line-height: 1.5;
text-align: center;
}
.risk-legend {
display: flex;
justify-content: center;
gap: 16px;
padding: 0 16px 12px;
font-size: 12px;
color: #666666;
}
.risk-legend__item {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
justify-content: center;
}
.risk-legend__pill {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 2px 12px;
border-radius: 9999px;
font-size: 12px;
font-weight: 600;
min-width: 64px;
}
.risk-legend__text {
white-space: nowrap;
}
</style>

View File

@@ -0,0 +1,259 @@
<template>
<div v-if="show" class="image-save-guide-overlay">
<div class="guide-content" @click.stop>
<!-- 关闭按钮 -->
<button class="close-button" @click="close">
<span class="close-icon">×</span>
</button>
<!-- 图片区域 -->
<div v-if="imageUrl" class="image-container">
<img :src="imageUrl" class="guide-image" />
</div>
<!-- 文字内容区域 -->
<div class="text-container">
<div class="guide-title">{{ title }}</div>
<div class="guide-instruction">长按图片保存</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, watch } from 'vue';
const props = defineProps({
show: {
type: Boolean,
default: false
},
imageUrl: {
type: String,
default: ''
},
title: {
type: String,
default: '保存图片到相册'
}
});
const emit = defineEmits(['close']);
const close = () => {
emit('close');
};
// 自动关闭功能已禁用
// watch(() => props.show, (newVal) => {
// if (newVal && props.autoCloseDelay > 0) {
// setTimeout(() => {
// close();
// }, props.autoCloseDelay);
// }
// });
</script>
<style scoped>
.image-save-guide-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
z-index: 9999;
display: flex;
justify-content: center;
align-items: center;
padding: 16px;
box-sizing: border-box;
}
.guide-content {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
max-width: 400px;
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
padding: 24px 20px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
color: #333;
text-align: center;
position: relative;
}
/* 关闭按钮 */
.close-button {
position: absolute;
top: 12px;
right: 12px;
width: 32px;
height: 32px;
border: none;
background: rgba(0, 0, 0, 0.1);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background-color 0.2s ease;
z-index: 10;
}
.close-button:hover {
background: rgba(0, 0, 0, 0.2);
}
.close-icon {
font-size: 20px;
color: #666;
font-weight: bold;
line-height: 1;
}
/* 图片容器 */
.image-container {
width: 100%;
margin-bottom: 20px;
display: flex;
justify-content: center;
}
.guide-image {
max-width: 100%;
max-height: 60vh;
width: auto;
height: auto;
border-radius: 12px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
object-fit: contain;
}
/* 文字容器 */
.text-container {
width: 100%;
display: flex;
flex-direction: column;
gap: 12px;
}
.guide-title {
font-size: 20px;
font-weight: 600;
color: #1a1a1a;
line-height: 1.3;
margin-bottom: 4px;
}
.guide-instruction {
font-size: 16px;
color: #4a4a4a;
line-height: 1.4;
font-weight: 500;
}
/* 超小屏幕 (320px - 375px) */
@media (max-width: 375px) {
.image-save-guide-overlay {
padding: 12px;
}
.guide-content {
padding: 20px 16px;
max-width: 100%;
}
.guide-title {
font-size: 18px;
}
.guide-instruction {
font-size: 15px;
}
.close-button {
width: 28px;
height: 28px;
top: 10px;
right: 10px;
}
.close-icon {
font-size: 18px;
}
.guide-image {
max-height: 50vh;
}
}
/* 小屏幕 (376px - 414px) */
@media (min-width: 376px) and (max-width: 414px) {
.guide-content {
padding: 22px 18px;
}
.guide-title {
font-size: 19px;
}
}
/* 中等屏幕 (415px - 480px) */
@media (min-width: 415px) and (max-width: 480px) {
.guide-content {
padding: 24px 20px;
}
.guide-title {
font-size: 20px;
}
}
/* 大屏幕 (481px+) */
@media (min-width: 481px) {
.guide-content {
padding: 28px 24px;
max-width: 420px;
}
.guide-title {
font-size: 22px;
}
.guide-instruction {
font-size: 17px;
}
}
/* 横屏适配 */
@media (orientation: landscape) and (max-height: 500px) {
.guide-content {
padding: 16px 20px;
}
.image-container {
margin-bottom: 12px;
}
.guide-image {
max-height: 40vh;
}
.text-container {
gap: 8px;
}
.guide-title {
font-size: 18px;
margin-bottom: 2px;
}
.guide-instruction {
font-size: 15px;
}
}
</style>

View File

@@ -0,0 +1,579 @@
<template>
<div class="inquire-bg min-h-screen relative pt-60" :style="backgroundStyle">
<!-- 主要内容区域 - 覆盖背景图片 -->
<div class="min-h-screen relative mx-4 pb-12">
<!-- 产品卡片牌匾效果 - 使用背景图片 -->
<div class="absolute -top-[12px] left-1/2 transform -translate-x-1/2 w-[140px]">
<div class="trapezoid-bg-image flex items-center justify-center" :style="trapezoidBgStyle">
<div class="text-xl whitespace-nowrap text-white" :style="trapezoidTextStyle">{{
featureData.product_name }}</div>
</div>
</div>
<div class="card-container">
<!-- 基本信息标题 -->
<div class="mb-6 flex items-center">
<SectionTitle title="基本信息" />
<div class="ml-auto flex items-center text-gray-600 cursor-pointer" @click="toExample">
<img src="@/assets/images/report/slbg_inquire_icon.png" alt="示例报告" class="w-4 h-4 mr-1" />
<span class="text-md">示例报告</span>
</div>
</div>
<!-- 表单输入区域 -->
<div class="space-y-4 mb-6">
<div class="flex items-center py-3 border-b border-gray-100">
<label for="name" class="w-20 font-medium text-gray-700">姓名</label>
<input v-model="formData.name" id="name" type="text" placeholder="请输入正确的姓名"
class="flex-1 border-none outline-none" @click="handleInputClick" />
</div>
<div class="flex items-center py-3 border-b border-gray-100">
<label for="idCard" class="w-20 font-medium text-gray-700">身份证号</label>
<input v-model="formData.idCard" id="idCard" type="text" placeholder="请输入准确的身份证号"
class="flex-1 border-none outline-none" @click="handleInputClick" />
</div>
<div class="flex items-center py-3 border-b border-gray-100">
<label for="mobile" class="w-20 font-medium text-gray-700">手机号</label>
<input v-model="formData.mobile" id="mobile" type="tel" placeholder="请输入手机号"
class="flex-1 border-none outline-none" @click="handleInputClick" />
</div>
<div class="flex items-center py-3 border-b border-gray-100">
<label for="verificationCode" class="w-20 font-medium text-gray-700">验证码</label>
<input v-model="formData.verificationCode" id="verificationCode" placeholder="请输入验证码"
maxlength="6" class="flex-1 border-none outline-none" @click="handleInputClick" />
<button class="text-primary font-medium text-nowrap"
:disabled="isCountingDown || !isPhoneNumberValid" @click="sendVerificationCode">
{{
isCountingDown
? `${countdown}s重新获取`
: "获取验证码"
}}
</button>
</div>
</div>
<!-- 协议同意 -->
<div class="flex items-center mb-6">
<input type="checkbox" v-model="formData.agreeToTerms" class="mr-3 accent-primary" />
<span class="text-sm text-gray-500">
我已阅读并同意
<span @click="toUserAgreement" class="text-primary cursor-pointer">用户协议</span>
<span @click="toPrivacyPolicy" class="text-primary cursor-pointer">隐私政策</span>
<span @click="toAuthorization" class="text-primary cursor-pointer">授权书</span>
</span>
</div>
<!-- 查询按钮 -->
<button
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>
</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天后将自动删除
</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="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);">
<div class="w-1.5 h-1.5 rounded-full mr-1.5"
style="background-color: var(--van-theme-primary);">
</div>
<span class="text-xs font-medium" style="color: var(--van-theme-primary);">更多信息请解锁报告</span>
</div>
</div>
</div>
<!-- 产品详情卡片 -->
<div class="card mt-4">
<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 class="text-lg" style="color: var(--van-text-color-2);">价格</div>
<div>
<div class="text-2xl font-semibold text-danger">
¥{{ featureData.sell_price }}
</div>
</div>
</div>
<div class="mb-4 leading-relaxed" style="color: var(--van-text-color-2);"
v-html="featureData.description">
</div>
<div class="mb-2 text-xs italic text-danger">
为保证用户的隐私以及数据安全查询的结果生成30天之后将自动清除
</div>
</div>
</div>
<!-- 支付组件 -->
<Payment v-model="showPayment" :data="featureData" :id="queryId" type="query" @close="showPayment = false" />
<BindPhoneDialog @bind-success="handleBindSuccess" />
<!-- 历史查询按钮 - 仅推广查询显示 -->
<div v-if="props.type === 'promotion'" @click="toHistory"
class="fixed right-2 top-3/4 px-4 py-2 text-sm bg-primary rounded-xl cursor-pointer text-white font-bold shadow active:bg-blue-500">
历史查询
</div>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted, onUnmounted, nextTick } 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 Payment from "@/components/Payment.vue";
import BindPhoneDialog from "@/components/BindPhoneDialog.vue";
import SectionTitle from "@/components/SectionTitle.vue";
import ReportFeatures from "@/components/ReportFeatures.vue";
// Props
const props = defineProps({
// 查询类型:'normal' | 'promotion'
type: {
type: String,
default: 'normal'
},
// 产品特征
feature: {
type: String,
required: true
},
// 推广链接标识符(仅推广查询需要)
linkIdentifier: {
type: String,
default: ''
},
// 产品数据(从外部传入)
featureData: {
type: Object,
default: () => ({})
}
});
// Emits
const emit = defineEmits(['submit-success']);
// 动态导入产品背景图片的函数
const loadProductBackground = async (productType) => {
try {
switch (productType) {
case 'companyinfo':
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':
return (await import("@/assets/images/report/grdsj_inquire_bg.png")).default;
case 'marriage':
return (await import("@/assets/images/report/marriage_inquire_bg.png")).default;
case 'homeservice':
return (await import("@/assets/images/report/homeservice_inquire_bg.png")).default;
case 'backgroundcheck':
return (await import("@/assets/images/report/backgroundcheck_inquire_bg.png")).default;
case 'consumerFinanceReport':
return (await import("@/assets/images/report/xjbg_inquire_bg.png")).default;
default:
return null;
}
} catch (error) {
console.warn(`Failed to load background image for ${productType}:`, error);
return null;
}
};
const route = useRoute();
const router = useRouter();
const dialogStore = useDialogStore();
const userStore = useUserStore();
const { isWeChat } = useEnv();
// 响应式数据
const showPayment = ref(false);
const pendingPayment = ref(false);
const queryId = ref(null);
const productBackground = ref('');
const trapezoidBgImage = ref('');
const isCountingDown = ref(false);
const countdown = ref(60);
// 使用传入的featureData或创建响应式引用
const featureData = computed(() => props.featureData || {});
// 表单数据
const formData = reactive({
name: "",
idCard: "",
mobile: "",
verificationCode: "",
agreeToTerms: false
});
// 计算属性
const isPhoneNumberValid = computed(() => {
return /^1[3-9]\d{9}$/.test(formData.mobile);
});
const isIdCardValid = computed(() => /^\d{17}[\dX]$/i.test(formData.idCard));
const isLoggedIn = computed(() => userStore.isLoggedIn);
const buttonText = computed(() => {
return isLoggedIn.value ? '立即查询' : '前往登录';
});
// 获取产品背景图片
const getProductBackground = computed(() => productBackground.value);
// 背景图片样式
const backgroundStyle = computed(() => {
if (getProductBackground.value) {
return {
backgroundImage: `url(${getProductBackground.value})`,
backgroundSize: 'contain',
backgroundPosition: 'center -40px',
backgroundRepeat: 'no-repeat',
};
}
return {};
});
// 动态加载牌匾背景图片
const loadTrapezoidBackground = async () => {
try {
let bgModule;
if (props.feature === 'marriage') {
bgModule = await import("@/assets/images/report/title_inquire_bg_red.png");
} else if (props.feature === 'homeservice') {
bgModule = await import("@/assets/images/report/title_inquire_bg_green.png");
} else if (props.feature === 'consumerFinanceReport' || props.feature === 'companyinfo') {
bgModule = await import("@/assets/images/report/title_inquire_bg_yellow.png");
} else {
bgModule = await import("@/assets/images/report/title_inquire_bg.png");
}
trapezoidBgImage.value = bgModule.default;
} catch (error) {
console.warn(`Failed to load trapezoid background image:`, error);
// 回退到默认图片
try {
const defaultModule = await import("@/assets/images/report/title_inquire_bg.png");
trapezoidBgImage.value = defaultModule.default;
} catch (e) {
console.error('Failed to load default trapezoid background:', e);
}
}
};
// 牌匾背景图片样式
const trapezoidBgStyle = computed(() => {
if (trapezoidBgImage.value) {
return {
backgroundImage: `url(${trapezoidBgImage.value})`,
};
}
return {};
});
// 牌匾文字样式
const trapezoidTextStyle = computed(() => {
// homeservice 和 marriage 使用白色文字
// 其他情况使用默认字体色(不设置 color使用浏览器默认或继承
return {};
});
// 方法
const validateField = (field, value, validationFn, errorMessage) => {
if (isHasInput(field) && !validationFn(value)) {
showToast({ message: errorMessage });
return false;
}
return true;
};
const defaultInput = ["name", "idCard", "mobile", "verificationCode"];
const isHasInput = (input) => {
return defaultInput.includes(input);
};
// 处理绑定手机号成功的回调
function handleBindSuccess() {
if (pendingPayment.value) {
pendingPayment.value = false;
submitRequest();
}
}
// 处理输入框点击事件
const handleInputClick = async () => {
if (!isLoggedIn.value) {
// 非微信浏览器环境:未登录用户提示跳转到登录页
if (!isWeChat.value) {
try {
await showConfirmDialog({
title: '提示',
message: '您需要登录后才能进行查询,是否前往登录?',
confirmButtonText: '前往登录',
cancelButtonText: '取消',
});
router.push('/login');
} catch {
// 用户点击取消,什么都不做
}
}
} else {
// 微信浏览器环境:已登录但检查是否需要绑定手机号
if (isWeChat.value && !userStore.mobile) {
dialogStore.openBindPhone();
}
}
};
function handleSubmit() {
// 非微信浏览器环境:检查登录状态
if (!isWeChat.value && !isLoggedIn.value) {
router.push('/login');
return;
}
// 基本协议验证
if (!formData.agreeToTerms) {
showToast({ message: `请阅读并同意用户协议和隐私政策` });
return;
}
if (
!validateField("name", formData.name, (v) => v, "请输入姓名") ||
!validateField(
"mobile",
formData.mobile,
(v) => isPhoneNumberValid.value,
"请输入有效的手机号"
) ||
!validateField(
"idCard",
formData.idCard,
(v) => isIdCardValid.value,
"请输入有效的身份证号码"
) ||
!validateField(
"verificationCode",
formData.verificationCode,
(v) => v,
"请输入验证码"
)
) {
return;
}
// 检查是否需要绑定手机号
if (!userStore.mobile) {
pendingPayment.value = true;
dialogStore.openBindPhone();
} else {
submitRequest();
}
}
async function submitRequest() {
const req = {
name: formData.name,
id_card: formData.idCard,
mobile: formData.mobile,
code: formData.verificationCode
};
const reqStr = JSON.stringify(req);
const encodeData = aesEncrypt(
reqStr,
import.meta.env.VITE_INQUIRE_AES_KEY
);
let apiUrl = '';
let requestData = { data: encodeData };
if (props.type === 'promotion') {
apiUrl = `/query/service_agent/${props.feature}`;
requestData.agent_identifier = props.linkIdentifier;
} else {
apiUrl = `/query/service/${props.feature}`;
}
const { data, error } = await useApiFetch(apiUrl)
.post(requestData)
.json();
if (data.value.code === 200) {
queryId.value = data.value.data.id;
// 推广查询需要保存token
if (props.type === 'promotion') {
localStorage.setItem("token", data.value.data.accessToken);
localStorage.setItem("refreshAfter", data.value.data.refreshAfter);
localStorage.setItem("accessExpire", data.value.data.accessExpire);
}
showPayment.value = true;
emit('submit-success', data.value.data);
}
}
async function sendVerificationCode() {
if (isCountingDown.value || !isPhoneNumberValid.value) return;
if (!isPhoneNumberValid.value) {
showToast({ message: "请输入有效的手机号" });
return;
}
const { data, error } = await useApiFetch("/auth/sendSms")
.post({ mobile: formData.mobile, actionType: "query" })
.json();
if (!error.value && data.value.code === 200) {
showToast({ message: "验证码发送成功", type: "success" });
startCountdown();
nextTick(() => {
const verificationCodeInput = document.getElementById('verificationCode');
if (verificationCodeInput) {
verificationCodeInput.focus();
}
});
} else {
showToast({ message: "验证码发送失败,请重试" });
}
}
let timer = null;
function startCountdown() {
isCountingDown.value = true;
countdown.value = 60;
timer = setInterval(() => {
if (countdown.value > 0) {
countdown.value--;
} else {
clearInterval(timer);
isCountingDown.value = false;
}
}, 1000);
}
function toUserAgreement() {
router.push(`/userAgreement`);
}
function toPrivacyPolicy() {
router.push(`/privacyPolicy`);
}
function toAuthorization() {
router.push(`/authorization`);
}
const toExample = () => {
router.push(`/example?feature=${props.feature}`);
};
const toHistory = () => {
router.push("/historyQuery");
};
// 生命周期
onMounted(async () => {
await loadBackgroundImage();
await loadTrapezoidBackground();
});
// 加载背景图片
const loadBackgroundImage = async () => {
const background = await loadProductBackground(props.feature);
productBackground.value = background || '';
};
onUnmounted(() => {
if (timer) {
clearInterval(timer);
}
});
</script>
<style scoped>
/* 背景样式 */
.inquire-bg {
background-color: var(--color-primary-50);
min-height: 100vh;
position: relative;
}
/* 卡片样式优化 */
.card {
@apply shadow-lg rounded-xl p-6 transition-all hover:shadow-xl;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06),
0 0 0 1px rgba(255, 255, 255, 0.05);
}
/* 按钮悬停效果 */
button:hover {
transform: translateY(-1px);
}
button:active {
transform: translateY(0);
}
/* 梯形背景图片样式 */
.trapezoid-bg-image {
background-size: contain;
background-repeat: no-repeat;
background-position: center;
height: 44px;
}
/* 卡片容器样式 */
.card-container {
background: white;
border-radius: 8px;
padding: 32px 16px;
box-shadow: 0px 0px 24px 0px #3F3F3F0F;
}
.card-container input::placeholder {
color: #DDDDDD;
}
/* 功能标签样式 */
.feature-tag {
background-color: var(--color-primary-light);
color: var(--color-primary);
padding: 6px 12px;
border-radius: 9999px;
font-size: 12px;
font-weight: 500;
display: flex;
align-items: center;
}
/* 功能标签圆点 */
.feature-dot {
width: 6px;
height: 6px;
background-color: var(--color-primary);
border-radius: 50%;
margin-right: 8px;
}
</style>

View File

@@ -0,0 +1,79 @@
<script setup>
// 接收 type 和 options props 以及 v-model
const props = defineProps({
type: {
type: String,
default: 'purple-pink', // 默认颜色渐变
},
options: {
type: Array,
required: true, // 动态传入选项
},
modelValue: {
type: String,
default: '', // v-model 绑定的值
},
})
const emit = defineEmits(['update:modelValue'])
// 选中内容绑定 v-model
const selected = ref(props.modelValue)
// 监听 v-model 的变化
watch(() => props.modelValue, (newValue) => {
selected.value = newValue
})
// 根据type动态生成分割线的类名
const lineClass = computed(() => {
// 统一使用主题色渐变
return 'bg-gradient-to-r from-red-600 via-red-500 to-red-700'
})
// 计算滑动线的位置和宽度
const slideLineStyle = computed(() => {
const index = props.options.findIndex(option => option.value === selected.value)
const buttonWidth = 100 / props.options.length
return {
width: `${buttonWidth}%`,
transform: `translateX(${index * 100}%)`,
}
})
// 选择选项函数
function selectOption(option) {
selected.value = option.value
// 触发 v-model 的更新
emit('update:modelValue', option.value)
}
</script>
<template>
<div class="relative flex">
<div
v-for="(option, index) in options"
:key="index"
class="flex-1 shrink-0 cursor-pointer py-2 text-center text-size-sm font-bold transition-transform duration-200 ease-in-out"
:class="{ 'text-gray-900': selected === option.value, 'text-gray-500': selected !== option.value }"
@click="selectOption(option)"
>
{{ option.label }}
</div>
<div
class="absolute bottom-0 h-[3px] rounded transition-all duration-300"
:style="slideLineStyle"
:class="lineClass"
/>
</div>
</template>
<style scoped>
/* 自定义样式 */
button {
outline: none;
border: none;
cursor: pointer;
}
button:focus {
outline: none;
}
</style>

41
src/components/LEmpty.vue Normal file
View File

@@ -0,0 +1,41 @@
<template>
<div class="card flex flex-col items-center justify-center text-center">
<!-- 图片插画 -->
<img src="@/assets/images/empty.svg" alt="空状态" class="w-64 h-64" />
<!-- 提示文字 -->
<h2 class="text-xl font-semibold text-gray-700 mb-2">
没有查询到相关结果
</h2>
<p class="text-gray-500 text-sm mb-2 leading-relaxed">
订单已申请退款预计
<span class="font-medium" style="color: var(--van-theme-primary);">24小时内到账</span>
</p>
<p class="text-gray-400 text-xs">
如果已到账您可以忽略本提示
</p>
<!-- 返回按钮 -->
<button @click="goBack"
class="mt-4 px-6 py-2 text-white rounded-lg transition duration-300 ease-in-out"
style="background-color: var(--van-theme-primary);"
onmouseover="this.style.backgroundColor='var(--van-theme-primary-dark)'"
onmouseout="this.style.backgroundColor='var(--van-theme-primary)'">
返回上一页
</button>
</div>
</template>
<script setup>
const route = useRoute();
// 返回上一页逻辑
function goBack() {
route.goBack()
}
</script>
<style scoped>
/* 你可以添加一些额外的样式(如果需要) */
</style>

View File

@@ -0,0 +1,55 @@
<script setup>
import { computed, ref, useSlots } from 'vue'
// 接收最大长度的 prop默认值 100
const props = defineProps({
maxLength: {
type: Number,
default: 100,
},
})
// 记录当前是否展开
const isExpanded = ref(false)
// 获取 slot 内容
const slots = useSlots()
// 计算截断后的内容
const truncatedContent = computed(() => {
const slotContent = getSlotContent()
return slotContent.length > props.maxLength
? `${slotContent.slice(0, props.maxLength)}...`
: slotContent
})
// 获取 slot 内容,确保返回的内容为字符串
function getSlotContent() {
const slotVNode = slots.default ? slots.default()[0] : null
return slotVNode ? slotVNode.children.toString().trim() : '' // 获取并转化为字符串
}
// 切换展开/收起状态
function toggleExpand() {
isExpanded.value = !isExpanded.value
}
</script>
<template>
<div>
<!-- 展开/收起按钮 -->
<!-- 展开/收起的内容 -->
<text v-if="isExpanded">
<slot /> <!-- 使用 slot 来展示传递的内容 -->
</text>
<text v-else>
<text>{{ truncatedContent }}</text>
</text>
<text :title="isExpanded ? '点击收起' : '点击展开'" class="cursor-pointer" style="color: var(--van-theme-primary);" @click="toggleExpand">
{{ isExpanded ? '收起' : '展开' }}
</text>
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,85 @@
<template>
<div class="card flex flex-col items-center justify-center text-center">
<!-- 图片插画 -->
<img
src="@/assets/images/pendding.svg"
alt="查询中"
class="w-64 h-64"
/>
<!-- 提示文字 -->
<h2 class="text-xl font-semibold text-gray-700 mb-2 floating-text">
报告正在查询中
</h2>
<p class="text-gray-500 text-sm mb-2 leading-relaxed">
请稍候我们正在为您查询报告查询过程可能需要一些时间
</p>
<p class="text-gray-400 text-xs mb-4">
您可以稍后刷新页面查看结果或之后访问历史报告列表查看
</p>
<p class="text-gray-400 text-xs mb-4">
如过久未查询成功请联系客服为您处理
</p>
<!-- 按钮组 -->
<div class="flex gap-4">
<!-- 刷新按钮 -->
<button
@click="refreshPage"
class="px-6 py-2 text-white rounded-lg transition duration-300 ease-in-out"
style="background-color: var(--van-theme-primary);"
onmouseover="this.style.backgroundColor='var(--van-theme-primary-dark)'"
onmouseout="this.style.backgroundColor='var(--van-theme-primary)'"
>
刷新页面
</button>
<!-- 历史报告按钮 -->
<button
@click="viewHistory"
class="px-6 py-2 text-white rounded-lg transition duration-300 ease-in-out"
style="background-color: var(--van-text-color-2);"
onmouseover="this.style.backgroundColor='var(--van-text-color)'"
onmouseout="this.style.backgroundColor='var(--van-text-color-2)'"
>
查看历史报告
</button>
</div>
</div>
</template>
<script setup>
const router = useRouter();
// 刷新页面逻辑
function refreshPage() {
location.reload(); // 浏览器刷新页面
}
// 查看历史报告逻辑
function viewHistory() {
router.replace({ path: "/historyQuery" }); // 假设历史报告页面的路由名为 'historyReports'
}
</script>
<style scoped>
@keyframes floatUpDown {
0% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
/* 向上浮动 */
}
100% {
transform: translateY(0);
/* 返回原位 */
}
}
/* 给提示文字和其他需要浮动的元素添加动画 */
.floating-text {
animation: floatUpDown 3s ease-in-out infinite;
/* 动画持续3秒缓入缓出循环播放 */
}
</style>

View File

@@ -0,0 +1,92 @@
<template>
<div class="l-remark-card my-[20px]">
<!-- 顶部连接点 -->
<div class="connection-line">
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAACTCAYAAADmz7tVAAAAAXNSR0IArs4c6QAAB59JREFUeF7tXFtsFFUY/s7sbreFUiyFglxaEJUgVaAYNBAjl/hgYrgU44PhRd+Ud3xQE4xP8izx0RfiGwX1xcQYMApegFakSghgWygIFVqope3e5phvzo47O53ZmcM2zT6cSTZsujPnfPP9l/P/w3xHQOM42S/rFxTQhCQ6bAtdkNgIidUSaOYwAhiFwDUI9Fo2upFH30gCY9tXiam404i4J/42KPcXJLosYOucRrTmckChANgFQEo1ihCAlQASCSCVAibGMWwDpxMC3RvaxdE4c0UC6rkhX4aNzyDxlGUhycldAFETECA/to08BK7AwjudK8T3la4LBXTmhmxosHEomcTBfD4+iLDJCCyZBPJ5HJ60cGjLCjEZdG4goL5+uSQrcCSdRlcmE8WD3u/pNJDJoLtO4kDHKnHbf/U0QASTEzglLKyx7fDJHH8pXs3vPFxT2hFmtSxA2ricktjmB1UGiGaqL+CoZaErDAz9N5cH7o0DQyPAv1PARFYBmlMHzKsHli8AWhqBVNKJvMCDoGwb3VMJ7Pear+z83kH5SV0dDgaZKWEBow+B6/eAm6PAVE4xxAG8DBEwGapPAcuagbYWoHkuUAhgm+bLZnF4Y7t4z0X9PyBGU8rCKYbzNLsK4NYI0HcTyOTD79p/HcGlk0DHMmDpguDAYHrI2djmRl8J0KD8UwBr/SHNu6dpegZKPqPnxoqxzpXKlEHjS+BSZ7t4pphcASY9W+JzAEnvZDTT0D3gXBVg3PEI6nmCagk0X94SeIvJU3A5aBL4ImFhr9+RxyaBX64B2bwuJ8Hn1yWBF1YDTQ3lv9PBCzaOj0m8KS5cla12Ehch0Oqlk/b/YwgYuBvfZ6Jgc8yVC4F1y8vHdIJCYtjK41lxYUDuqG/Edw/Hy4cjKycvAflC1DR6vycTwPa1ANnyHnMbgalx7BS91+WnqRQOZD0ZmYhvjQK//gUkrfAJyWjeLjkqqU8U16+wq3j+5ieApc3lDl6XBnI5HBG9g/K0lcCWgsdP6Mw/XwVuPwiOLALhHa5cBHS2Ay3zFJM3R4Dfh4C/R51QDkyKdO4l84EXnyx37kTSqRzOiN4BeRsCi73+Q0Df9gETmVLSc++Y5zHRvboeWN0KZHJArgBkCyqDE0j/MHCuHxjn9T6qeP2cNPBKRzmgoh/dET0Dksaq815HO584H3yHqQTw+mblnE5WtpXZCIZJcyoLTOWVyX+6Gpyhed2eTYH+mQ0F9OX56V7Au1uzFHhjs0p2PPgvzUWGyBaXFILiUvHjFWU+d2nxjrg7DJCOyWiat18C2haWhnYB8TeHoRwwmVW5iz74w2WALuA1eUWT6Tg1J/lgj4o8965pMrLhZ4hmI2Nf9QB0AW/GruzUGmFPBj7eBxSkCm8e/M7amoCyRZORITJF9o6dLQcUGfY6ibEioLwC5ZgsEw4oMjHqLB3VAoq1dOgsrtUCirW40g/ilh/VAIpdfrje3xOjQGNohzp1BR/iGqdVoBFUnBL2/CDw4e6QKAsB9HUvsKHtEUpYgooq8v8ZA7atVU38tLAPAMSouz+hOhDtIp+Aotog+sH6NpUU4wByGoKQPihWG0RQlRpFZuX17dUDit0oug4e1krPBCDtVtoFFfSwoRpAVT1s8JYJ3scxEkg+t0LPZHxWNCOPY/zVEJNnroA9m1ZhH507jlOzPygUcMwSODFjD6z8wKSUMnC1Dwj7VAOwZmFYnAW3AZFP0AygsP4p7O/GZFGMGYYMQz4GTB6KcgnDkGHI20qb1b7s6Yeph4rhYRbXqDxhGDIMmfLDNIrug3OzdJilIyojGoYMQ+YJGmC6DtN1BMWBqamj8qNhyDBkug7TdZiuwxcFJjGaxGgSo0mMJjGaxBiVCQ1DhqHKDJj/Jo/yEMOQYci8SGC6jqgoMAyFMVQzbwvXzPvUNfXGeU29k19TqoWa03XUlPKlam1QiALvkbVBWuopG6B6hUeoRpHbcEig+6w6V0s9paUv61ITufs1hKk4HY2iDXSfUxrF2PoyXQXeR/vURH5AfhUnhUpUTB0vAiKbsRR4uhrFQ3vVRK52ld+nKYGp5swr4FQUu7LSWBpFHRUnda7v71L0e33IUQJT6+oqgXNKeMvzqCj26lwjVZw68nZOcvA1Z8+Oksm47QaF226U8bWd4odSZr/wNlLnqqME5qTv7lR37FcCO4CKAm5HVV4E+c3Fcs3+jMrbeXe7NgKPP1ZSmjtRRj9yWSqCoW+NPFS6fS2ttI68nSbgxiI71pWk6fwb0wABOZ8iODLIrTnuPAjQ7M+UvJ2hS+H21qeB1ia1xYELyNXdkxmyxq1cLlwPFktynBmRt3Mghu7ctJKKLm5SjLggCI7MDI8Bl26pPWW0dyTQcWq3vCUoMrWoCWhvAeY3KKbuTwKDdxU7BBr04CDaqTXk7f56myCcrX2KPxCAs8FNhUcYkWGvkxijWqQ4v0cmRp2lI86Elc6JtXToLK7VAoq1uHKSuOVHNYBilx/uJHEKNLfk0AWmvf8QJ4hTws7qDk0EFVXkz+oeVgQU1Qa5mXrWdvnihDW1D5rrrDW1U5wLqqb20vOGdc3sNujPNTWzH6MX2GzsWPkfBLU1i3+dVUIAAAAASUVORK5CYII="
alt="左链条" class="connection-chain left" />
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAACTCAYAAADmz7tVAAAAAXNSR0IArs4c6QAAB59JREFUeF7tXFtsFFUY/s7sbreFUiyFglxaEJUgVaAYNBAjl/hgYrgU44PhRd+Ud3xQE4xP8izx0RfiGwX1xcQYMApegFakSghgWygIFVqope3e5phvzo47O53ZmcM2zT6cSTZsujPnfPP9l/P/w3xHQOM42S/rFxTQhCQ6bAtdkNgIidUSaOYwAhiFwDUI9Fo2upFH30gCY9tXiam404i4J/42KPcXJLosYOucRrTmckChANgFQEo1ihCAlQASCSCVAibGMWwDpxMC3RvaxdE4c0UC6rkhX4aNzyDxlGUhycldAFETECA/to08BK7AwjudK8T3la4LBXTmhmxosHEomcTBfD4+iLDJCCyZBPJ5HJ60cGjLCjEZdG4goL5+uSQrcCSdRlcmE8WD3u/pNJDJoLtO4kDHKnHbf/U0QASTEzglLKyx7fDJHH8pXs3vPFxT2hFmtSxA2ricktjmB1UGiGaqL+CoZaErDAz9N5cH7o0DQyPAv1PARFYBmlMHzKsHli8AWhqBVNKJvMCDoGwb3VMJ7Pear+z83kH5SV0dDgaZKWEBow+B6/eAm6PAVE4xxAG8DBEwGapPAcuagbYWoHkuUAhgm+bLZnF4Y7t4z0X9PyBGU8rCKYbzNLsK4NYI0HcTyOTD79p/HcGlk0DHMmDpguDAYHrI2djmRl8J0KD8UwBr/SHNu6dpegZKPqPnxoqxzpXKlEHjS+BSZ7t4pphcASY9W+JzAEnvZDTT0D3gXBVg3PEI6nmCagk0X94SeIvJU3A5aBL4ImFhr9+RxyaBX64B2bwuJ8Hn1yWBF1YDTQ3lv9PBCzaOj0m8KS5cla12Ehch0Oqlk/b/YwgYuBvfZ6Jgc8yVC4F1y8vHdIJCYtjK41lxYUDuqG/Edw/Hy4cjKycvAflC1DR6vycTwPa1ANnyHnMbgalx7BS91+WnqRQOZD0ZmYhvjQK//gUkrfAJyWjeLjkqqU8U16+wq3j+5ieApc3lDl6XBnI5HBG9g/K0lcCWgsdP6Mw/XwVuPwiOLALhHa5cBHS2Ay3zFJM3R4Dfh4C/R51QDkyKdO4l84EXnyx37kTSqRzOiN4BeRsCi73+Q0Df9gETmVLSc++Y5zHRvboeWN0KZHJArgBkCyqDE0j/MHCuHxjn9T6qeP2cNPBKRzmgoh/dET0Dksaq815HO584H3yHqQTw+mblnE5WtpXZCIZJcyoLTOWVyX+6Gpyhed2eTYH+mQ0F9OX56V7Au1uzFHhjs0p2PPgvzUWGyBaXFILiUvHjFWU+d2nxjrg7DJCOyWiat18C2haWhnYB8TeHoRwwmVW5iz74w2WALuA1eUWT6Tg1J/lgj4o8965pMrLhZ4hmI2Nf9QB0AW/GruzUGmFPBj7eBxSkCm8e/M7amoCyRZORITJF9o6dLQcUGfY6ibEioLwC5ZgsEw4oMjHqLB3VAoq1dOgsrtUCirW40g/ilh/VAIpdfrje3xOjQGNohzp1BR/iGqdVoBFUnBL2/CDw4e6QKAsB9HUvsKHtEUpYgooq8v8ZA7atVU38tLAPAMSouz+hOhDtIp+Aotog+sH6NpUU4wByGoKQPihWG0RQlRpFZuX17dUDit0oug4e1krPBCDtVtoFFfSwoRpAVT1s8JYJ3scxEkg+t0LPZHxWNCOPY/zVEJNnroA9m1ZhH507jlOzPygUcMwSODFjD6z8wKSUMnC1Dwj7VAOwZmFYnAW3AZFP0AygsP4p7O/GZFGMGYYMQz4GTB6KcgnDkGHI20qb1b7s6Yeph4rhYRbXqDxhGDIMmfLDNIrug3OzdJilIyojGoYMQ+YJGmC6DtN1BMWBqamj8qNhyDBkug7TdZiuwxcFJjGaxGgSo0mMJjGaxBiVCQ1DhqHKDJj/Jo/yEMOQYci8SGC6jqgoMAyFMVQzbwvXzPvUNfXGeU29k19TqoWa03XUlPKlam1QiALvkbVBWuopG6B6hUeoRpHbcEig+6w6V0s9paUv61ITufs1hKk4HY2iDXSfUxrF2PoyXQXeR/vURH5AfhUnhUpUTB0vAiKbsRR4uhrFQ3vVRK52ld+nKYGp5swr4FQUu7LSWBpFHRUnda7v71L0e33IUQJT6+oqgXNKeMvzqCj26lwjVZw68nZOcvA1Z8+Oksm47QaF226U8bWd4odSZr/wNlLnqqME5qTv7lR37FcCO4CKAm5HVV4E+c3Fcs3+jMrbeXe7NgKPP1ZSmjtRRj9yWSqCoW+NPFS6fS2ttI68nSbgxiI71pWk6fwb0wABOZ8iODLIrTnuPAjQ7M+UvJ2hS+H21qeB1ia1xYELyNXdkxmyxq1cLlwPFktynBmRt3Mghu7ctJKKLm5SjLggCI7MDI8Bl26pPWW0dyTQcWq3vCUoMrWoCWhvAeY3KKbuTwKDdxU7BBr04CDaqTXk7f56myCcrX2KPxCAs8FNhUcYkWGvkxijWqQ4v0cmRp2lI86Elc6JtXToLK7VAoq1uHKSuOVHNYBilx/uJHEKNLfk0AWmvf8QJ4hTws7qDk0EFVXkz+oeVgQU1Qa5mXrWdvnihDW1D5rrrDW1U5wLqqb20vOGdc3sNujPNTWzH6MX2GzsWPkfBLU1i3+dVUIAAAAASUVORK5CYII="
alt="右链条" class="connection-chain right" />
</div>
<div>
<van-icon name="info-o" class="tips-icon" />
<span class="tips-title">温馨提示</span>
</div>
<div>
<van-text-ellipsis rows="2" :content="content" expand-text="展开" collapse-text="收起" />
</div>
</div>
</template>
<script setup>
import { ref, defineProps } from 'vue';
const props = defineProps({
content: {
type: String,
required: true
}
});
const isExpanded = ref(false);
</script>
<style scoped>
.l-remark-card {
position: relative;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
border-radius: 0.75rem;
background-color: #ffffff;
padding: 24px;
}
.tips-card {
background: var(--van-theme-primary-light);
border-radius: 8px;
padding: 12px;
}
.tips-icon {
color: var(--van-theme-primary);
margin-right: 5px;
}
.tips-title {
font-weight: bold;
font-size: 16px;
}
.tips-content {
font-size: 14px;
color: #333;
}
/* 连接链条样式 */
.connection-line {
position: absolute;
top: -40px;
left: 0;
right: 0;
height: 60px;
z-index: 20;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 10px;
}
.connection-chain {
height: 60px;
object-fit: contain;
}
.connection-chain.left {
width: 80px;
margin-left: -10px;
}
.connection-chain.right {
width: 80px;
margin-right: -10px;
}
</style>

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

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

27
src/components/LTitle.vue Normal file
View File

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

173
src/components/Payment.vue Normal file
View File

@@ -0,0 +1,173 @@
<template>
<van-popup
v-model:show="show"
position="bottom"
class="flex flex-col justify-between p-6"
:style="{ height: '50%' }"
>
<div class="text-center">
<h3 class="text-lg font-bold">支付</h3>
</div>
<div class="text-center">
<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 }"
>
¥ {{ data.sell_price }}
</div>
<div>
¥
{{
discountPrice
? (data.sell_price * 0.2).toFixed(2)
: data.sell_price
}}
</div>
</div>
<!-- 仅在折扣时显示活动说明 -->
<div v-if="discountPrice" class="text-sm text-red-500 mt-1">
活动价2折优惠
</div>
</div>
<!-- 支付方式选择 -->
<div class="">
<van-cell-group inset>
<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'"
>
<template #icon>
<van-icon
size="24"
name="alipay"
color="#00A1E9"
class="mr-2"
/>
</template>
<template #right-icon>
<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
>
</div>
</van-popup>
</template>
<script setup>
import { ref, defineProps } from "vue";
const { isWeChat } = useEnv();
const props = defineProps({
data: {
type: Object,
required: true,
},
id: {
type: String,
required: true,
},
type: {
type: String,
required: true,
},
});
const show = defineModel();
const selectedPaymentMethod = ref(isWeChat.value ? "wechat" : "alipay");
onMounted(() => {
if (isWeChat.value) {
selectedPaymentMethod.value = "wechat";
} else {
selectedPaymentMethod.value = "alipay";
}
});
const orderNo = ref("");
const router = useRouter();
const discountPrice = ref(false); // 是否应用折扣
onMounted(() => {
if (isWeChat.value) {
selectedPaymentMethod.value = "wechat";
} else {
selectedPaymentMethod.value = "alipay";
}
});
async function getPayment() {
const { data, error } = await useApiFetch("/pay/payment")
.post({
id: props.id,
pay_method: selectedPaymentMethod.value,
pay_type: props.type,
})
.json();
if (data.value && !error.value) {
if (selectedPaymentMethod.value === "alipay") {
orderNo.value = data.value.data.order_no;
// 存储订单ID以便支付宝返回时获取
const prepayUrl = data.value.data.prepay_id;
const paymentForm = document.createElement("form");
paymentForm.method = "POST";
paymentForm.action = prepayUrl;
paymentForm.style.display = "none";
document.body.appendChild(paymentForm);
paymentForm.submit();
} else {
const payload = data.value.data.prepay_data;
WeixinJSBridge.invoke(
"getBrandWCPayRequest",
payload,
function (res) {
if (res.err_msg == "get_brand_wcpay_request:ok") {
// 支付成功,直接跳转到结果页面
router.push({
path: "/payment/result",
query: { orderNo: data.value.data.order_no },
});
}
}
);
}
}
show.value = false;
}
</script>
<style scoped></style>

View File

@@ -0,0 +1,161 @@
<template>
<van-popup v-model:show="show" destroy-on-close round position="bottom">
<div class="min-h-[500px] bg-gray-50 text-gray-600">
<div class="h-10 bg-white flex items-center justify-center font-semibold text-lg">设置客户查询价
</div>
<div class="card m-4">
<div class="flex items-center justify-between">
<div class="text-lg">
客户查询价 ()</div>
</div>
<div class="border-b border-gray-200">
<van-field v-model="price" type="number" label="¥" label-width="28"
:placeholder="`${productConfig.price_range_min} - ${productConfig.price_range_max}`"
@blur="onBlurPrice" class="!text-3xl" />
</div>
<div class="flex items-center justify-between mt-2">
<div>推广收益为<span class="text-orange-500"> {{ promotionRevenue }} </span></div>
<div>我的成本为<span class="text-orange-500"> {{ costPrice }} </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 class="">设定范围<span class="text-orange-500">{{
productConfig.price_range_min }}</span> - <span class="text-orange-500">{{
productConfig.price_range_max }}</span></div>
</div>
<div class="px-4 pb-4">
<van-button class="w-full" round type="primary" size="large" @click="onConfirm">确认</van-button>
</div>
</div>
</van-popup>
</template>
<script setup>
const props = defineProps({
defaultPrice: {
type: Number,
required: true
},
productConfig: {
type: Object,
required: true
}
})
const { defaultPrice, productConfig } = toRefs(props)
const emit = defineEmits(["change"])
const show = defineModel("show")
const price = ref(null)
watch(show, () => {
price.value = defaultPrice.value
})
const costPrice = computed(() => {
if (!productConfig.value) return 0.00
// 平台定价成本
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
}
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
}
}
}
return safeTruncate(platformPricing)
})
const promotionRevenue = computed(() => {
return safeTruncate(price.value - costPrice.value)
});
// 价格校验与修正逻辑
const validatePrice = (currentPrice) => {
const min = productConfig.value.price_range_min;
const max = productConfig.value.price_range_max;
let newPrice = Number(currentPrice);
let message = '';
// 处理无效输入
if (isNaN(newPrice)) {
newPrice = defaultPrice.value;
return { newPrice, message: '输入无效,请输入价格' };
}
// 处理小数位数(兼容科学计数法)
try {
const priceString = newPrice.toString()
const [_, decimalPart = ""] = priceString.split('.');
console.log(priceString, decimalPart)
// 当小数位数超过2位时处理
if (decimalPart.length > 2) {
newPrice = parseFloat(safeTruncate(newPrice));
message = '价格已自动格式化为两位小数';
}
} catch (e) {
console.error('价格格式化异常:', e);
}
// 范围校验(基于可能格式化后的值)
if (newPrice < min) {
message = `价格不能低于 ${min}`;
newPrice = min;
} else if (newPrice > max) {
message = `价格不能高于 ${max}`;
newPrice = max;
}
console.log(newPrice, message)
return { newPrice, message };
}
function safeTruncate(num, decimals = 2) {
if (isNaN(num) || !isFinite(num)) return "0.00";
const factor = 10 ** decimals;
const scaled = Math.trunc(num * factor);
const truncated = scaled / factor;
return truncated.toFixed(decimals);
}
const isManualConfirm = ref(false)
const onConfirm = () => {
if (isManualConfirm.value) return
const { newPrice, message } = validatePrice(price.value)
if (message) {
price.value = newPrice
showToast({ message });
} else {
emit("change", price.value)
show.value = false
}
}
const onBlurPrice = () => {
const { newPrice, message } = validatePrice(price.value)
if (message) {
isManualConfirm.value = true
price.value = newPrice
showToast({ message });
}
setTimeout(() => {
isManualConfirm.value = false
}, 0)
}
</script>
<style lang="scss" scoped></style>

648
src/components/QRcode.vue Normal file
View File

@@ -0,0 +1,648 @@
<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">
<canvas :ref="(el) => (posterCanvasRefs[index] = el)"
class="poster-canvas rounded-lg sm:rounded-xl m-auto"></canvas>
</van-swipe-item>
</van-swipe>
</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>
<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>
<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>
</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>
<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>
</template>
</div>
</div>
</van-popup>
<!-- 图片保存指引遮罩层 -->
<ImageSaveGuide :show="showImageGuide" :image-url="currentImageUrl" :title="imageGuideTitle"
@close="closeImageGuide" />
</template>
<script setup>
import { ref, watch, nextTick, computed, onMounted, toRefs } from "vue";
import QRCode from "qrcode";
import { showToast } from "vant";
import { useWeixinShare } from "@/composables/useWeixinShare";
import ImageSaveGuide from "./ImageSaveGuide.vue";
const props = defineProps({
linkIdentifier: {
type: String,
required: true,
},
mode: {
type: String,
default: "promote", // 例如 "promote" | "invitation"
},
});
const { linkIdentifier, mode } = toRefs(props);
const posterCanvasRefs = ref([]); // 用于绘制海报的canvas数组
const currentIndex = ref(0); // 当前显示的海报索引
const postersGenerated = ref([]); // 标记海报是否已经生成过将在onMounted中初始化
const show = defineModel("show");
// 微信环境检测
const isWeChat = computed(() => {
return /MicroMessenger/i.test(navigator.userAgent);
});
// 微信分享功能
const { configWeixinShare } = useWeixinShare();
// 图片保存指引遮罩层相关
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/`;
});
// 海报图片数组
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 onSwipeChange = (index) => {
currentIndex.value = index;
if (!postersGenerated.value[index]) {
generatePoster(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];
// 加载图片
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...`
);
}
// 如果 .png 不存在,尝试加载 .jpg 文件
try {
const module = await import(
`@/assets/images/${basePrefix}${i}.jpg`
);
images.push(module.default);
} 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]);
}
}
}
return images;
};
onMounted(async () => {
posterImages.value = await loadPosterImages();
// 根据加载的图片数量初始化postersGenerated数组
postersGenerated.value = Array(posterImages.value.length).fill(false);
});
// 生成海报并合成二维码
const generatePoster = async (index) => {
// 如果已经生成过海报,就直接返回
if (postersGenerated.value[index]) return;
// 确保 DOM 已经渲染完成
await nextTick();
const canvas = posterCanvasRefs.value[index];
if (!canvas) return; // 如果 canvas 元素为空则直接返回
const ctx = canvas.getContext("2d");
// 1. 加载海报图片
const posterImg = new Image();
posterImg.src = posterImages.value[index];
posterImg.onload = () => {
// 设置 canvas 尺寸与海报图一致
canvas.width = posterImg.width;
canvas.height = posterImg.height;
// 2. 绘制海报图片
ctx.drawImage(posterImg, 0, 0);
// 3. 生成二维码
QRCode.toDataURL(
generalUrl(),
{ width: 150, margin: 0 },
(err, qrCodeUrl) => {
if (err) {
console.error(err);
return;
}
// 4. 加载二维码图片
const qrCodeImg = new Image();
qrCodeImg.src = qrCodeUrl;
qrCodeImg.onload = () => {
// 获取当前海报的二维码位置配置
const positions = qrCodePositions.value[mode.value];
const position = positions[index] || positions[0]; // 如果没有对应索引的配置,则使用第一个配置
// 计算Y坐标负值表示从底部算起的位置
const qrY =
position.y < 0
? posterImg.height + position.y
: position.y;
// 绘制二维码
ctx.drawImage(
qrCodeImg,
position.x,
qrY,
position.size,
position.size
);
// 标记海报已生成
postersGenerated.value[index] = true;
};
}
);
};
};
// 监听 show 变化show 为 true 时生成海报
watch(show, (newVal) => {
if (newVal && !postersGenerated.value[currentIndex.value]) {
generatePoster(currentIndex.value); // 当弹窗显示且当前海报未生成时生成海报
}
});
// 分享给好友
const shareToFriend = () => {
if (!isWeChat.value) {
showToast({ message: "请在微信中打开" });
return;
}
const shareUrl = generalUrl();
const shareConfig = {
title: mode.value === "promote"
? "一查查 - 推广链接"
: "一查查 - 邀请链接",
desc: mode.value === "promote"
? "扫码查看一查查推广信息"
: "扫码申请一查查代理权限",
link: shareUrl,
imgUrl: "https://www.quannengcha.com/logo.png"
};
configWeixinShare(shareConfig);
// 显示分享指引
showShareGuide("好友");
};
// 分享到朋友圈
const shareToTimeline = () => {
if (!isWeChat.value) {
showToast({ message: "请在微信中打开" });
return;
}
const shareUrl = generalUrl();
const shareConfig = {
title: mode.value === "promote"
? "一查查 - 推广链接"
: "一查查 - 邀请链接",
desc: mode.value === "promote"
? "扫码查看一查查推广信息"
: "扫码申请一查查代理权限",
link: shareUrl,
imgUrl: "https://www.quannengcha.com/logo.png"
};
configWeixinShare(shareConfig);
// 显示分享指引
showShareGuide("朋友圈");
};
// 显示分享指引
const showShareGuide = (target) => {
// 设置遮罩层内容
currentImageUrl.value = ''; // 分享指引不需要图片
imageGuideTitle.value = `分享到${target}`;
// 显示遮罩层
showImageGuide.value = true;
};
// 微信环境保存图片
const savePosterForWeChat = () => {
const canvas = posterCanvasRefs.value[currentIndex.value];
const dataURL = canvas.toDataURL("image/png");
// 设置遮罩层内容
currentImageUrl.value = dataURL;
imageGuideTitle.value = '保存图片到相册';
// 显示遮罩层
showImageGuide.value = true;
};
// 关闭图片保存指引
const closeImageGuide = () => {
showImageGuide.value = false;
};
// 保存海报图片 - 多种保存方式(非微信环境)
const savePoster = () => {
const canvas = posterCanvasRefs.value[currentIndex.value];
const dataURL = canvas.toDataURL("image/png");
// 检测环境
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
if (isMobile) {
// 手机浏览器环境
saveForMobile(dataURL);
} else {
// PC浏览器环境
saveForPC(dataURL);
}
};
// PC浏览器保存方式
const saveForPC = (dataURL) => {
const a = document.createElement("a");
a.href = dataURL;
a.download = "一查查海报.png";
a.click();
};
// 手机浏览器保存方式
const saveForMobile = async (dataURL) => {
// 方法1: 尝试使用 File System Access API (Chrome 86+)
const fileSystemSuccess = await saveWithFileSystemAPI(dataURL);
if (fileSystemSuccess) return;
// 方法2: 尝试使用 Blob 和 URL.createObjectURL
try {
const blob = dataURLToBlob(dataURL);
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "一查查海报.png";
a.style.display = "none";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
// 清理 URL 对象
setTimeout(() => URL.revokeObjectURL(url), 100);
showToast({ message: "图片已保存到相册" });
} catch (error) {
console.error("Blob保存失败:", error);
// 方法3: 尝试使用 share API (支持分享到其他应用)
const shareSuccess = await tryShareAPI(dataURL);
if (!shareSuccess) {
// 方法4: 降级到长按保存提示
showLongPressTip(dataURL);
}
}
};
// 显示长按保存提示(非微信环境使用)
const showLongPressTip = (dataURL) => {
// 设置遮罩层内容
currentImageUrl.value = dataURL;
imageGuideTitle.value = '保存图片到相册';
// 显示遮罩层
showImageGuide.value = true;
};
// 将 dataURL 转换为 Blob
const dataURLToBlob = (dataURL) => {
const arr = dataURL.split(',');
const mime = arr[0].match(/:(.*?);/)[1];
const bstr = atob(arr[1]);
let n = bstr.length;
const u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new Blob([u8arr], { type: mime });
};
// 备用保存方法 - 使用 File System Access API (现代浏览器)
const saveWithFileSystemAPI = async (dataURL) => {
if ('showSaveFilePicker' in window) {
try {
const blob = dataURLToBlob(dataURL);
const fileHandle = await window.showSaveFilePicker({
suggestedName: '一查查海报.png',
types: [{
description: 'PNG images',
accept: { 'image/png': ['.png'] }
}]
});
const writable = await fileHandle.createWritable();
await writable.write(blob);
await writable.close();
showToast({ message: "图片已保存" });
return true;
} catch (error) {
console.error("File System API 保存失败:", error);
return false;
}
}
return false;
};
// 尝试使用 Share API
const tryShareAPI = async (dataURL) => {
if (navigator.share && navigator.canShare) {
try {
const blob = dataURLToBlob(dataURL);
const file = new File([blob], '一查查海报.png', { type: 'image/png' });
if (navigator.canShare({ files: [file] })) {
await navigator.share({
title: '一查查海报',
text: '分享海报图片',
files: [file]
});
showToast({ message: "图片已分享" });
return true;
}
} catch (error) {
console.error("Share API 失败:", error);
}
}
return false;
};
const generalUrl = () => {
return url.value + encodeURIComponent(linkIdentifier.value);
};
const copyUrl = () => {
copyToClipboard(generalUrl());
};
// 复制链接
const copyToClipboard = (text) => {
if (navigator.clipboard && window.isSecureContext) {
// 支持 Clipboard API
navigator.clipboard
.writeText(text)
.then(() => {
showToast({ message: "链接已复制!" });
})
.catch((err) => {
console.error("复制失败:", err);
});
} else {
// 对于不支持 Clipboard API 的浏览器,使用 fallback 方法
const textArea = document.createElement("textarea");
textArea.value = text;
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand("copy");
showToast({ message: "链接已复制!" });
} catch (err) {
console.error("复制失败:", err);
} finally {
document.body.removeChild(textArea);
}
}
};
</script>
<style lang="scss" scoped>
.qrcode-popup-container {
display: flex;
flex-direction: column;
max-height: 95vh;
overflow: hidden;
}
.qrcode-content {
flex-shrink: 0;
padding: 0.75rem;
}
/* 小屏设备优化 */
@media (max-width: 375px) {
.qrcode-content {
padding: 0.5rem;
}
}
/* 中等及以上屏幕 */
@media (min-width: 640px) {
.qrcode-content {
padding: 1rem;
}
}
.poster-swiper {
height: calc(95vh - 180px);
min-height: 300px;
max-height: 500px;
width: 100%;
}
/* 小屏设备:更小的海报高度 */
@media (max-width: 375px) {
.poster-swiper {
height: calc(95vh - 160px);
min-height: 280px;
max-height: 400px;
}
}
/* 中等屏幕 */
@media (min-width: 640px) and (max-width: 767px) {
.poster-swiper {
height: calc(95vh - 190px);
max-height: 520px;
}
}
/* 大屏幕 */
@media (min-width: 768px) {
.poster-swiper {
height: calc(95vh - 200px);
max-height: 600px;
}
}
.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;
}
}
.share-icon {
transition: transform 0.2s ease;
}
.share-icon:active {
transform: scale(0.95);
}
@keyframes fadeInOut {
0%,
100% {
opacity: 0.5;
}
50% {
opacity: 1;
}
}
@keyframes slideLeftRight {
0%,
100% {
transform: translateX(0);
}
50% {
transform: translateX(5px);
}
}
/* 优化 van-divider 在小屏幕上的间距 */
:deep(.van-divider) {
margin: 0.5rem 0;
}
@media (min-width: 640px) {
:deep(.van-divider) {
margin: 0.75rem 0;
}
}
</style>

View File

@@ -0,0 +1,14 @@
<template>
<van-popup v-model:show="show" closeable round :style="{ padding: '18px' }">
<img src="@/assets/images/qrcode_qnc.jpg" alt="qrcode" />
<div class="text-center font-bold text-2xl">
更多服务请关注一查查公众号
</div>
</van-popup>
</template>
<script setup>
const show = defineModel("show");
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,376 @@
<script setup>
import { ref, computed } from "vue";
import { useDialogStore } from "@/stores/dialogStore";
const router = useRouter();
const dialogStore = useDialogStore();
const agentStore = useAgentStore();
const userStore = useUserStore();
import { showToast } from "vant";
// 表单数据
const realName = ref("");
const idCard = ref("");
const phoneNumber = ref("");
const verificationCode = ref("");
const isAgreed = ref(false);
// 倒计时相关
const isCountingDown = ref(false);
const countdown = ref(60);
let timer = null;
// 聚焦状态变量
const nameFocused = ref(false);
const idCardFocused = ref(false);
const phoneFocused = ref(false);
const codeFocused = ref(false);
// 表单验证
const isPhoneNumberValid = computed(() => {
return /^1[3-9]\d{9}$/.test(phoneNumber.value);
});
const isIdCardValid = computed(() => {
return /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/.test(idCard.value);
});
const isRealNameValid = computed(() => {
return /^[\u4e00-\u9fa5]{2,}$/.test(realName.value);
});
const canSubmit = computed(() => {
return (
isPhoneNumberValid.value &&
isIdCardValid.value &&
isRealNameValid.value &&
verificationCode.value.length === 6 &&
isAgreed.value
);
});
// 发送验证码
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) {
showToast({ message: "获取成功" });
startCountdown();
} else {
showToast(data.value.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 handleSubmit() {
if (!isRealNameValid.value) {
showToast({ message: "请输入有效的姓名" });
return;
}
if (!isIdCardValid.value) {
showToast({ message: "请输入有效的身份证号" });
return;
}
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("/agent/real_name")
.post({
name: realName.value,
id_card: idCard.value,
mobile: phoneNumber.value,
code: verificationCode.value,
})
.json();
if (data.value && !error.value) {
if (data.value.code === 200) {
showToast({ message: "认证成功" });
// 更新实名状态
agentStore.isRealName = true;
// 刷新代理状态信息
await agentStore.fetchAgentStatus();
// 刷新用户信息
await userStore.fetchUserInfo();
closeDialog();
} else {
showToast(data.value.msg);
}
}
}
function closeDialog() {
dialogStore.closeRealNameAuth();
// 重置表单
realName.value = "";
idCard.value = "";
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.showRealNameAuth" class="real-name-auth-dialog-box">
<van-popup v-model:show="dialogStore.showRealNameAuth" round position="bottom" @close="closeDialog"
:style="{ maxHeight: '90vh' }">
<div class="real-name-auth-dialog"
style="background: linear-gradient(135deg, var(--van-theme-primary-light), rgba(255,255,255,0.9));">
<div class="title-bar">
<div class="text-base sm:text-lg font-bold" style="color: var(--van-text-color);">实名认证</div>
<van-icon name="cross" class="close-icon" style="color: var(--van-text-color-2);"
@click="closeDialog" />
</div>
<div class="dialog-content">
<div class="px-4 sm:px-6 md:px-8 py-3 sm:py-4">
<div class="auth-notice p-3 sm:p-4 rounded-lg mb-4 sm:mb-6"
style="background-color: var(--van-theme-primary-light); border: 1px solid rgba(162, 37, 37, 0.2);">
<div class="text-xs sm:text-sm space-y-1.5 sm:space-y-2"
style="color: var(--van-text-color);">
<p class="font-medium" style="color: var(--van-theme-primary);">
实名认证说明
</p>
<p>1. 实名认证是提现的必要条件</p>
<p>2. 提现金额将转入您实名认证的银行卡账户</p>
<p>3. 请确保填写的信息真实有效否则将影响提现功能的使用</p>
<p>4. 认证信息提交后将无法修改请仔细核对</p>
</div>
</div>
<div class="space-y-3 sm:space-y-4">
<!-- 姓名输入 -->
<div :class="[
'input-container',
nameFocused ? 'focused' : '',
]"
style="background-color: var(--van-theme-primary-light); border: 2px solid rgba(162, 37, 37, 0);">
<input v-model="realName" class="input-field" type="text" placeholder="请输入真实姓名"
style="color: var(--van-text-color);" @focus="nameFocused = true"
@blur="nameFocused = false" />
</div>
<!-- 身份证号输入 -->
<div :class="[
'input-container',
idCardFocused ? 'focused' : '',
]"
style="background-color: var(--van-theme-primary-light); border: 2px solid rgba(162, 37, 37, 0);">
<input v-model="idCard" class="input-field" type="text" placeholder="请输入身份证号"
maxlength="18" style="color: var(--van-text-color);" @focus="idCardFocused = true"
@blur="idCardFocused = false" />
</div>
<!-- 手机号输入 -->
<div :class="[
'input-container',
phoneFocused ? 'focused' : '',
]"
style="background-color: var(--van-theme-primary-light); border: 2px solid rgba(162, 37, 37, 0);">
<input v-model="phoneNumber" class="input-field" type="tel" placeholder="请输入手机号"
maxlength="11" style="color: var(--van-text-color);" @focus="phoneFocused = true"
@blur="phoneFocused = false" />
</div>
<!-- 验证码输入 -->
<div class="flex items-center gap-2">
<div :class="[
'input-container flex-1',
codeFocused ? 'focused' : '',
]"
style="background-color: var(--van-theme-primary-light); border: 2px solid rgba(162, 37, 37, 0);">
<input v-model="verificationCode" class="input-field" placeholder="请输入验证码"
maxlength="6" style="color: var(--van-text-color);" @focus="codeFocused = true"
@blur="codeFocused = false" />
</div>
<button
class="verify-code-btn px-3 sm:px-4 py-2.5 sm:py-3 text-xs sm:text-sm font-bold flex-shrink-0 rounded-lg transition duration-300 whitespace-nowrap"
:class="isCountingDown || !isPhoneNumberValid
? 'cursor-not-allowed bg-gray-300 text-gray-500'
: 'text-white hover:opacity-90'
" :style="isCountingDown || !isPhoneNumberValid
? ''
: 'background-color: var(--van-theme-primary);'"
@click="sendVerificationCode">
{{
isCountingDown
? `${countdown}s`
: "获取验证码"
}}
</button>
</div>
<!-- 协议同意框 -->
<div class="flex items-start gap-2">
<input type="checkbox" v-model="isAgreed"
class="mt-0.5 sm:mt-1 flex-shrink-0 accent-primary" />
<span class="text-xs sm:text-sm leading-relaxed"
style="color: var(--van-text-color-2);">
我已阅读并同意
<a class="cursor-pointer hover:underline" style="color: var(--van-theme-primary);"
@click="toUserAgreement">用户协议</a>
<a class="cursor-pointer hover:underline" style="color: var(--van-theme-primary);"
@click="toPrivacyPolicy">隐私政策</a>
并确认以上信息真实有效将用于提现等资金操作
</span>
</div>
</div>
<button
class="mb-6 sm:mb-8 mt-6 sm:mt-8 w-full py-2.5 sm:py-3 text-base sm:text-lg font-bold text-white rounded-full transition duration-300"
:class="{ 'opacity-50 cursor-not-allowed': !canSubmit }"
:style="canSubmit ? 'background-color: var(--van-theme-primary);' : 'background-color: var(--van-text-color-3);'"
@click="handleSubmit">
确认认证
</button>
</div>
</div>
</div>
</van-popup>
</div>
</template>
<style scoped>
.real-name-auth-dialog {
max-height: 90vh;
display: flex;
flex-direction: column;
}
.dialog-content {
overflow-y: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
}
.title-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid var(--van-border-color);
flex-shrink: 0;
}
.close-icon {
font-size: 18px;
cursor: pointer;
}
@media (min-width: 640px) {
.close-icon {
font-size: 20px;
}
}
.input-container {
border-radius: 0.75rem;
transition: all 0.2s ease;
}
@media (min-width: 640px) {
.input-container {
border-radius: 1rem;
}
}
.input-container.focused {
border: 2px solid var(--van-theme-primary) !important;
}
.input-field {
width: 100%;
padding: 0.75rem;
background: transparent;
border: none;
outline: none;
font-size: 14px;
transition: border-color 0.3s ease;
}
@media (min-width: 640px) {
.input-field {
padding: 0.875rem 1rem;
font-size: 15px;
}
}
@media (min-width: 768px) {
.input-field {
padding: 1rem;
font-size: 16px;
}
}
.verify-code-btn {
min-width: 85px;
}
@media (min-width: 640px) {
.verify-code-btn {
min-width: 100px;
}
}
/* 优化小屏幕上的间距 */
@media (max-width: 375px) {
.input-field {
padding: 0.625rem;
font-size: 13px;
}
.verify-code-btn {
min-width: 75px;
padding-left: 0.5rem;
padding-right: 0.5rem;
}
}
/* 确保弹窗在键盘弹出时可以滚动 */
.real-name-auth-dialog-box {
position: relative;
}
</style>

84
src/components/Remark.vue Normal file
View File

@@ -0,0 +1,84 @@
<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>
</div>
<div class="mt-1 ml-4">
<van-text-ellipsis rows="2" :content="content" expand-text="展开" collapse-text="收起" />
</div>
</div>
</template>
<script setup>
import { ref, defineProps } from 'vue';
const props = defineProps({
content: {
type: String,
required: true
}
});
const isExpanded = ref(false);
</script>
<style scoped>
.tips-card {
background: var(--van-theme-primary-light);
border-radius: 8px;
padding: 12px;
}
.tips-icon {
width: 16px;
height: 16px;
margin-right: 5px;
object-fit: contain;
}
.tips-title {
font-weight: bold;
font-size: 16px;
}
.tips-content {
font-size: 14px;
color: #333;
}
/* 连接链条样式 */
.connection-line {
position: absolute;
top: -40px;
left: 0;
right: 0;
height: 60px;
z-index: 20;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 10px;
}
.connection-chain {
height: 60px;
object-fit: contain;
}
.connection-chain.left {
width: 80px;
margin-left: -10px;
}
.connection-chain.right {
width: 80px;
margin-right: -10px;
}
:deep(.van-text-ellipsis) {
font-size: 12px;
color: #999999;
}
</style>

View File

@@ -0,0 +1,251 @@
<template>
<div v-if="features && features.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));">
</div>
报告包含内容
</div>
<div class="grid grid-cols-4 gap-2">
<template v-for="(feature, index) in features" :key="feature.id">
<!-- FLXG0V4B 特殊处理显示8个独立的案件类型 -->
<template v-if="feature.api_id === 'FLXG0V4B'">
<div v-for="(caseType, caseIndex) in [
{ name: '管辖案件', icon: 'beijianguanrenyuan.svg' },
{ name: '刑事案件', icon: 'xingshi.svg' },
{ name: '民事案件', icon: 'minshianjianguanli.svg' },
{ name: '失信被执行', icon: 'shixinren.svg' },
{ name: '行政案件', icon: 'xingzhengfuwu.svg' },
{ name: '赔偿案件', icon: 'yuepeichang.svg' },
{ name: '执行案件', icon: 'zhixinganjian.svg' },
{ name: '限高被执行', icon: 'xianzhigaoxiaofei.svg' },
]" :key="`${feature.id}-${caseIndex}`"
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(index + caseIndex)">
<div class="mb-1">
<img :src="`/inquire_icons/${caseType.icon}`" :alt="caseType.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;">
{{ caseType.name }}
</div>
</div>
</template>
<!-- DWBG8B4D 特殊处理:显示拆分模块 -->
<template v-else-if="feature.api_id === 'DWBG8B4D'">
<div v-for="(module, moduleIndex) in [
{ name: '要素核查', icon: 'beijianguanrenyuan.svg' },
{ name: '运营商核验', icon: 'mingxiacheliang.svg' },
{ name: '公安重点人员检验', icon: 'xingshi.svg' },
{ name: '逾期风险综述', icon: 'huankuanyali.svg' },
{ name: '法院曝光台信息', icon: 'sifasheyu.svg' },
{ name: '借贷评估', icon: 'jiedaishenqing.svg' },
{ name: '租赁风险评估', icon: 'jiedaixingwei.svg' },
{ name: '关联风险监督', icon: 'renqiguanxi.svg' },
{ name: '规则风险提示', icon: 'fengxianxingwei.svg' },
]" :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(index + moduleIndex)">
<div class="text-xl mb-1 flex items-center justify-center">
<img :src="`/inquire_icons/${module.icon}`" :alt="module.name"
class="w-6 h-6"
@error="handleIconError" />
</div>
<div class="text-xs leading-tight px-1 font-medium"
style="word-break: break-all; line-height: 1.2">
{{ module.name }}
</div>
</div>
</template>
<!-- JRZQ7F1A 特殊处理:显示拆分模块 -->
<template v-else-if="feature.api_id === 'JRZQ7F1A'">
<div v-for="(module, moduleIndex) in [
{ name: '申请行为详情', icon: 'jiedaishenqing.svg' },
{ name: '放款还款详情', icon: 'jiedaixingwei.svg' },
{ name: '大数据详情', icon: 'fengxianxingwei.svg' },
]" :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(index + moduleIndex)">
<div class="text-xl mb-1 flex items-center justify-center">
<img :src="`/inquire_icons/${module.icon}`" :alt="module.name"
class="w-6 h-6"
@error="handleIconError" />
</div>
<div class="text-xs leading-tight px-1 font-medium"
style="word-break: break-all; line-height: 1.2">
{{ module.name }}
</div>
</div>
</template>
<!-- CJRZQ5E9F 特殊处理:显示拆分模块 -->
<template v-else-if="feature.api_id === 'JRZQ5E9F'">
<div v-for="(module, moduleIndex) in [
{ name: '信用评分', icon: 'huankuanyali.svg' },
{ name: '贷款行为分析', icon: 'jiedaixingwei.svg' },
{ name: '机构分析', icon: 'jiedaishenqing.svg' },
{ name: '时间趋势分析', icon: 'zhixinganjian.svg' },
{ name: '风险指标详情', icon: 'fengxianxingwei.svg' },
{ name: '专业建议', icon: 'yuepeichang.svg' },
]" :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(index + moduleIndex)">
<div class="text-xl mb-1 flex items-center justify-center">
<img :src="`/inquire_icons/${module.icon}`" :alt="module.name"
class="w-6 h-6"
@error="handleIconError" />
</div>
<div class="text-xs leading-tight px-1 font-medium"
style="word-break: break-all; line-height: 1.2">
{{ module.name }}
</div>
</div>
</template>
<!-- PersonEnterprisePro/CQYGL3F8E 特殊处理:显示拆分模块 -->
<template v-else-if="feature.api_id === 'PersonEnterprisePro' || feature.api_id === 'QYGL3F8E'">
<div v-for="(module, moduleIndex) in [
{ name: '投资企业记录', icon: 'renqiguanxi.svg' },
{ name: '高管任职记录', icon: 'beijianguanrenyuan.svg' },
{ name: '涉诉风险', icon: 'sifasheyu.svg' },
{ name: '对外投资历史', icon: 'renqiguanxi.svg' },
{ name: '融资历史', icon: 'huankuanyali.svg' },
{ name: '行政处罚', icon: 'xingzhengfuwu.svg' },
{ name: '经营异常', icon: 'fengxianxingwei.svg' },
]" :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(index + moduleIndex)">
<div class="text-xl mb-1 flex items-center justify-center">
<img :src="`/inquire_icons/${module.icon}`" :alt="module.name"
class="w-6 h-6"
@error="handleIconError" />
</div>
<div class="text-xs leading-tight px-1 font-medium"
style="word-break: break-all; line-height: 1.2">
{{ module.name }}
</div>
</div>
</template>
<!-- DWBG6A2C 特殊处理:显示拆分模块 -->
<template v-else-if="feature.api_id === 'DWBG6A2C'">
<div v-for="(module, moduleIndex) in [
{ name: '命中风险标注', icon: 'fengxianxingwei.svg' },
{ name: '公安重点人员核验', icon: 'beijianguanrenyuan.svg' },
{ name: '涉赌涉诈人员核验', icon: 'xingshi.svg' },
{ name: '风险名单', icon: 'jiedaiweiyue.svg' },
{ name: '历史借贷行为', icon: 'jiedaixingwei.svg' },
{ name: '近24个月放款情况', icon: 'jiedaishenqing.svg' },
{ name: '履约情况', icon: 'huankuanyali.svg' },
{ name: '历史逾期记录', icon: 'jiedaiweiyue.svg' },
{ name: '授信详情', icon: 'huankuanyali.svg' },
{ name: '租赁行为', icon: 'mingxiacheliang.svg' },
{ name: '关联风险监督', icon: 'renqiguanxi.svg' },
{ name: '法院风险信息', icon: 'sifasheyu.svg' },
]" :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(index + moduleIndex)">
<div class="text-xl mb-1 flex items-center justify-center">
<img :src="`/inquire_icons/${module.icon}`" :alt="module.name"
class="w-6 h-6"
@error="handleIconError" />
</div>
<div class="text-xs leading-tight px-1 font-medium"
style="word-break: break-all; line-height: 1.2">
{{ module.name }}
</div>
</div>
</template>
<!-- 其他功能正常显示 -->
<div v-else
class="aspect-square rounded-xl text-center text-sm text-gray-700 font-medium flex flex-col items-center justify-between p-2"
:class="getCardClass(index)">
<div class="flex items-center justify-center flex-1">
<img :src="getFeatureIcon(feature.api_id)" :alt="feature.name"
class="w-6 h-6"
@error="handleIconError" />
</div>
<div class="text-xs leading-tight font-medium h-8 flex items-center justify-center"
style="word-break: break-all; line-height: 1.1">
{{ feature.name }}
</div>
</div>
</template>
</div>
</div>
</template>
<script setup>
// Props
const props = defineProps({
features: {
type: Array,
default: () => []
},
containerClass: {
type: String,
default: ''
},
titleStyle: {
type: Object,
default: () => ({})
}
});
// 获取功能图标
const getFeatureIcon = (apiId) => {
const iconMap = {
JRZQ4AA8: "/inquire_icons/huankuanyali.svg",
QCXG7A2B: "/inquire_icons/mingxiacheliang.svg",
QCXG9P1C: "/inquire_icons/mingxiacheliang.svg",
BehaviorRiskScan: "/inquire_icons/fengxianxingwei.svg",
IVYZ5733: "/inquire_icons/hunyinzhuangtai.svg",
IVYZ81NC: "/inquire_icons/hunyinzhuangtai.svg",
IVYZ9A2B: "/inquire_icons/beijianguanrenyuan.svg",
IVYZ7F3A: "/inquire_icons/beijianguanrenyuan.svg",
IVYZ8I9J: "/inquire_icons/fengxianxingwei.svg",
PersonEnterprisePro: "/inquire_icons/renqiguanxi.svg",
QYGL3F8E: "/inquire_icons/renqiguanxi.svg",
JRZQ0A03: "/inquire_icons/jiedaishenqing.svg",
FLXG3D56: "/inquire_icons/jiedaiweiyue.svg",
FLXG0V4B: "/inquire_icons/sifasheyu.svg",
FLXG7E8F: "/inquire_icons/sifasheyu.svg",
FLXGDEA9: "/inquire_icons/shixinren.svg",
JRZQ8203: "/inquire_icons/jiedaixingwei.svg",
JRZQ09J8: "/inquire_icons/beijianguanrenyuan.svg",
JRZQ4B6C: "/inquire_icons/fengxianxingwei.svg",
JRZQ8A2D: "/inquire_icons/jiedaiweiyue.svg",
JRZQ7F1A: "/inquire_icons/fengxianxingwei.svg",
DWBG8B4D: "/inquire_icons/fengxianxingwei.svg",
DWBG6A2C: "/inquire_icons/fengxianxingwei.svg",
JRZQ5E9F: "/inquire_icons/fengxianxingwei.svg",
DWBG7F3A: "/inquire_icons/jiedaishenqing.svg",
YYSY7D3E: "/inquire_icons/mingxiacheliang.svg",
YYSY8B1C: "/inquire_icons/mingxiacheliang.svg",
};
return iconMap[apiId] || "/inquire_icons/default.svg";
};
// 处理图标加载错误
const handleIconError = (event) => {
event.target.style.display = "none";
};
// 获取卡片样式类
const getCardClass = (index) => {
const colorIndex = index % 4;
const colorClasses = [
'bg-gradient-to-br from-blue-50 via-blue-25 to-white border border-blue-100',
'bg-gradient-to-br from-green-50 via-green-25 to-white border border-green-100',
'bg-gradient-to-br from-purple-50 via-purple-25 to-white border border-purple-100',
'bg-gradient-to-br from-orange-50 via-orange-25 to-white border border-orange-100'
];
return colorClasses[colorIndex];
};
</script>

View File

@@ -0,0 +1,17 @@
<template>
<div class="flex items-center">
<div class="flex items-center gap-2">
<div class="w-1.5 h-5 bg-primary rounded-xl"></div>
<div class="text-lg text-gray-800">{{ title }}</div>
</div>
</div>
</template>
<script setup>
defineProps({
title: {
type: String,
required: true
}
});
</script>

View File

@@ -0,0 +1,131 @@
<script setup>
import { ref } from "vue";
import { showToast, showDialog } from "vant";
const props = defineProps({
orderId: {
type: String,
default: "",
},
orderNo: {
type: String,
default: "",
},
isExample: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
});
const isLoading = ref(false);
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;
}
};
</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">
<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 ? "分享示例" : "分享报告") }}
</span>
</div>
</template>
<style lang="scss" scoped>
/* 样式已通过 Tailwind CSS 类实现 */
</style>

View File

@@ -0,0 +1,44 @@
<template>
<van-tabs v-bind="$attrs" type="card" class="styled-tabs" swipe-threshold="4">
<slot></slot>
</van-tabs>
</template>
<script setup>
// 透传所有属性和事件到 van-tabs
defineOptions({
inheritAttrs: false
})
</script>
<style scoped>
/* van-tabs 卡片样式定制 - 仅用于此组件 */
.styled-tabs:deep(.van-tabs__line) {
background-color: var(--color-primary-second) !important;
}
.styled-tabs:deep(.van-tabs__nav) {
gap: 10px;
}
.styled-tabs:deep(.van-tabs__nav--card) {
border: unset !important;
}
.styled-tabs:deep(.van-tab--card) {
color: #666666 !important;
border-right: unset !important;
background-color: #eeeeee !important;
border-radius: 8px !important;
}
.styled-tabs:deep(.van-tab--active) {
color: white !important;
background-color: var(--color-primary-second) !important;
}
.styled-tabs:deep(.van-tabs__wrap) {
background-color: #ffffff !important;
padding: 9px 0;
}
</style>

View File

@@ -0,0 +1,23 @@
<template>
<div class="title-banner">
<slot></slot>
</div>
</template>
<script setup>
// 不需要额外的 props 或逻辑,只是一个简单的样式组件
</script>
<style scoped>
.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);
border: 1px solid var(--color-primary-300);
background-image:
linear-gradient(45deg, transparent 25%, rgba(255, 255, 255, 0.1) 25%, rgba(255, 255, 255, 0.1) 50%, transparent 50%, transparent 75%, rgba(255, 255, 255, 0.1) 75%);
background-size: 20px 20px;
background-position: 0 0;
position: relative;
overflow: hidden;
}
</style>

View File

@@ -0,0 +1,185 @@
<template>
<div class="card" style="padding-left: 0; padding-right: 0; padding-bottom: 24px;">
<div class="flex flex-col gap-y-2">
<!-- 报告信息 -->
<div class="flex items-center justify-between py-2">
<LTitle title="报告信息"></LTitle>
<!-- 分享按钮 -->
<ShareReportButton v-if="!isShare" :order-id="orderId" :order-no="orderNo" :isExample="isExample"
class="mr-4" />
</div>
<div class="flex flex-col gap-2 my-2 mx-4">
<div class="flex pb-2 pl-2">
<span class="text-[#666666] w-[6em]">报告时间</span>
<span class="text-gray-600">{{
reportDateTime ||
"2025-01-01 12:00:00"
}}</span>
</div>
<div class="flex pb-2 pl-2" v-if="!isEmpty">
<span class="text-[#666666] w-[6em]">报告项目</span>
<span class="text-gray-600 font-bold">
{{ reportName }}</span>
</div>
</div>
<!-- 报告对象 -->
<template v-if="Object.keys(reportParams).length != 0">
<LTitle title="报告对象"></LTitle>
<div class="flex flex-col gap-2 my-2 mx-4">
<!-- 姓名 -->
<div class="flex pb-2 pl-2" v-if="reportParams?.name">
<span class="text-[#666666] w-[6em]">姓名</span>
<span class="text-gray-600">{{
maskValue(
"name",
reportParams?.name
)
}}</span>
</div>
<!-- 身份证号 -->
<div class="flex pb-2 pl-2" v-if="reportParams?.id_card">
<span class="text-[#666666] w-[6em]">身份证号</span>
<span class="text-gray-600">{{
maskValue(
"id_card",
reportParams?.id_card
)
}}</span>
</div>
<!-- 手机号 -->
<div class="flex pb-2 pl-2" v-if="reportParams?.mobile">
<span class="text-[#666666] w-[6em]">手机号</span>
<span class="text-gray-600">{{
maskValue(
"mobile",
reportParams?.mobile
)
}}</span>
</div>
<!-- 验证卡片 -->
<div class="flex flex-col gap-4 mt-4">
<!-- 身份证检查结果 -->
<div class="flex items-center px-4 py-3 flex-1 border border-[#EEEEEE] rounded-lg bg-[#F9F9F9]">
<div class="w-11 h-11 flex items-center justify-center mr-4">
<img src="@/assets/images/report/sfz.png" alt="身份证" class="w-10 h-10 object-contain" />
</div>
<div class="flex-1">
<div class="font-bold text-gray-800 text-lg">
身份证检查结果
</div>
<div class="text-sm text-[#999999]">
身份证信息核验通过
</div>
</div>
<div class="w-11 h-11 flex items-center justify-center ml-4">
<img src="@/assets/images/report/zq.png" alt="资金安全" class="w-10 h-10 object-contain" />
</div>
</div>
<!-- 手机号检测结果 -->
<!-- <div class="flex items-center px-4 py-3 flex-1 border border-[#EEEEEE] rounded-lg bg-[#F9F9F9]">
<div class="w-11 h-11 flex items-center justify-center mr-4">
<img src="@/assets/images/report/sjh.png" alt="手机号" class="w-10 h-10 object-contain" />
</div>
<div class="flex-1">
<div class="font-bold text-gray-800 text-lg">
手机号检测结果
</div>
<div class="text-sm text-[#999999]">
被查询人姓名与运营商提供的一致
</div>
<div class="text-sm text-[#999999]">
被查询人身份证与运营商提供的一致
</div>
</div>
<div class="w-11 h-11 flex items-center justify-center ml-4">
<img src="@/assets/images/report/zq.png" alt="资金安全" class="w-10 h-10 object-contain" />
</div>
</div> -->
</div>
</div>
</template>
</div>
</div>
</template>
<script setup>
import LTitle from './LTitle.vue'
import ShareReportButton from './ShareReportButton.vue'
const props = defineProps({
reportParams: {
type: Object,
required: true
},
reportDateTime: {
type: [String, null],
required: false,
default: null
},
reportName: {
type: String,
required: true
},
isEmpty: {
type: Boolean,
required: true
},
isShare: {
type: Boolean,
required: false,
default: false
},
orderId: {
type: [String, Number],
required: false,
default: null
},
orderNo: {
type: String,
required: false,
default: null
},
isExample: {
type: Boolean,
required: false,
default: false
}
})
// 脱敏函数
const maskValue = (type, value) => {
if (!value) return value;
if (type === "name") {
// 姓名脱敏(保留首位)
if (value.length === 1) {
return "*"; // 只保留一个字,返回 "*"
} else if (value.length === 2) {
return value[0] + "*"; // 两个字,保留姓氏,第二个字用 "*" 替代
} else {
return (
value[0] +
"*".repeat(value.length - 2) +
value[value.length - 1]
); // 两个字以上,保留第一个和最后一个字,其余的用 "*" 替代
}
} else if (type === "id_card") {
// 身份证号脱敏保留前6位和最后4位
return value.replace(/^(.{6})(?:\d+)(.{4})$/, "$1****$2");
} else if (type === "mobile") {
if (value.length === 11) {
return value.substring(0, 3) + "****" + value.substring(7);
}
return value; // 如果手机号不合法或长度不为 11 位,直接返回原手机号
}
return value;
};
</script>
<style scoped>
/* 组件样式已通过 Tailwind CSS 类实现 */
</style>

View File

@@ -0,0 +1,18 @@
<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,72 @@
<template>
<div v-if="isWeChat" class="wechat-overlay">
<div class="wechat-content">
<p class="wechat-message">
点击右上角的<van-icon class="ml-2" name="weapp-nav" /><br />然后点击在浏览器中打开
</p>
<img src="@/assets/images/llqdk.jpg" alt="In WeChat" class="wechat-image" />
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
// 定义一个响应式变量,表示是否在微信环境
const isWeChat = ref(false);
// 检查是否为微信环境
const checkIfWeChat = () => {
const userAgent = navigator.userAgent.toLowerCase();
isWeChat.value = /micromessenger/.test(userAgent);
};
// 在组件挂载后检查环境
onMounted(() => {
checkIfWeChat();
});
</script>
<style scoped>
/* 遮罩层样式 */
.wechat-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
z-index: 9999;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
/* 遮罩中的内容 */
.wechat-content {
text-align: center;
color: white;
font-size: 16px;
}
/* 图片样式 */
.wechat-image {
/* position: absolute;
bottom: 0;
left: 0; */
margin-top: 20px;
width: 100%;
}
/* 提示信息的样式 */
.wechat-message {
font-size: 24px;
}
/* 图标样式 */
.icon-more-vert {
font-size: 20px;
margin-left: 5px;
}
</style>