This commit is contained in:
2025-09-27 17:41:14 +08:00
commit ff5cb63960
301 changed files with 61730 additions and 0 deletions

View File

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

View File

@@ -0,0 +1,175 @@
<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">海南天远大数据科技有限公司</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>
你通过天远数据自愿支付相应费用用于购买海南天远大数据科技有限公司的大数据报告产品如若对产品内容存在异议可通过邮箱admin@iieeii.com或APP联系客服按钮进行反馈贵司将在收到异议之日起20日内进行核查和处理并将结果答复
</li>
<li>
你向海南天远大数据科技有限公司的支付方式为海南天远大数据科技有限公司及其经官方授权的相关企业的支付宝账户
</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 emit = defineEmits(['agreed', 'cancel']); // 定义事件
const props = defineProps({
name: {
type: String,
required: true,
},
idCard: {
type: String,
required: true,
},
mobile: {
type: String,
required: true,
},
})
const signature = ref(false);
const formatDate = (date) => {
const options = { year: "numeric", month: "long", day: "numeric" };
return new Intl.DateTimeFormat("zh-CN", options).format(date);
};
const signTime = ref(formatDate(new Date()));
const canAgree = ref(false); // 同意按钮状态
const scrollMessage = ref("请滑动并阅读完整授权书以继续");
// 滚动事件处理
let timeout = null;
const handleScroll = (event) => {
clearTimeout(timeout);
timeout = setTimeout(() => {
const element = event.target;
if (
Math.abs(
element.scrollHeight - element.scrollTop - element.clientHeight
) <= 50
) {
canAgree.value = true;
scrollMessage.value = "您已阅读完整授权书,可以继续";
}
}, 100);
};
// 用户同意
const agree = () => {
if (signature.value) {
emit("agreed")
return
}
signature.value = true
};
// 用户取消
const cancel = () => {
emit("cancel")
};
</script>
<style scoped></style>

View File

@@ -0,0 +1,947 @@
<script setup>
import ShareReportButton from "./ShareReportButton.vue";
import { splitDWBG8B4DForTabs } from '@/ui/CDWBG8B4D/utils/simpleSplitter.js';
import { splitDWBG6A2CForTabs } from '@/ui/DWBG6A2C/utils/simpleSplitter.js';
import { splitCJRZQ5E9FForTabs } from '@/ui/CJRZQ5E9F/utils/simpleSplitter.js';
import { splitCQYGL3F8EForTabs } from '@/ui/CQYGL3F8E/utils/simpleSplitter.js';
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,
},
});
// 使用toRefs将props转换为组件内的ref
const {
feature,
reportData,
reportParams,
reportName,
reportDateTime,
isEmpty,
isDone,
} = toRefs(props);
const active = ref(null);
const reportScore = ref(0); // 默认分数
// 处理数据拆分支持DWBG8B4D、DWBG6A2C、CJRZQ5E9F和CQYGL3F8E
const processedReportData = computed(() => {
let data = reportData.value;
// 拆分DWBG8B4D数据
data = splitDWBG8B4DForTabs(data);
// 拆分DWBG6A2C数据
data = splitDWBG6A2CForTabs(data);
// 拆分CJRZQ5E9F数据
data = splitCJRZQ5E9FForTabs(data);
// 拆分CQYGL3F8E数据
data = splitCQYGL3F8EForTabs(data);
return data;
});
watch(reportData, () => {
reportScore.value = calculateScore(reportData.value);
});
const featureMap = {
IVYZ5733: {
name: "婚姻状态",
component: defineAsyncComponent(() => import("@/ui/CIVYZ5733.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")),
},
BehaviorRiskScan: {
name: "风险行为扫描",
component: defineAsyncComponent(() =>
import("@/ui/CBehaviorRiskScan.vue")
),
},
JRZQ4AA8: {
name: "还款压力",
component: defineAsyncComponent(() => import("@/ui/CJRZQ4AA8.vue")),
},
IVYZ9A2B: {
name: "学历信息查询",
component: defineAsyncComponent(() => import("@/ui/CIVYZ9A2B.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")),
}
};
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;
};
});
// 计算综合评分的函数
const calculateScore = (reportData) => {
// 从0分开始0分表示无风险
let score = 0;
// 最高分为90分90分表示最高风险
const maxScore = 90;
// 定义各接口的相对风险权重比例
const relativeWeights = {
// 关键风险指标(高优先级)
FLXG0V3B: 250, // 不良记录
FLXG3D56: 100, // 违约异常
FLXG0V4B: 400, // 司法涉诉
G35SC01: 20, // 司法涉诉(次要)
Q23SC01: 50, // 企业涉诉
FIN019: 100, // 银行卡黑名单
// 高风险指标(中优先级)
JRZQ0A03: 40, // 借贷申请记录
JRZQ8203: 40, // 借贷行为记录
FLXG54F5: 70, // 手机号码风险
// 中风险指标(低优先级)
YYSYF7DB: 50, // 手机二次卡
YYSY4B37: 50, // 手机在网时长
QYGLB4C0: 50, // 人企关系
JRZQ4B6C: 60, // 信贷表现
JRZQ09J8: 40, // 收入评估
// 验证指标(最低优先级)
YYSY6F2E: 25, // 手机三要素
IVYZ0B03: 25, // 手机号二要素
KZEYS: 25, // 身份证二要素
JRZQDCBE: 25, // 银行卡四要素核验
};
// 找出当前报告中包含的接口
const availableAPIs = reportData
.map((item) => item.data.apiID)
.filter((id) => relativeWeights[id]);
// 如果没有可评分的接口,返回默认分数
if (availableAPIs.length === 0) return 30; // 默认30分中等风险
// 计算当前报告中所有接口的相对权重总和
let totalWeight = 0;
availableAPIs.forEach((apiID) => {
totalWeight += relativeWeights[apiID];
});
// 计算每个权重点对应的分数
const pointValue = maxScore / totalWeight;
// 基于当前报告中的接口计算实际权重
const actualWeights = {};
availableAPIs.forEach((apiID) => {
// 将相对权重转换为实际分数权重
actualWeights[apiID] = relativeWeights[apiID] * pointValue;
});
// 遍历报告数据进行评分 - 风险越高分数越高
reportData.forEach((item) => {
const apiID = item.data.apiID;
const data = item.data.data;
// 如果没有定义权重,跳过
if (!actualWeights[apiID]) return;
// 根据不同的API ID计算分数有风险时加分
switch (apiID) {
case "G09SC02": // 婚姻状态
// 不计入风险
break;
case "JRZQ0A03": // 借贷申请记录
if (data) {
// 检查是否有申请记录(有则表示风险)
let hasRisk = false;
for (const key in data) {
if (
data[key] !== 0 &&
data[key] !== "0" &&
key.indexOf("allnum") > -1 &&
!isNaN(parseInt(data[key])) &&
parseInt(data[key]) > 0
) {
hasRisk = true;
break;
}
}
if (hasRisk) {
score += actualWeights[apiID];
}
}
break;
case "JRZQ8203": // 借贷行为记录
if (data) {
// 检查是否有借贷记录(有则表示风险)
let hasRisk = false;
for (const key in data) {
if (
data[key] !== 0 &&
data[key] !== "0" &&
(key.indexOf("lendamt") > -1 ||
key.indexOf("num") > -1) &&
!isNaN(parseInt(data[key])) &&
parseInt(data[key]) > 0
) {
hasRisk = true;
break;
}
}
if (hasRisk) {
score += actualWeights[apiID];
}
}
break;
case "FLXG3D56": // 违约异常
if (data) {
// 检查除特定字段外的其他字段是否有异常值(有异常则表示风险)
const excludeFields = [
"swift_number",
"code",
"flag_specialList_c",
];
let hasRisk = false;
for (const key in data) {
if (
!excludeFields.includes(key) &&
data[key] !== 0 &&
data[key] !== "0"
) {
hasRisk = true;
break;
}
}
if (hasRisk) {
score += actualWeights[apiID];
}
}
break;
case "FLXG0V3B": // 不良记录
if (data && data.risk_level) {
// 根据风险等级加分
switch (data.risk_level) {
case "A": // 无风险
// 不加分
break;
case "F": // 低风险
score += actualWeights[apiID] * 0.3;
break;
case "C": // 中风险
case "D": // 中风险
score += actualWeights[apiID] * 0.7;
break;
case "B": // 高风险
case "E": // 高风险
score += actualWeights[apiID];
break;
}
}
break;
case "G35SC01": // 司法涉诉
case "FLXG0V4B": // 司法涉诉
case "Q23SC01": // 企业涉诉
if (data) {
let hasRisk = false;
// 检查各种涉诉信息 - 处理嵌套数据结构
// entout是一个单元素数组数组中第一个元素是JSON对象对象中有entout属性
if (
data.entout &&
Array.isArray(data.entout) &&
data.entout.length > 0 &&
data.entout[0] &&
data.entout[0].entout &&
((Array.isArray(data.entout[0].entout) &&
data.entout[0].entout.length > 0) ||
(typeof data.entout[0].entout === "object" &&
Object.keys(data.entout[0].entout).length > 0))
) {
hasRisk = true;
}
// 处理sxbzxr(失信被执行人)嵌套结构
if (
data.sxbzxr &&
Array.isArray(data.sxbzxr) &&
data.sxbzxr.length > 0 &&
data.sxbzxr[0] &&
data.sxbzxr[0].sxbzxr &&
((Array.isArray(data.sxbzxr[0].sxbzxr) &&
data.sxbzxr[0].sxbzxr.length > 0) ||
(typeof data.sxbzxr[0].sxbzxr === "object" &&
Object.keys(data.sxbzxr[0].sxbzxr).length > 0))
) {
hasRisk = true;
}
// 处理xgbzxr(限制高消费被执行人)嵌套结构
if (
data.xgbzxr &&
Array.isArray(data.xgbzxr) &&
data.xgbzxr.length > 0 &&
data.xgbzxr[0] &&
data.xgbzxr[0].xgbzxr &&
((Array.isArray(data.xgbzxr[0].xgbzxr) &&
data.xgbzxr[0].xgbzxr.length > 0) ||
(typeof data.xgbzxr[0].xgbzxr === "object" &&
Object.keys(data.xgbzxr[0].xgbzxr).length > 0))
) {
hasRisk = true;
}
if (hasRisk) {
score += actualWeights[apiID];
}
}
break;
case "FLXG54F5": // 手机号码风险
if (data && data.filterType) {
// 根据filterType判断风险等级
switch (data.filterType) {
case "0": // 安全
// 不加分
break;
case "3": // 低危
score += actualWeights[apiID] * 0.3;
break;
case "2": // 中危
score += actualWeights[apiID] * 0.7;
break;
case "1": // 高危
score += actualWeights[apiID];
break;
}
}
break;
case "YYSYF7DB": // 手机二次卡
if (data && data.is_second_card === true) {
score += actualWeights[apiID];
}
break;
case "YYSY4B37": // 手机在网时长
if (data && data.online_months < 6) {
score += actualWeights[apiID];
}
break;
case "YYSY6F2E": // 手机三要素
case "IVYZ0B03": // 手机号二要素
case "KZEYS": // 身份证二要素
case "JRZQDCBE": // 银行卡四要素核验
if (data && data.is_consistent === false) {
score += actualWeights[apiID];
}
break;
case "FIN019": // 银行卡黑名单
if (data && data.is_blacklisted === true) {
score += actualWeights[apiID];
}
break;
case "JRZQ4B6C": // 信贷表现
if (data) {
let riskScore = 0;
// 根据结果编码评分
switch (data.result_code) {
case "1": // A(Overdue) - 逾期,高风险
riskScore += 0.8;
break;
case "3": // B(Delay) - 延迟,中风险
riskScore += 0.5;
break;
case "2": // B(Normal) - 正常,低风险
riskScore += 0.1;
break;
case "4": // U - 未知,中等风险
riskScore += 0.3;
break;
}
// 当前逾期机构数
const currentlyOverdue = parseInt(data.currently_overdue) || 0;
if (currentlyOverdue > 0) {
riskScore += Math.min(currentlyOverdue * 0.1, 0.3);
}
// 异常还款机构数
const accExc = parseInt(data.acc_exc) || 0;
if (accExc > 0) {
riskScore += Math.min(accExc * 0.05, 0.2);
}
// 应用风险评分
score += actualWeights[apiID] * Math.min(riskScore, 1.0);
}
break;
case "JRZQ09J8": // 收入评估
if (data && data.level) {
let riskScore = 0;
// 根据收入等级评分(收入越低风险越高)
switch (data.level) {
case "-": // 无记录,高风险
riskScore = 0.9;
break;
case "A": // A级中高风险
riskScore = 0.7;
break;
case "B": // B级中等风险
riskScore = 0.5;
break;
case "C": // C级中低风险
riskScore = 0.3;
break;
case "D": // D级低风险
riskScore = 0.2;
break;
case "E": // E级很低风险
riskScore = 0.1;
break;
case "F": // F级极低风险
case "G": // G级极低风险
case "H": // H级无风险
case "I": // I级无风险
case "J": // J级零风险
riskScore = 0.05;
break;
}
// 应用风险评分
score += actualWeights[apiID] * riskScore;
}
break;
default:
// 未知接口类型不影响评分
break;
}
});
// 确保分数在0-90范围内并四舍五入
return Math.max(0, Math.min(maxScore, Math.round(score)));
};
</script>
<template>
<div class="min-h-full from-blue-100 to-white bg-gradient-to-b">
<template v-if="isDone">
<van-tabs 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 id="analysis" class="title mb-4">分析指数</div>
<div class="card mb-4">
<div class="my-4">
<GaugeChart :score="reportScore" />
</div>
</div>
</van-tab>
<van-tab title="基本信息">
<div id="basic" class="title mb-4">基本信息</div>
<div class="card">
<div class="flex flex-col gap-y-2">
<LTitle title="报告信息" type="blue-green"></LTitle>
<div class="flex flex-col gap-2 my-2">
<div class="flex justify-between border-b pb-2 pl-2">
<span class="text-gray-700 font-bold">报告时间</span>
<span class="text-gray-600">{{
reportDateTime ||
"2025-01-01 12:00:00"
}}</span>
</div>
<div class="flex justify-between border-b pb-2 pl-2" v-if="!isEmpty">
<span class="text-gray-700 font-bold">报告项目</span>
<span class="text-gray-600">
{{ reportName }}</span>
</div>
</div>
<template v-if="Object.keys(reportParams).length != 0">
<LTitle title="报告对象" type="blue-green"></LTitle>
<div class="flex flex-col gap-2 my-2">
<div class="flex justify-between border-b pb-2 pl-2" v-if="reportParams?.name">
<span class="text-gray-700 font-bold">姓名</span>
<span class="text-gray-600">{{
maskValue(
"name",
reportParams?.name
)
}}</span>
</div>
<div class="flex justify-between border-b pb-2 pl-2"
v-if="reportParams?.id_card">
<span class="text-gray-700 font-bold">身份证号</span>
<span class="text-gray-600">
{{
maskValue(
"id_card",
reportParams?.id_card
)
}}</span>
</div>
<div class="flex justify-between border-b pb-2 pl-2"
v-if="reportParams?.mobile">
<span class="text-gray-700 font-bold">手机号</span>
<span class="text-gray-600">{{
maskValue(
"mobile",
reportParams?.mobile
)
}}</span>
</div>
<div class="flex flex-col gap-4">
<div class="flex items-center bg-blue-100 rounded-xl px-4 py-2 flex-1">
<div
class="bg-green-500 w-12 h-12 text-white text-xl flex items-center justify-center rounded-full mr-4">
</div>
<div>
<div class="font-bold text-lg">
身份证检查结果
</div>
<div class="text-sm text-gray-600">
身份证信息核验通过
</div>
</div>
</div>
<div class="flex items-center bg-blue-100 rounded-xl px-4 py-2 flex-1">
<div
class="bg-green-500 w-12 h-12 text-white text-xl flex items-center justify-center rounded-full mr-4">
</div>
<div>
<div class="font-bold text-lg">
手机号检测结果
</div>
<div class="text-sm text-gray-600">
被查询人姓名与运营商提供的一致
</div>
<div class="text-sm text-gray-600">
被查询人身份证与运营商提供的一致
</div>
</div>
</div>
</div>
</div>
</template>
</div>
<ShareReportButton v-if="!isShare" :order-id="orderId" :order-no="orderNo"
:is-example="!orderId" />
</div>
<LRemark content="如查询的姓名/身份证与运营商提供的不一致,可能会存在报告内容不匹配的情况" />
</van-tab>
<van-tab v-for="(item, index) in processedReportData" :key="index"
:title="featureMap[item.data.apiID]?.name">
<div :id="item.data.apiID" class="title mb-4">
{{ featureMap[item.data.apiID]?.name }}
</div>
<component :is="featureMap[item.data.apiID]?.component" :data="item.data.data"
:params="reportParams">
</component>
<LRemark v-if="featureMap[item.data.apiID]?.remark"
:content="featureMap[item.data.apiID]?.remark" />
</van-tab>
<ShareReportButton v-if="!isShare" class="mb-4" :order-id="orderId" :order-no="orderNo"
:is-example="!orderId" />
<div class="card">
<div>
<div class="text-bold text-blue-500 mb-2">
报告说明
</div>
<div>
&nbsp;
&nbsp;本报告的数据由用户本人明确授权后我们才向相关合法存有用户个人数据的机构调取本报告相关内容本平台只做大数据的获取与分析仅向用户个人展示参考
</div>
<p>
&nbsp; &nbsp; 报告有效期<strong class="text-red-500">30</strong>过期自动删除
</p>
<p>
&nbsp; &nbsp;
若您的数据不全面可能是数据具有延迟性或者合作信息机构未获取到您的数据若数据有错误请联系客服
</p>
<p>
&nbsp;
&nbsp;本产品所有数据均来自第三方可能部分数据未公开数据更新延迟或信息受到限制贵司不对数据的准确性真实性完整性做任何承诺用户需根据实际情况结合报告内容自行判断与决策
</p>
</div>
</div>
</div>
</van-tabs>
</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>
.title {
@apply mx-auto mt-2 w-64 border rounded-3xl py-2 text-center text-white font-bold;
background: linear-gradient(135deg, var(--van-theme-primary), var(--van-theme-primary-dark));
}
.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;
}
</style>

View File

@@ -0,0 +1,256 @@
<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();
agentStore.fetchAgentStatus();
userStore.fetchUserInfo();
// 发出绑定成功的事件
emit('bind-success')
} 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.jpg" alt="Logo" />
<div class="text-3xl mt-4 text-slate-700 font-bold">
天远数据
</div>
</div>
</div>
<div class="space-y-5">
<!-- 手机号输入 -->
<div :class="[
'input-container bg-blue-300/20',
phoneFocused ? 'focused' : '',
]">
<input v-model="phoneNumber" class="input-field" type="tel" placeholder="请输入手机号"
maxlength="11" @focus="phoneFocused = true" @blur="phoneFocused = false" />
</div>
<!-- 验证码输入 -->
<div class="flex items-center justify-between">
<div :class="[
'input-container bg-blue-300/20',
codeFocused ? 'focused' : '',
]">
<input v-model="verificationCode" id="verificationCode" class="input-field"
placeholder="请输入验证码" maxlength="6" @focus="codeFocused = true"
@blur="codeFocused = false" />
</div>
<button
class="ml-2 px-4 py-2 text-sm font-bold flex-shrink-0 rounded-lg transition duration-300"
:class="isCountingDown || !isPhoneNumberValid
? 'cursor-not-allowed bg-gray-300 text-gray-500'
: 'bg-blue-500 text-white hover:bg-blue-600'
" @click="sendVerificationCode">
{{
isCountingDown
? `${countdown}s重新获取`
: "获取验证码"
}}
</button>
</div>
<!-- 协议同意框 -->
<div class="flex items-start space-x-2">
<input type="checkbox" v-model="isAgreed" class="mt-1" />
<span class="text-xs text-gray-400 leading-tight">
绑定手机号即代表您已阅读并同意
<a class="cursor-pointer text-blue-400" @click="toUserAgreement">
用户协议
</a>
<a class="cursor-pointer text-blue-400" @click="toPrivacyPolicy">
隐私政策
</a>
</span>
</div>
</div>
<button
class="mt-10 w-full py-3 text-lg font-bold text-white bg-blue-500 rounded-full transition duration-300"
:class="{ 'opacity-50 cursor-not-allowed': !canBind }" @click="handleBind">
确认绑定
</button>
</div>
</div>
</van-popup>
</div>
</template>
<style scoped>
.bind-phone-dialog {
background: url("@/assets/images/login_bg.png") no-repeat;
background-position: center;
background-size: cover;
height: 100%;
}
.title-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid #eee;
}
.close-icon {
font-size: 20px;
color: #666;
cursor: pointer;
}
.input-container {
border: 2px solid rgba(125, 211, 252, 0);
border-radius: 1rem;
transition: duration-200;
}
.input-container.focused {
border: 2px solid #3b82f6;
}
.input-field {
width: 100%;
padding: 1rem;
background: transparent;
border: none;
outline: none;
transition: border-color 0.3s ease;
}
</style>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,391 @@
<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 = '#2563eb'
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 })
console.log(
'clickedList:',
clickedList.value.map(i => i.char)
) // 调试
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.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
padding: 1rem;
}
.captcha-modal {
background: #fff;
border-radius: 1rem;
width: 100%;
max-width: 340px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.18);
overflow: hidden;
}
.captcha-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
border-bottom: 1px solid #e5e7eb;
background: #f8fafc;
}
.captcha-title {
font-size: 1.125rem;
font-weight: 600;
color: #1f2937;
margin: 0;
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
color: #6b7280;
cursor: pointer;
padding: 0.25rem;
border-radius: 0.25rem;
transition: color 0.2s;
}
.close-btn:hover {
color: #374151;
}
.captcha-content {
padding: 1.2rem 1.2rem 0.5rem 1.2rem;
}
.captcha-canvas {
width: 100%;
border-radius: 0.75rem;
background: #e0e7ef;
display: block;
margin: 0 auto;
}
.captcha-instruction {
margin: 1rem 0 0.5rem 0;
text-align: center;
}
.target-list {
color: #3b82f6;
font-weight: 600;
}
.captcha-status {
text-align: center;
min-height: 1.5rem;
}
.error-message {
color: #dc2626;
font-size: 0.95em;
margin: 0;
}
.success-message {
color: #059669;
font-size: 0.95em;
margin: 0;
}
.status-text {
color: #6b7280;
font-size: 0.95em;
margin: 0;
}
.captcha-footer {
padding: 1rem 1.5rem;
border-top: 1px solid #e5e7eb;
background: #f8fafc;
}
.refresh-btn {
width: 100%;
padding: 0.75rem;
background: #3b82f6;
color: white;
border: none;
border-radius: 0.5rem;
font-size: 0.95em;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
}
.refresh-btn:hover:not(:disabled) {
background: #2563eb;
}
.refresh-btn:disabled {
background: #9ca3af;
cursor: not-allowed;
}
.confirm-btn {
width: 100%;
margin-top: 0.5rem;
padding: 0.75rem;
background: #059669;
color: white;
border: none;
border-radius: 0.5rem;
font-size: 0.95em;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
}
.confirm-btn:disabled {
background: #9ca3af;
cursor: not-allowed;
}
@media (max-width: 480px) {
.captcha-modal {
max-width: 100%;
}
.captcha-content {
padding: 0.7rem;
}
.captcha-canvas {
min-height: 120px;
}
}
</style>

View File

@@ -0,0 +1,255 @@
<template>
<div>
<div ref="chartRef" :style="{ width: '100%', height: '200px' }"></div>
<div class="risk-description">
{{ riskDescription }}
</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 >= 0 && score <= 25) {
return {
level: "无任何风险",
color: "#52c41a",
gradient: [
{ offset: 0, color: "#52c41a" },
{ offset: 1, color: "#7fdb42" }
]
};
} else if (score > 25 && score <= 50) {
return {
level: "风险指数较低",
color: "#faad14",
gradient: [
{ offset: 0, color: "#faad14" },
{ offset: 1, color: "#ffc53d" }
]
};
} else if (score > 50 && score <= 75) {
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 >= 0 && score <= 25) {
return "根据综合分析,当前报告未检测到明显风险因素,各项指标表现正常,总体状况良好。";
} else if (score > 25 && score <= 50) {
return "根据综合分析,当前报告存在少量风险信号,建议关注相关指标变化,保持警惕。";
} else if (score > 50 && score <= 75) {
return "根据综合分析,当前报告风险指数较高,多项指标显示异常,建议进一步核实相关情况。";
} else {
return "根据综合分析,当前报告显示高度风险状态,多项重要指标严重异常,请立即采取相应措施。";
}
});
const chartRef = ref(null);
let chartInstance = null;
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: 10,
shadowColor: 'rgba(0, 0, 0, 0.2)',
},
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: "rgba(0, 0, 0, 0.1)"
},
{
offset: 1,
color: "rgba(0, 0, 0, 0.05)"
}
])]
]
}
},
axisTick: {
show: false
},
splitLine: {
show: true,
distance: -26,
length: 5,
lineStyle: {
color: "#999",
width: 2
}
},
axisLabel: {
show: true,
distance: -8,
fontSize: 12,
color: "#999",
formatter: function (value) {
if (value % 20 === 0) {
return value;
}
return "";
}
},
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-top: 10px;
padding: 8px 12px;
background-color: #f5f5f5;
border-radius: 4px;
color: #666;
font-size: 12px;
line-height: 1.5;
text-align: center;
}
</style>

View File

@@ -0,0 +1,511 @@
<script setup>
import { ref, reactive, computed, onMounted, onUnmounted } from "vue";
import { aesEncrypt } from "@/utils/crypto";
import { useRoute } from "vue-router";
import CarNumberInput from "@/components/CarNumberInput.vue";
const route = useRoute();
const router = useRouter()
const showAuthorizationPopup = ref(false);
const authorization = ref(true);
const showPayment = ref(false);
const queryId = ref(null);
const name = ref("");
const nameMan = ref("");
const nameWoman = ref("");
const idCard = ref("");
const idCardMan = ref("");
const idCardWoman = ref("");
const mobile = ref("");
const bankCard = ref("");
const startDate = ref([])
const dateVal = ref("")
const showDatePicker = ref(false)
// 当前日期
const today = new Date();
const maxDate = today; // 最大日期为当前日期
// 最小日期为2000年1月1日
const minDate = new Date('2000-01-01');
const entName = ref("");
const entCode = ref("");
const verificationCode = ref("");
const agreeToTerms = ref(false);
const isCountingDown = ref(false);
const countdown = ref(60);
const feature = ref(route.params.feature);
const featureData = ref({});
const carLicense = ref("");
const carType = ref("小型汽车");
const carPickerVal = ref([{ value: "02", text: "小型汽车" }]);
const showCarTypePicker = ref(false);
const carTypeColumns = [
{ value: "01", text: "大型汽车" },
{ value: "02", text: "小型汽车" },
{ value: "03", text: "使馆汽车" },
{ value: "04", text: "领馆汽车" },
{ value: "05", text: "境外汽车" },
{ value: "06", text: "外籍汽车" },
{ value: "07", text: "普通摩托车" },
{ value: "08", text: "轻便摩托车" },
{ value: "09", text: "使馆摩托车" },
{ value: "10", text: "领馆摩托车" },
{ value: "11", text: "境外摩托车" },
{ value: "12", text: "外籍摩托车" },
{ value: "13", text: "低速车" },
{ value: "14", text: "拖拉机" },
{ value: "15", text: "挂车" },
{ value: "16", text: "教练汽车" },
{ value: "17", text: "教练摩托车" },
{ value: "20", text: "临时入境汽车" },
{ value: "21", text: "临时入境摩托车" },
{ value: "22", text: "临时行驶车" },
{ value: "23", text: "警用汽车" },
{ value: "24", text: "警用摩托车" },
{ value: "51", text: "新能源大型车" },
{ value: "52", text: "新能源小型车" },
];
const formatterDate = (type, option) => {
if (type === 'year') {
option.text += '年';
}
if (type === 'month') {
option.text += '月';
}
if (type === 'day') {
option.text += '日';
}
return option;
};
const onConfirmDate = ({ selectedValues, selectedOptions }) => {
dateVal.value = selectedOptions.map(item => item.text).join('');
showDatePicker.value = false
}
const carLicenseChange = (e) => {
carLicense.value = e;
};
const onConfirmCarType = ({ selectedValues, selectedOptions }) => {
showCarTypePicker.value = false;
carPickerVal.value = selectedValues;
carType.value = selectedOptions[0].text;
};
onMounted(() => {
isFinishPayment()
getProduct();
initAuthorization();
});
const discountPrice = ref(false) // 是否应用折扣
function isFinishPayment() {
const query = new URLSearchParams(window.location.search);
let orderNo = query.get("out_trade_no");
if (orderNo) {
router.push({ path: '/report', query: { orderNo } });
}
}
async function getProduct() {
const { data, error } = await useApiFetch(`/product/en/${feature.value}`)
.get()
.json();
if (data.value) {
featureData.value = data.value.data;
}
}
function initAuthorization() {
if (NeedAuthorization.includes(feature.value)) {
authorization.value = false;
}
}
const isPhoneNumberValid = computed(() => {
return /^1[3-9]\d{9}$/.test(mobile.value);
});
const isIdCardValid = computed(() => /^\d{17}[\dX]$/i.test(idCard.value));
const isIdCardManValid = computed(() => /^\d{17}[\dX]$/i.test(idCardMan.value));
const isIdCardWomanValid = computed(() =>
/^\d{17}[\dX]$/i.test(idCardWoman.value)
);
const isCreditCodeValid = computed(() => /^.{18}$/.test(entCode.value));
const isCarLicense = computed(() => carLicense.value.trim().length > 6);
const isBankCardValid = computed(() => {
const card = bankCard.value.replace(/\D/g, ""); // 移除所有非数字字符
if (card.length < 13 || card.length > 19) {
return false; // 校验长度
}
let sum = 0;
let shouldDouble = false;
// 从卡号的右边开始遍历
for (let i = card.length - 1; i >= 0; i--) {
let digit = parseInt(card.charAt(i));
if (shouldDouble) {
digit *= 2;
if (digit > 9) {
digit -= 9;
}
}
sum += digit;
shouldDouble = !shouldDouble; // 反转是否乘 2
}
return sum % 10 === 0; // 如果最终和能被 10 整除,则银行卡号有效
});
function handleSubmit() {
if (!agreeToTerms.value) {
showToast({ message: `请阅读并同意用户协议、隐私政策${!NeedAuthorization.includes(feature.value) ? '和授权书' : ''}` });
return;
}
if (
!validateField("name", name.value, (v) => v, "请输入姓名") ||
!validateField("nameMan", nameMan.value, (v) => v, "请输入男方姓名") ||
!validateField(
"nameWoman",
nameWoman.value,
(v) => v,
"请输入女方姓名"
) ||
!validateField(
"mobile",
mobile.value,
(v) => isPhoneNumberValid.value,
"请输入有效的手机号"
) ||
!validateField(
"idCard",
idCard.value,
(v) => isIdCardValid.value,
"请输入有效的身份证号码"
) ||
!validateField(
"idCardMan",
idCardMan.value,
(v) => isIdCardManValid.value,
"请输入有效的男方身份证号码"
) ||
!validateField(
"idCardWoman",
idCardWoman.value,
(v) => isIdCardWomanValid.value,
"请输入有效的女方身份证号码"
) ||
!validateField(
"bankCard",
bankCard.value,
(v) => isBankCardValid.value,
"请输入有效的银行卡号码"
) ||
!validateField(
"verificationCode",
verificationCode.value,
(v) => v,
"请输入验证码"
) ||
!validateField(
"carPickerVal",
carPickerVal.value,
(v) => v,
"请选择车辆类型"
) ||
!validateField(
"carLicense",
carLicense.value,
(v) => isCarLicense.value,
"请输入正确的车牌号"
) ||
!validateField("entName", entName.value, (v) => v, "请输入企业名称") ||
!validateField(
"entCode",
entCode.value,
(v) => isCreditCodeValid.value,
"请输入统一社会信用代码"
) ||
!validateField(
"date",
dateVal.value,
(v) => v,
"请选择日期"
)
) {
return;
}
submitRequest();
}
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 specialProduct = {
toc_EnterpriseLawsuit: ["entName", "entCode", "mobile", "verificationCode"],
toc_PhoneThreeElements: ["name", "idCard", "mobile"],
toc_IDCardTwoElements: ["name", "idCard"],
toc_PhoneTwoElements: ["name", "mobile"],
toc_PersonVehicleVerification: ["name", "carType", "carLicense"],
toc_VehiclesUnderName: ["name", "idCard"],
toc_DualMarriage: ["nameMan", "idCardMan", "nameWoman", "idCardWoman"],
toc_BankCardBlacklist: ["name", "idCard", "mobile", "bankCard"],
toc_BankCardFourElements: ["name", "idCard", "mobile", "bankCard"],
toc_NaturalLifeStatus: ["name", "idCard"],
toc_NetworkDuration: ["mobile"],
toc_PhoneSecondaryCard: ["mobile", "date"],
toc_PhoneNumberRisk: ["mobile"],
};
const NeedAuthorization = [
"toc_Marriage",
"marriage"
];
const isHasInput = (input) => {
if (specialProduct[feature.value]) {
return specialProduct[feature.value].includes(input);
} else {
return defaultInput.includes(input);
}
};
async function submitRequest() {
const req = {};
if (isHasInput("name")) {
req.name = name.value;
}
if (isHasInput("idCard")) {
req.id_card = idCard.value;
}
if (isHasInput("nameMan")) {
req.name_man = nameMan.value;
}
if (isHasInput("idCardMan")) {
req.id_card_man = idCardMan.value;
}
if (isHasInput("nameWoman")) {
req.name_woman = nameWoman.value;
}
if (isHasInput("idCardWoman")) {
req.id_card_woman = idCardWoman.value;
}
if (isHasInput("bankCard")) {
req.bank_card = bankCard.value.replace(/\D/g, "");
}
if (isHasInput("mobile")) {
req.mobile = mobile.value;
}
if (isHasInput("verificationCode")) {
req.code = verificationCode.value;
}
if (isHasInput("carType")) {
req.car_type = carPickerVal.value[0].value;
}
if (isHasInput("carLicense")) {
req.car_license = carLicense.value.trim();
}
if (isHasInput("date")) {
req.start_date = startDate.value.map(item => item).join('')
}
if (isHasInput("entName")) {
req.ent_name = entName.value;
}
if (isHasInput("entCode")) {
req.ent_code = entCode.value;
}
const reqStr = JSON.stringify(req);
const encodeData = aesEncrypt(reqStr, "3a7f1e9d4c2b8a05e6f0d3c7b2a9845d");
const { data, error } = await useApiFetch(`/query/service/${feature.value}`)
.post({ data: encodeData })
.json();
if (data.value.code === 200) {
queryId.value = data.value.data.id;
if (authorization.value) {
showPayment.value = true;
} else {
showAuthorizationPopup.value = true;
}
}
}
async function sendVerificationCode() {
if (isCountingDown.value || !isPhoneNumberValid.value) return;
if (!isPhoneNumberValid.value) {
showToast({ message: "请输入有效的手机号" });
return;
}
const { data, error } = await useApiFetch("/auth/sendSms")
.post({ mobile: mobile.value, actionType: "query" })
.json();
if (!error.value && data.value.code === 200) {
showToast({ message: "验证码发送成功", type: "success" });
startCountdown();
} 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=${feature.value}`)
};
onUnmounted(() => {
if (timer) {
clearInterval(timer);
}
});
</script>
<template>
<div class="inquire-bg min-h-screen p-6">
<div class="card">
<div class="mb-4 text-lg font-semibold text-gray-800">基本信息</div>
<div class="mb-4 flex items-center" v-if="isHasInput('name')">
<label for="name" class="form-label">姓名</label>
<input v-model="name" id="name" type="text" placeholder="请输入姓名" class="form-input" />
</div>
<div class="mb-4 flex items-center" v-if="isHasInput('idCard')">
<label for="idCard" class="form-label">身份证号</label>
<input v-model="idCard" id="idCard" type="text" placeholder="请输入身份证号" class="form-input" />
</div>
<!-- 双人婚姻 -->
<div class="mb-4 flex items-center" v-if="isHasInput('nameMan')">
<label for="nameMan" class="form-label">男方姓名</label>
<input v-model="nameMan" id="nameMan" type="text" placeholder="请输入男方姓名" class="form-input" />
</div>
<div class="mb-4 flex items-center" v-if="isHasInput('idCardMan')">
<label for="idCardMan" class="form-label">男方身份证号</label>
<input v-model="idCardMan" id="idCardMan" type="text" placeholder="请输入男方身份证号" class="form-input" />
</div>
<div class="mb-4 flex items-center" v-if="isHasInput('nameWoman')">
<label for="nameWoman" class="form-label">女方姓名</label>
<input v-model="nameWoman" id="nameWoman" type="text" placeholder="请输入女方姓名" class="form-input" />
</div>
<div class="mb-4 flex items-center" v-if="isHasInput('idCardWoman')">
<label for="idCardWoman" class="form-label">女方身份证号</label>
<input v-model="idCardWoman" id="idCardWoman" type="text" placeholder="请输入女方身份证号" class="form-input" />
</div>
<!-- 双人婚姻 -->
<div class="mb-4 flex items-center" v-if="isHasInput('entName')">
<label for="entName" class="form-label">企业名称</label>
<input v-model="entName" id="entName" type="text" placeholder="请输入企业名称" class="form-input" />
</div>
<div class="mb-4 flex items-center" v-if="isHasInput('entCode')">
<label for="entCode" class="form-label">统一社会信用代码</label>
<input v-model="entCode" id="entCode" type="text" placeholder="请输入统一社会信用代码" class="form-input" />
</div>
<div class="mb-4 flex items-center" v-if="isHasInput('carType')">
<label for="carType" class="form-label">汽车类型</label>
<van-field id="carType" v-model="carType" is-link readonly placeholder="点击选择汽车类型"
@click="showCarTypePicker = true" class="form-input" />
<van-popup v-model:show="showCarTypePicker" destroy-on-close round position="bottom">
<van-picker :model-value="carPickerVal" :columns="carTypeColumns"
@cancel="showCarTypePicker = false" @confirm="onConfirmCarType" />
</van-popup>
</div>
<div class="mb-4 flex items-center" v-if="isHasInput('carLicense')">
<!-- <label for="entCode" class="form-label">车牌号</label> -->
<CarNumberInput class="form-input" @number-input-result="carLicenseChange" :default-str="carLicense">
</CarNumberInput>
</div>
<div class="mb-4 flex items-center" v-if="isHasInput('bankCard')">
<label for="bankCard" class="form-label">银行卡号</label>
<input v-model="bankCard" id="bankCard" type="tel" placeholder="请输入银行卡号" class="form-input" />
</div>
<div class="mb-4 flex items-center" v-if="isHasInput('mobile')">
<label for="mobile" class="form-label">手机号</label>
<input v-model="mobile" id="mobile" type="tel" placeholder="请输入手机号" class="form-input" />
</div>
<div class="mb-4 flex items-center" v-if="isHasInput('date')">
<label for="date" class="form-label">业务日期</label>
<van-field id="date" v-model="dateVal" is-link readonly placeholder="点击选择日期"
@click="showDatePicker = true" class="form-input" />
<van-popup v-model:show="showDatePicker" destroy-on-close round position="bottom">
<van-date-picker v-model="startDate" :formatter="formatterDate" :min-date="minDate"
:max-date="maxDate" title="选择日期" @confirm="onConfirmDate" @cancel="showDatePicker = false" />
</van-popup>
</div>
<div class="mb-4 flex items-center" v-if="isHasInput('verificationCode')">
<label for="verificationCode" class="form-label">验证码</label>
<div class="flex-1 flex items-center">
<input v-model="verificationCode" id="verificationCode" type="text" placeholder="请输入验证码"
class="form-input flex-1" />
<button class="ml-2 px-4 py-2 text-sm text-blue-500 disabled:text-gray-400"
:disabled="isCountingDown || !isPhoneNumberValid" @click="sendVerificationCode">
{{
isCountingDown
? `${countdown}s重新获取`
: "获取验证码"
}}
</button>
</div>
</div>
<div class="mb-4 flex items-center">
<input type="checkbox" v-model="agreeToTerms" />
<span class="ml-2 text-xs text-gray-400">
我已阅读并同意
<span @click="toUserAgreement" class="text-blue-500 ">用户协议</span>
<span @click="toPrivacyPolicy" class="text-blue-500 ">隐私政策</span>
<template v-if="!NeedAuthorization.includes(feature)">
<span @click="toAuthorization" class="text-blue-500 ">授权书</span>
</template>
</span>
</div>
<div class="flex items-center">
<button class="w-24 rounded-l-xl bg-blue-400 py-2 text-white" @click="toExample">
示例报告
</button>
<button class="flex-1 rounded-r-xl bg-blue-500 py-2 text-white" @click="handleSubmit">
立即查询
</button>
</div>
</div>
</div>
</template>
<style scoped>
.form-label {
@apply w-20 text-sm font-medium text-gray-700 flex-shrink-0;
}
.form-input::placeholder {
color: var(--van-text-color-3);
}
.form-input {
@apply w-full border-b border-gray-200 px-2 py-2 focus:outline-none;
}
</style>

View File

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

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

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

View File

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

View File

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

View File

@@ -0,0 +1,90 @@
<template>
<div class="l-remark-card my-[20px]">
<!-- 顶部连接点 -->
<div class="connection-line">
<img src="data:image/png;base64,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" alt="左链条" class="connection-chain left" />
<img src="data:image/png;base64,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" alt="右链条" class="connection-chain right" />
</div>
<div>
<van-icon name="info-o" class="tips-icon" />
<span class="tips-title">温馨提示</span>
</div>
<div>
<van-text-ellipsis rows="2" :content="content" expand-text="展开" collapse-text="收起" />
</div>
</div>
</template>
<script setup>
import { ref, defineProps } from 'vue';
const props = defineProps({
content: {
type: String,
required: true
}
});
const isExpanded = ref(false);
</script>
<style scoped>
.l-remark-card {
position: relative;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
border-radius: 0.75rem;
background-color: #ffffff;
padding: 24px;
}
.tips-card {
background: var(--van-theme-primary-light);
border-radius: 8px;
padding: 12px;
}
.tips-icon {
color: var(--van-theme-primary);
margin-right: 5px;
}
.tips-title {
font-weight: bold;
font-size: 16px;
}
.tips-content {
font-size: 14px;
color: #333;
}
/* 连接链条样式 */
.connection-line {
position: absolute;
top: -40px;
left: 0;
right: 0;
height: 60px;
z-index: 20;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 10px;
}
.connection-chain {
height: 60px;
object-fit: contain;
}
.connection-chain.left {
width: 80px;
margin-left: -10px;
}
.connection-chain.right {
width: 80px;
margin-right: -10px;
}
</style>

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

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

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

@@ -0,0 +1,37 @@
<script setup>
// 接收 props
const props = defineProps({
title: String,
})
const titleClass = computed(() => {
// 统一使用主题色渐变
return 'bg-gradient-to-r from-red-600 via-red-500 to-red-700'
})
// 分割线颜色与背景对应
const lineClass = computed(() => {
// 统一使用主题色渐变
return 'bg-gradient-to-r from-red-600 via-red-500 to-red-700'
})
</script>
<template>
<div class="relative">
<!-- 标题部分 -->
<div :class="titleClass" class="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 class="relative mt-1.5">
<div :class="lineClass" class="h-[2px] w-full rounded" />
</div>
</div>
</template>
<style scoped></style>

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

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

View File

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

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

@@ -0,0 +1,338 @@
<template>
<van-popup v-model:show="show" round position="bottom">
<div class="max-h-[calc(100vh-100px)] m-4">
<div class="p-4">
<van-swipe
class="poster-swiper 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-xl h-[800px] m-auto"
></canvas>
</van-swipe-item>
</van-swipe>
</div>
<div
v-if="mode === 'promote'"
class="swipe-tip text-center text-gray-700 text-sm mb-2"
>
<span class="swipe-icon"></span> 左右滑动切换海报
<span class="swipe-icon"></span>
</div>
<van-divider>分享到好友</van-divider>
<div class="flex items-center justify-around">
<div
class="flex flex-col items-center justify-center"
@click="savePoster"
>
<img
src="@/assets/images/icon_share_img.svg"
class="w-10 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"
@click="copyUrl"
>
<img
src="@/assets/images/icon_share_url.svg"
class="w-10 h-10 rounded-full"
/>
<div class="text-center mt-1 text-gray-600 text-xs">
复制链接
</div>
</div>
</div>
</div>
</van-popup>
</template>
<script setup>
import { ref, watch, nextTick, computed, onMounted, toRefs } from "vue";
import QRCode from "qrcode";
import { showToast } from "vant";
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 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模式的配置 (tg_qrcode)
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 imageCount = mode.value === "promote" ? 8 : 1;
// 加载图片
for (let i = 1; i <= imageCount; i++) {
// 尝试加载 .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 toPromote = () => {
// 这里可以实现微信分享的功能比如调用微信JS-SDK等
console.log("分享到微信好友");
};
// 保存海报图片
const savePoster = () => {
const canvas = posterCanvasRefs.value[currentIndex.value];
const dataURL = canvas.toDataURL("image/png"); // 获取 canvas 内容为图片
const a = document.createElement("a");
a.href = dataURL;
a.download = "天远数据查询.png";
a.click();
};
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>
.poster-swiper {
height: 500px;
width: 100%;
}
.poster-canvas {
width: 100%;
height: 100%;
object-fit: contain;
}
.swipe-tip {
animation: fadeInOut 2s infinite;
}
.swipe-icon {
display: inline-block;
animation: slideLeftRight 1.5s infinite;
}
@keyframes fadeInOut {
0%,
100% {
opacity: 0.5;
}
50% {
opacity: 1;
}
}
@keyframes slideLeftRight {
0%,
100% {
transform: translateX(0);
}
50% {
transform: translateX(5px);
}
}
</style>

View File

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

View File

@@ -0,0 +1,362 @@
<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"
>
<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="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="px-8 py-2">
<div class="auth-notice p-4 rounded-lg mb-6" style="background-color: var(--van-theme-primary-light); border: 1px solid rgba(162, 37, 37, 0.2);">
<div class="text-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-5">
<!-- 姓名输入 -->
<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 justify-between">
<div
:class="[
'input-container',
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="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'
: '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 space-x-2">
<input
type="checkbox"
v-model="isAgreed"
class="mt-1"
/>
<span 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="toPrivacyPolicy"
>隐私政策</a
>
并确认以上信息真实有效将用于提现等资金操作
</span>
</div>
</div>
<button
class="mb-12 mt-10 w-full py-3 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>
</van-popup>
</div>
</template>
<style scoped>
.title-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid var(--van-border-color);
}
.close-icon {
font-size: 20px;
cursor: pointer;
}
.input-container {
border-radius: 1rem;
transition: duration-200;
}
.input-container.focused {
border: 2px solid var(--van-theme-primary) !important;
}
.input-field {
width: 100%;
padding: 1rem;
background: transparent;
border: none;
outline: none;
transition: border-color 0.3s ease;
}
</style>

View File

@@ -0,0 +1,140 @@
<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,
},
});
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) 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="mt-4 flex flex-col items-center gap-2">
<van-button
type="primary"
size="small"
class="!bg-blue-500 !border-blue-500"
:loading="isLoading"
:disabled="isLoading"
@click="handleShare"
>
<template #icon>
<van-icon name="share-o" />
</template>
{{ isLoading ? "生成中..." : (isExample ? "分享示例" : "分享报告") }}
</van-button>
<div class="text-xs text-gray-500">{{ isExample ? "分享当前示例链接" : "分享链接将在7天后过期" }}</div>
</div>
</template>
<style lang="scss" scoped>
:deep(.van-button) {
min-width: 120px;
}
</style>

View File

@@ -0,0 +1,18 @@
<template>
<div class="mb-4" @click="toAgentVip">
<img
src="@/assets/images/vip_banner.png"
class="rounded-xl shadow-lg"
alt=""
/>
</div>
</template>
<script setup>
const router = useRouter();
const toAgentVip = () => {
router.push({ name: "agentVipApply" });
};
</script>
<style lang="scss" scoped></style>

View File

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