f
This commit is contained in:
263
src/components/AgentApplicationForm.vue
Normal file
263
src/components/AgentApplicationForm.vue
Normal 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>
|
||||
177
src/components/Authorization.vue
Normal file
177
src/components/Authorization.vue
Normal 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>
|
||||
940
src/components/BaseReport.vue
Normal file
940
src/components/BaseReport.vue
Normal 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]">
|
||||
|
||||
1、本份报告是在取得您个人授权后,我们才向合法存有您以上个人信息的机构去调取相关内容,我们不会以任何形式对您的报告进行存储,除您和您授权的人外不会提供给任何人和机构进行查看。
|
||||
</p>
|
||||
<p class="text-[#999999]">
|
||||
2、本报告自生成之日起,有效期 30
|
||||
天,过期自动删除。如果您对本份报告存有异议,可能是合作机构数据有延迟或未能获取到您的相关数据,出于合作平台数据隐私的保护,本平台将不做任何解释。
|
||||
</p>
|
||||
<p class="text-[#999999]">
|
||||
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>
|
||||
266
src/components/BindPhoneDialog.vue
Normal file
266
src/components/BindPhoneDialog.vue
Normal 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>
|
||||
333
src/components/CarNumberInput.vue
Normal file
333
src/components/CarNumberInput.vue
Normal file
File diff suppressed because one or more lines are too long
521
src/components/ClickCaptcha.vue
Normal file
521
src/components/ClickCaptcha.vue
Normal 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>
|
||||
303
src/components/GaugeChart.vue
Normal file
303
src/components/GaugeChart.vue
Normal 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>
|
||||
259
src/components/ImageSaveGuide.vue
Normal file
259
src/components/ImageSaveGuide.vue
Normal 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>
|
||||
579
src/components/InquireForm.vue
Normal file
579
src/components/InquireForm.vue
Normal 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>
|
||||
79
src/components/LButtonGroup.vue
Normal file
79
src/components/LButtonGroup.vue
Normal 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
41
src/components/LEmpty.vue
Normal 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>
|
||||
55
src/components/LExpandCollapse.vue
Normal file
55
src/components/LExpandCollapse.vue
Normal 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>
|
||||
85
src/components/LPendding.vue
Normal file
85
src/components/LPendding.vue
Normal 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>
|
||||
92
src/components/LRemark.vue
Normal file
92
src/components/LRemark.vue
Normal 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
80
src/components/LTable.vue
Normal 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
27
src/components/LTitle.vue
Normal 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
173
src/components/Payment.vue
Normal 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>
|
||||
161
src/components/PriceInputPopup.vue
Normal file
161
src/components/PriceInputPopup.vue
Normal 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
648
src/components/QRcode.vue
Normal 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>
|
||||
14
src/components/QrcodePop.vue
Normal file
14
src/components/QrcodePop.vue
Normal 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>
|
||||
376
src/components/RealNameAuthDialog.vue
Normal file
376
src/components/RealNameAuthDialog.vue
Normal 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
84
src/components/Remark.vue
Normal 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>
|
||||
|
||||
251
src/components/ReportFeatures.vue
Normal file
251
src/components/ReportFeatures.vue
Normal 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>
|
||||
|
||||
17
src/components/SectionTitle.vue
Normal file
17
src/components/SectionTitle.vue
Normal 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>
|
||||
131
src/components/ShareReportButton.vue
Normal file
131
src/components/ShareReportButton.vue
Normal 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>
|
||||
44
src/components/StyledTabs.vue
Normal file
44
src/components/StyledTabs.vue
Normal 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>
|
||||
23
src/components/TitleBanner.vue
Normal file
23
src/components/TitleBanner.vue
Normal 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>
|
||||
185
src/components/VerificationCard.vue
Normal file
185
src/components/VerificationCard.vue
Normal 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>
|
||||
18
src/components/VipBanner.vue
Normal file
18
src/components/VipBanner.vue
Normal 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>
|
||||
72
src/components/WechatOverlay.vue
Normal file
72
src/components/WechatOverlay.vue
Normal 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>
|
||||
Reference in New Issue
Block a user