This commit is contained in:
2025-12-16 19:27:20 +08:00
parent c85b46c18e
commit 430b8f12ba
89 changed files with 7166 additions and 4061 deletions

View File

@@ -6,17 +6,22 @@
>
成为代理
</div>
<div v-if="ancestor" class="text-center text-xs my-2" style="color: var(--van-text-color-2);">
{{ maskName(ancestor) }}邀您成为一查查代理方
</div>
<div class="p-4">
<van-field
label-width="56"
v-model="form.referrer"
label="邀请信息"
name="referrer"
placeholder="请输入邀请码/代理编码/代理手机号"
required
/>
<van-field
label-width="56"
v-model="form.region"
is-link
readonly
label="地区"
placeholder="请选择地区"
placeholder="请选择地区(可选)"
@click="showCascader = true"
/>
<van-popup v-model:show="showCascader" round position="bottom">
@@ -124,10 +129,6 @@ 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,
@@ -137,11 +138,12 @@ const props = defineProps({
default: "",
},
});
const { ancestor, isSelf, userName } = toRefs(props);
const { isSelf, userName } = toRefs(props);
const form = ref({
referrer: "",
region: "",
mobile: "",
code: "", // 增加验证码字段
code: "", // 验证码字段
});
const showCascader = ref(false);
const cascaderValue = ref("");
@@ -207,10 +209,11 @@ onUnmounted(() => {
});
const submit = () => {
// 校验表单字段
if (!form.value.region) {
showToast({ message: "请选择地区" });
if (!form.value.referrer || !form.value.referrer.trim()) {
showToast({ message: "请输入邀请信息" });
return;
}
if (!form.value.mobile) {
showToast({ message: "请输入手机号" });
return;
@@ -231,15 +234,13 @@ const submit = () => {
showToast({ message: "请先阅读并同意用户协议及相关条款" });
return;
}
console.log("form", form.value);
// 触发父组件提交申请
emit("submit", form.value);
emit("submit", {
...form.value,
referrer: form.value.referrer.trim()
});
};
const maskName = computed(() => {
return (name) => {
return name.substring(0, 3) + "****" + name.substring(7);
};
});
const closePopup = () => {
emit("close");
};

View File

@@ -8,6 +8,7 @@ import { splitDWBG6A2CForTabs } from '@/ui/DWBG6A2C/utils/simpleSplitter.js';
import { splitJRZQ7F1AForTabs } from '@/ui/JRZQ7F1A/utils/simpleSplitter.js';
import { splitCJRZQ5E9FForTabs } from '@/ui/CJRZQ5E9F/utils/simpleSplitter.js';
import { splitCQYGL3F8EForTabs } from '@/ui/CQYGL3F8E/utils/simpleSplitter.js';
import { useAppStore } from "@/stores/appStore";
// 动态导入产品背景图片的函数
const loadProductBackground = async (productType) => {
@@ -17,7 +18,7 @@ const loadProductBackground = async (productType) => {
return (await import("@/assets/images/report/xwqy_inquire_bg.png")).default;
case 'preloanbackgroundcheck':
return (await import("@/assets/images/report/dqfx_inquire_bg.png")).default;
case 'personalData':
case 'riskassessment':
return (await import("@/assets/images/report/grdsj_inquire_bg.png")).default;
case 'marriage':
return (await import("@/assets/images/report/marriage_inquire_bg.png")).default;
@@ -36,6 +37,8 @@ const loadProductBackground = async (productType) => {
}
};
const appStore = useAppStore();
const props = defineProps({
isShare: {
type: Boolean,
@@ -879,7 +882,7 @@ watch([reportData, componentRiskScores], () => {
1本份报告是在取得您个人授权后我们才向合法存有您以上个人信息的机构去调取相关内容我们不会以任何形式对您的报告进行存储除您和您授权的人外不会提供给任何人和机构进行查看
</p>
<p class="text-[#999999]">
&nbsp; &nbsp; 2本报告自生成之日起有效期 30
&nbsp; &nbsp; 2本报告自生成之日起有效期 {{ useAppStore().queryRetentionDays || 30 }}
过期自动删除如果您对本份报告存有异议可能是合作机构数据有延迟或未能获取到您的相关数据出于合作平台数据隐私的保护本平台将不做任何解释
</p>
<p class="text-[#999999]">
@@ -894,17 +897,17 @@ watch([reportData, componentRiskScores], () => {
</div>
<div class="disclaimer">
<div class="flex flex-col items-center">
<div class="flex items-center">
<!-- <div class="flex items-center">
<img class="w-4 h-4 mr-2" src="@/assets/images/public_security_record_icon.png" alt="公安备案" />
<text>琼公网安备46010002000584号</text>
</div>
</div> -->
<div>
<a class="text-blue-500" href="https://beian.miit.gov.cn">
琼ICP备2024048057-2
琼ICP备2024038584-10
</a>
</div>
</div>
<div>海南省学宇思网络科技有限公司版权所有</div>
<div>海南海宇大数据有限公司版权所有</div>
</div>
</template>

View File

@@ -1,33 +1,72 @@
<script setup>
import { ref, computed, nextTick } from "vue";
import { ref, computed, nextTick, onMounted } from "vue";
import { useRouter, useRoute } from "vue-router";
import { useDialogStore } from "@/stores/dialogStore";
import { useAgentStore } from "@/stores/agentStore";
import { useUserStore } from "@/stores/userStore";
import { showToast } from "vant";
import useApiFetch from "@/composables/useApiFetch";
import { registerByInviteCode } from "@/api/agent";
const emit = defineEmits(['bind-success'])
const emit = defineEmits(['register-success'])
const router = useRouter();
const route = useRoute();
const dialogStore = useDialogStore();
const agentStore = useAgentStore();
const userStore = useUserStore();
const phoneNumber = ref("");
const verificationCode = ref("");
const inviteCode = ref("");
const isCountingDown = ref(false);
const countdown = ref(60);
const isAgreed = ref(false);
const hasAccount = ref(false); // 是否有平台账号
let timer = null;
// 聚焦状态变量
const phoneFocused = ref(false);
const codeFocused = ref(false);
const inviteFocused = ref(false);
// 从URL参数中读取邀请码并自动填入
onMounted(() => {
const inviteCodeParam = route.query.invite_code;
if (inviteCodeParam) {
inviteCode.value = inviteCodeParam;
}
// 如果用户已登录且有手机号,自动填充手机号
const token = localStorage.getItem("token");
if (token && userStore.mobile) {
phoneNumber.value = userStore.mobile;
}
});
const isPhoneNumberValid = computed(() => {
return /^1[3-9]\d{9}$/.test(phoneNumber.value);
});
const canBind = computed(() => {
return (
isPhoneNumberValid.value &&
verificationCode.value.length === 6 &&
isAgreed.value
);
const isInviteCodeValid = computed(() => {
return inviteCode.value.trim().length > 0;
});
const canRegister = computed(() => {
if (hasAccount.value) {
// 已有账号模式:只需要手机号和验证码
return (
isPhoneNumberValid.value &&
verificationCode.value.length === 6 &&
isAgreed.value
);
} else {
// 新注册模式:需要手机号、验证码和邀请码
return (
isPhoneNumberValid.value &&
verificationCode.value.length === 6 &&
isInviteCodeValid.value &&
isAgreed.value
);
}
});
async function sendVerificationCode() {
@@ -36,8 +75,13 @@ async function sendVerificationCode() {
showToast({ message: "请输入有效的手机号" });
return;
}
if (!hasAccount.value && !isInviteCodeValid.value) {
showToast({ message: "请先输入邀请码" });
return;
}
const actionType = hasAccount.value ? "bindMobile" : "agentApply";
const { data, error } = await useApiFetch("auth/sendSms")
.post({ mobile: phoneNumber.value, actionType: "bindMobile" })
.post({ mobile: phoneNumber.value, actionType })
.json();
if (data.value && !error.value) {
@@ -46,7 +90,7 @@ async function sendVerificationCode() {
startCountdown();
// 聚焦到验证码输入框
nextTick(() => {
const verificationCodeInput = document.getElementById('verificationCode');
const verificationCodeInput = document.getElementById('registerVerificationCode');
if (verificationCodeInput) {
verificationCodeInput.focus();
}
@@ -70,7 +114,7 @@ function startCountdown() {
}, 1000);
}
async function handleBind() {
async function handleRegister() {
if (!isPhoneNumberValid.value) {
showToast({ message: "请输入有效的手机号" });
return;
@@ -79,48 +123,117 @@ async function handleBind() {
showToast({ message: "请输入有效的验证码" });
return;
}
if (!hasAccount.value && !isInviteCodeValid.value) {
showToast({ message: "请输入邀请码" });
return;
}
if (!isAgreed.value) {
showToast({ message: "请先同意用户协议" });
return;
}
const { data, error } = await useApiFetch("/user/bindMobile")
.post({ mobile: phoneNumber.value, code: verificationCode.value })
.json();
try {
if (hasAccount.value) {
// 已有账号模式:绑定手机号登录
const { data, error } = await useApiFetch("/user/bindMobile")
.post({ mobile: phoneNumber.value, code: verificationCode.value })
.json();
if (data.value && !error.value) {
if (data.value.code === 200) {
showToast({ message: "绑定成功" });
localStorage.setItem('token', data.value.data.accessToken)
localStorage.setItem('refreshAfter', data.value.data.refreshAfter)
localStorage.setItem('accessExpire', data.value.data.accessExpire)
closeDialog();
await Promise.all([
agentStore.fetchAgentStatus(),
userStore.fetchUserInfo()
]);
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)
// 发出绑定成功的事件
emit('bind-success');
closeDialog();
await Promise.all([
agentStore.fetchAgentStatus(),
userStore.fetchUserInfo()
]);
// 延迟执行路由检查,确保状态已更新
setTimeout(() => {
// 重新触发路由检查
const currentRoute = router.currentRoute.value;
router.replace(currentRoute.path);
}, 100);
// 发出注册成功的事件
emit('register-success');
// 检查是否是代理,如果是代理跳转到代理主页,否则跳转到首页
setTimeout(() => {
if (agentStore.isAgent) {
router.replace("/agent");
} else {
router.replace("/");
}
}, 300);
} else {
// 检查是否是手机号已绑定其他微信的错误
if (data.value.msg && data.value.msg.includes("已绑定其他微信号")) {
showToast({ message: "该手机号已绑定其他微信号,一个微信只能绑定一个手机号" });
} else {
showToast(data.value.msg || "绑定失败,请重试");
}
}
}
} else {
showToast(data.value.msg);
// 新注册模式:通过邀请码注册成为代理
const { data, error } = await registerByInviteCode({
mobile: phoneNumber.value,
code: verificationCode.value,
referrer: inviteCode.value.trim()
});
if (data.value && !error.value) {
if (data.value.code === 200) {
showToast({ message: "注册成功!" });
localStorage.setItem('token', data.value.data.accessToken)
localStorage.setItem('refreshAfter', data.value.data.refreshAfter)
localStorage.setItem('accessExpire', data.value.data.accessExpire)
// 更新代理信息到store
if (data.value.data.agent_id) {
agentStore.updateAgentInfo({
isAgent: true,
agentID: data.value.data.agent_id,
level: data.value.data.level || 1,
levelName: data.value.data.level_name || '普通代理'
});
}
closeDialog();
await Promise.all([
agentStore.fetchAgentStatus(),
userStore.fetchUserInfo()
]);
// 发出注册成功的事件
emit('register-success');
// 跳转到代理主页
setTimeout(() => {
router.replace("/agent");
}, 300);
} else {
// 检查是否是手机号已绑定其他微信的错误
if (data.value.msg && data.value.msg.includes("已绑定其他微信号")) {
showToast({ message: "该手机号已绑定其他微信号,一个微信只能绑定一个手机号" });
} else {
showToast(data.value.msg || "注册失败,请重试");
}
}
}
}
} catch (err) {
console.error('操作失败:', err);
showToast({ message: "操作失败,请重试" });
}
}
function closeDialog() {
dialogStore.closeBindPhone();
dialogStore.closeRegisterAgent();
// 重置表单
phoneNumber.value = "";
verificationCode.value = "";
inviteCode.value = "";
isAgreed.value = false;
hasAccount.value = false;
if (timer) {
clearInterval(timer);
}
@@ -138,17 +251,14 @@ function toPrivacyPolicy() {
</script>
<template>
<div v-if="dialogStore.showBindPhone">
<van-popup v-model:show="dialogStore.showBindPhone" round position="bottom" :style="{ height: '80%' }"
<div v-if="dialogStore.showRegisterAgent">
<van-popup v-model:show="dialogStore.showRegisterAgent" round position="bottom" :style="{ height: '85%' }"
@close="closeDialog">
<div class="bind-phone-dialog">
<div class="register-agent-dialog">
<div class="title-bar">
<div class="font-bold">绑定手机号码</div>
<div class="font-bold">注册成为代理</div>
<div class="text-sm text-gray-500 mt-1">
为使用完整功能请绑定手机号码
</div>
<div class="text-sm text-gray-500 mt-1">
如该微信号之前已绑定过手机号请输入已绑定的手机号
{{ hasAccount ? '绑定手机号登录已有账号' : '请输入手机号和邀请码完成代理注册' }}
</div>
<van-icon name="cross" class="close-icon" @click="closeDialog" />
</div>
@@ -163,6 +273,48 @@ function toPrivacyPolicy() {
</div>
<div class="space-y-5">
<!-- 账号类型选择 -->
<div class="flex items-center space-x-4 mb-4">
<button @click="hasAccount = false" :class="[
'flex-1 py-2 px-4 rounded-lg text-sm font-medium transition-all',
!hasAccount
? 'bg-blue-500 text-white shadow-md'
: 'bg-gray-100 text-gray-600'
]">
新注册
</button>
<button @click="hasAccount = true" :class="[
'flex-1 py-2 px-4 rounded-lg text-sm font-medium transition-all',
hasAccount
? 'bg-blue-500 text-white shadow-md'
: 'bg-gray-100 text-gray-600'
]">
已有平台账号
</button>
</div>
<!-- 重要提示 -->
<div v-if="hasAccount" class="bg-yellow-50 border border-yellow-200 rounded-lg p-3 mb-4">
<div class="flex items-start">
<van-icon name="warning-o" class="text-yellow-600 mr-2 mt-0.5 flex-shrink-0" />
<div class="text-xs text-yellow-800 leading-relaxed">
<p class="font-semibold mb-1">重要提示</p>
<p> 一个微信只能绑定一个手机号</p>
<p> 如果该手机号已绑定其他微信号将无法在此微信登录</p>
<p> 请确保输入的是您已注册的手机号</p>
</div>
</div>
</div>
<!-- 邀请码输入仅新注册模式显示 -->
<div v-if="!hasAccount" :class="[
'input-container bg-blue-300/20',
inviteFocused ? 'focused' : '',
]">
<input v-model="inviteCode" class="input-field" type="text" placeholder="请输入邀请码"
@focus="inviteFocused = true" @blur="inviteFocused = false" />
</div>
<!-- 手机号输入 -->
<div :class="[
'input-container bg-blue-300/20',
@@ -178,20 +330,20 @@ function toPrivacyPolicy() {
'input-container bg-blue-300/20',
codeFocused ? 'focused' : '',
]">
<input v-model="verificationCode" id="verificationCode" class="input-field"
<input v-model="verificationCode" id="registerVerificationCode" class="input-field"
placeholder="请输入验证码" maxlength="6" @focus="codeFocused = true"
@blur="codeFocused = false" />
</div>
<button
class="ml-2 px-4 py-2 text-sm font-bold flex-shrink-0 rounded-lg transition duration-300"
:class="isCountingDown || !isPhoneNumberValid
:class="isCountingDown || !isPhoneNumberValid || (!hasAccount && !isInviteCodeValid)
? 'cursor-not-allowed bg-gray-300 text-gray-500'
: 'bg-blue-500 text-white hover:bg-blue-600'
" @click="sendVerificationCode">
{{
isCountingDown
? `${countdown}s重新获取`
: "获取验证码"
isCountingDown
? `${countdown}s重新获取`
: "获取验证码"
}}
</button>
</div>
@@ -200,7 +352,7 @@ function toPrivacyPolicy() {
<div class="flex items-start space-x-2">
<input type="checkbox" v-model="isAgreed" class="mt-1" />
<span class="text-xs text-gray-400 leading-tight">
绑定手机号即代表您已阅读并同意
注册成为代理即代表您已阅读并同意
<a class="cursor-pointer text-blue-400" @click="toUserAgreement">
用户协议
</a>
@@ -214,8 +366,8 @@ function toPrivacyPolicy() {
<button
class="mt-10 w-full py-3 text-lg font-bold text-white bg-blue-500 rounded-full transition duration-300"
:class="{ 'opacity-50 cursor-not-allowed': !canBind }" @click="handleBind">
确认绑定
:class="{ 'opacity-50 cursor-not-allowed': !canRegister }" @click="handleRegister">
{{ hasAccount ? '绑定手机号登录' : '注册成为代理' }}
</button>
</div>
</div>
@@ -224,7 +376,7 @@ function toPrivacyPolicy() {
</template>
<style scoped>
.bind-phone-dialog {
.register-agent-dialog {
background: url("@/assets/images/login_bg.png") no-repeat;
background-position: center;
background-size: cover;

View File

@@ -0,0 +1,267 @@
<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('bindPhoneVerificationCode');
if (verificationCodeInput) {
verificationCodeInput.focus();
}
});
} else {
showToast(data.value.msg);
}
}
}
function startCountdown() {
isCountingDown.value = true;
countdown.value = 60;
timer = setInterval(() => {
if (countdown.value > 0) {
countdown.value--;
} else {
clearInterval(timer);
isCountingDown.value = false;
}
}, 1000);
}
async function handleBind() {
if (!isPhoneNumberValid.value) {
showToast({ message: "请输入有效的手机号" });
return;
}
if (verificationCode.value.length !== 6) {
showToast({ message: "请输入有效的验证码" });
return;
}
if (!isAgreed.value) {
showToast({ message: "请先同意用户协议" });
return;
}
const { data, error } = await useApiFetch("/user/bindMobile")
.post({ mobile: phoneNumber.value, code: verificationCode.value })
.json();
if (data.value && !error.value) {
if (data.value.code === 200) {
showToast({ message: "绑定成功" });
localStorage.setItem('token', data.value.data.accessToken)
localStorage.setItem('refreshAfter', data.value.data.refreshAfter)
localStorage.setItem('accessExpire', data.value.data.accessExpire)
closeDialog();
await Promise.all([
agentStore.fetchAgentStatus(),
userStore.fetchUserInfo()
]);
// 发出绑定成功的事件
emit('bind-success');
// 延迟执行路由检查,确保状态已更新
setTimeout(() => {
// 重新触发路由检查
const currentRoute = router.currentRoute.value;
router.replace(currentRoute.path);
}, 100);
} else {
showToast(data.value.msg);
}
}
}
function closeDialog() {
dialogStore.closeBindPhone();
// 重置表单
phoneNumber.value = "";
verificationCode.value = "";
isAgreed.value = false;
if (timer) {
clearInterval(timer);
}
}
function toUserAgreement() {
closeDialog();
router.push(`/userAgreement`);
}
function toPrivacyPolicy() {
closeDialog();
router.push(`/privacyPolicy`);
}
</script>
<template>
<div v-if="dialogStore.showBindPhone">
<van-popup v-model:show="dialogStore.showBindPhone" round position="bottom" :style="{ height: '80%' }"
@close="closeDialog">
<div class="bind-phone-dialog">
<div class="title-bar">
<div class="font-bold">绑定手机号码</div>
<div class="text-sm text-gray-500 mt-1">
为使用完整功能请绑定手机号码
</div>
<div class="text-sm text-gray-500 mt-1">
如该微信号之前已绑定过手机号请输入已绑定的手机号
</div>
<van-icon name="cross" class="close-icon" @click="closeDialog" />
</div>
<div class="px-8">
<div class="mb-8 pt-8 text-left">
<div class="flex flex-col items-center">
<img class="h-16 w-16 rounded-full shadow" src="/logo.png" alt="Logo" />
<div class="text-3xl mt-4 text-slate-700 font-bold">
一查查
</div>
</div>
</div>
<div class="space-y-5">
<!-- 手机号输入 -->
<div :class="[
'input-container bg-blue-300/20',
phoneFocused ? 'focused' : '',
]">
<input v-model="phoneNumber" class="input-field" type="tel" placeholder="请输入手机号"
maxlength="11" @focus="phoneFocused = true" @blur="phoneFocused = false" />
</div>
<!-- 验证码输入 -->
<div class="flex items-center justify-between">
<div :class="[
'input-container bg-blue-300/20',
codeFocused ? 'focused' : '',
]">
<input v-model="verificationCode" id="bindPhoneVerificationCode" class="input-field"
placeholder="请输入验证码" maxlength="6" @focus="codeFocused = true"
@blur="codeFocused = false" />
</div>
<button
class="ml-2 px-4 py-2 text-sm font-bold flex-shrink-0 rounded-lg transition duration-300"
:class="isCountingDown || !isPhoneNumberValid
? 'cursor-not-allowed bg-gray-300 text-gray-500'
: 'bg-blue-500 text-white hover:bg-blue-600'
" @click="sendVerificationCode">
{{
isCountingDown
? `${countdown}s重新获取`
: "获取验证码"
}}
</button>
</div>
<!-- 协议同意框 -->
<div class="flex items-start space-x-2">
<input type="checkbox" v-model="isAgreed" class="mt-1" />
<span class="text-xs text-gray-400 leading-tight">
绑定手机号即代表您已阅读并同意
<a class="cursor-pointer text-blue-400" @click="toUserAgreement">
用户协议
</a>
<a class="cursor-pointer text-blue-400" @click="toPrivacyPolicy">
隐私政策
</a>
</span>
</div>
</div>
<button
class="mt-10 w-full py-3 text-lg font-bold text-white bg-blue-500 rounded-full transition duration-300"
:class="{ 'opacity-50 cursor-not-allowed': !canBind }" @click="handleBind">
确认绑定
</button>
</div>
</div>
</van-popup>
</div>
</template>
<style scoped>
.bind-phone-dialog {
background: url("@/assets/images/login_bg.png") no-repeat;
background-position: center;
background-size: cover;
height: 100%;
}
.title-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid #eee;
}
.close-icon {
font-size: 20px;
color: #666;
cursor: pointer;
}
.input-container {
border: 2px solid rgba(125, 211, 252, 0);
border-radius: 1rem;
transition: duration-200;
}
.input-container.focused {
border: 2px solid #3b82f6;
}
.input-field {
width: 100%;
padding: 1rem;
background: transparent;
border: none;
outline: none;
transition: border-color 0.3s ease;
}
</style>

View File

@@ -47,8 +47,8 @@ const canvasWidth = 300
const canvasHeight = 180
const bgImgUrl = '/image/clickCaptcha.jpg' // 可替换为任意背景图
const allChars = ['大', '数', '据', '全', '能', '查', '风', '险', '报', '告']
const targetChars = ref(['', '', '查']) // 目标点击顺序固定
const allChars = ['大', '数', '据', '', '查', '风', '险', '报', '告']
const targetChars = ref(['', '', '查']) // 目标点击顺序固定
const charPositions = ref([]) // [{char, x, y, w, h}]
const clickedIndex = ref(0)
const errorMessage = ref('')
@@ -134,7 +134,7 @@ function generateCaptcha() {
;[chars[i], chars[j]] = [chars[j], chars[i]]
}
currentChars = chars
targetChars.value = ['', '', '查']
targetChars.value = ['', '', '查']
clickedIndex.value = 0
errorMessage.value = ''
successMessage.value = ''
@@ -238,284 +238,284 @@ watch(
z-index: 9999;
padding: 1rem;
backdrop-filter: blur(4px);
animation: fadeIn 0.2s ease-out;
animation: fadeIn 0.2s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
to {
opacity: 1;
}
}
.captcha-modal {
background: #fff;
border-radius: 1rem;
width: 100%;
max-width: 360px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05);
overflow: hidden;
animation: slideUp 0.3s ease-out;
}
@keyframes slideUp {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.captcha-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.25rem 1.5rem;
border-bottom: 1px solid var(--color-border-primary, #ebedf0);
background: linear-gradient(135deg, var(--color-primary-light, rgba(140, 198, 247, 0.1)), #ffffff);
}
.captcha-title {
font-size: 1.125rem;
font-weight: 600;
color: var(--color-text-primary, #323233);
margin: 0;
background: linear-gradient(135deg, var(--color-primary, #8CC6F7), var(--color-primary-600, #709ec6));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.close-btn {
background: rgba(0, 0, 0, 0.05);
border: none;
font-size: 1.5rem;
color: var(--color-text-secondary, #646566);
cursor: pointer;
padding: 0.375rem;
width: 32px;
height: 32px;
border-radius: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
line-height: 1;
}
.close-btn:hover {
color: var(--color-text-primary, #323233);
background: rgba(0, 0, 0, 0.1);
transform: rotate(90deg);
}
.captcha-content {
padding: 1.5rem 1.5rem 1rem 1.5rem;
background: #ffffff;
}
.captcha-canvas {
width: 100%;
border-radius: 0.75rem;
background: var(--color-bg-tertiary, #f8f8f8);
display: block;
margin: 0 auto;
border: 2px solid var(--color-border-primary, #ebedf0);
transition: all 0.2s ease;
cursor: pointer;
}
.captcha-canvas:hover {
border-color: var(--color-primary, #8CC6F7);
box-shadow: 0 0 0 3px var(--color-primary-light, rgba(140, 198, 247, 0.1));
}
.captcha-instruction {
margin: 1.25rem 0 0.75rem 0;
text-align: center;
}
.captcha-instruction p {
font-size: 0.95rem;
color: var(--color-text-secondary, #646566);
margin: 0;
line-height: 1.5;
}
.target-list {
color: var(--color-primary, #8CC6F7);
font-weight: 600;
font-size: 1.05rem;
padding: 0.25rem 0.5rem;
background: var(--color-primary-light, rgba(140, 198, 247, 0.1));
border-radius: 0.375rem;
display: inline-block;
}
.captcha-status {
text-align: center;
min-height: 1.75rem;
margin-top: 0.5rem;
}
.error-message {
color: var(--color-danger, #ee0a24);
font-size: 0.875rem;
margin: 0;
padding: 0.5rem;
background: rgba(238, 10, 36, 0.1);
border-radius: 0.5rem;
animation: shake 0.3s ease-in-out;
}
@keyframes shake {
0%,
100% {
transform: translateX(0);
}
25% {
transform: translateX(-5px);
}
75% {
transform: translateX(5px);
}
}
.success-message {
color: var(--color-success, #07c160);
font-size: 0.875rem;
margin: 0;
padding: 0.5rem;
background: rgba(7, 193, 96, 0.1);
border-radius: 0.5rem;
animation: bounce 0.4s ease-out;
}
@keyframes bounce {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-3px);
}
}
.status-text {
color: var(--color-text-tertiary, #969799);
font-size: 0.875rem;
margin: 0;
}
.captcha-footer {
padding: 1.25rem 1.5rem;
border-top: 1px solid var(--color-border-primary, #ebedf0);
background: var(--color-bg-secondary, #fafafa);
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.refresh-btn {
width: 100%;
padding: 0.875rem;
background: var(--color-primary, #8CC6F7);
color: white;
border: none;
border-radius: 0.625rem;
font-size: 0.95rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(140, 198, 247, 0.3);
}
.refresh-btn:hover:not(:disabled) {
background: var(--color-primary-dark, rgba(140, 198, 247, 0.8));
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(140, 198, 247, 0.4);
}
.refresh-btn:active:not(:disabled) {
transform: translateY(0);
}
.refresh-btn:disabled {
background: var(--color-text-disabled, #c8c9cc);
cursor: not-allowed;
box-shadow: none;
}
.confirm-btn {
width: 100%;
padding: 0.875rem;
background: var(--color-success, #07c160);
color: white;
border: none;
border-radius: 0.625rem;
font-size: 0.95rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(7, 193, 96, 0.3);
}
.confirm-btn:hover:not(:disabled) {
background: rgba(7, 193, 96, 0.9);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(7, 193, 96, 0.4);
}
.confirm-btn:active:not(:disabled) {
transform: translateY(0);
}
.confirm-btn:disabled {
background: var(--color-text-disabled, #c8c9cc);
cursor: not-allowed;
box-shadow: none;
}
@media (max-width: 480px) {
.captcha-overlay {
padding: 0.75rem;
}
.captcha-modal {
background: #fff;
border-radius: 1rem;
width: 100%;
max-width: 360px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05);
overflow: hidden;
animation: slideUp 0.3s ease-out;
max-width: 100%;
border-radius: 0.875rem;
}
@keyframes slideUp {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.captcha-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.25rem 1.5rem;
border-bottom: 1px solid var(--color-border-primary, #ebedf0);
background: linear-gradient(135deg, var(--color-primary-light, rgba(140, 198, 247, 0.1)), #ffffff);
padding: 1rem 1.25rem;
}
.captcha-title {
font-size: 1.125rem;
font-weight: 600;
color: var(--color-text-primary, #323233);
margin: 0;
background: linear-gradient(135deg, var(--color-primary, #8CC6F7), var(--color-primary-600, #709ec6));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.close-btn {
background: rgba(0, 0, 0, 0.05);
border: none;
font-size: 1.5rem;
color: var(--color-text-secondary, #646566);
cursor: pointer;
padding: 0.375rem;
width: 32px;
height: 32px;
border-radius: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
line-height: 1;
}
.close-btn:hover {
color: var(--color-text-primary, #323233);
background: rgba(0, 0, 0, 0.1);
transform: rotate(90deg);
}
.captcha-content {
padding: 1.5rem 1.5rem 1rem 1.5rem;
background: #ffffff;
padding: 1.25rem 1.25rem 0.875rem 1.25rem;
}
.captcha-canvas {
width: 100%;
border-radius: 0.75rem;
background: var(--color-bg-tertiary, #f8f8f8);
display: block;
margin: 0 auto;
border: 2px solid var(--color-border-primary, #ebedf0);
transition: all 0.2s ease;
cursor: pointer;
min-height: 140px;
}
.captcha-canvas:hover {
border-color: var(--color-primary, #8CC6F7);
box-shadow: 0 0 0 3px var(--color-primary-light, rgba(140, 198, 247, 0.1));
}
.captcha-instruction {
margin: 1.25rem 0 0.75rem 0;
text-align: center;
}
.captcha-instruction p {
font-size: 0.95rem;
color: var(--color-text-secondary, #646566);
margin: 0;
line-height: 1.5;
}
.target-list {
color: var(--color-primary, #8CC6F7);
font-weight: 600;
font-size: 1.05rem;
padding: 0.25rem 0.5rem;
background: var(--color-primary-light, rgba(140, 198, 247, 0.1));
border-radius: 0.375rem;
display: inline-block;
}
.captcha-status {
text-align: center;
min-height: 1.75rem;
margin-top: 0.5rem;
}
.error-message {
color: var(--color-danger, #ee0a24);
font-size: 0.875rem;
margin: 0;
padding: 0.5rem;
background: rgba(238, 10, 36, 0.1);
border-radius: 0.5rem;
animation: shake 0.3s ease-in-out;
}
@keyframes shake {
0%,
100% {
transform: translateX(0);
}
25% {
transform: translateX(-5px);
}
75% {
transform: translateX(5px);
}
}
.success-message {
color: var(--color-success, #07c160);
font-size: 0.875rem;
margin: 0;
padding: 0.5rem;
background: rgba(7, 193, 96, 0.1);
border-radius: 0.5rem;
animation: bounce 0.4s ease-out;
}
@keyframes bounce {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-3px);
}
}
.status-text {
color: var(--color-text-tertiary, #969799);
font-size: 0.875rem;
margin: 0;
}
.captcha-footer {
padding: 1.25rem 1.5rem;
border-top: 1px solid var(--color-border-primary, #ebedf0);
background: var(--color-bg-secondary, #fafafa);
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 1rem 1.25rem;
}
.refresh-btn {
width: 100%;
padding: 0.875rem;
background: var(--color-primary, #8CC6F7);
color: white;
border: none;
border-radius: 0.625rem;
font-size: 0.95rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(140, 198, 247, 0.3);
}
.refresh-btn:hover:not(:disabled) {
background: var(--color-primary-dark, rgba(140, 198, 247, 0.8));
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(140, 198, 247, 0.4);
}
.refresh-btn:active:not(:disabled) {
transform: translateY(0);
}
.refresh-btn:disabled {
background: var(--color-text-disabled, #c8c9cc);
cursor: not-allowed;
box-shadow: none;
}
.confirm-btn {
width: 100%;
padding: 0.875rem;
background: var(--color-success, #07c160);
color: white;
border: none;
border-radius: 0.625rem;
font-size: 0.95rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(7, 193, 96, 0.3);
}
.confirm-btn:hover:not(:disabled) {
background: rgba(7, 193, 96, 0.9);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(7, 193, 96, 0.4);
}
.confirm-btn:active:not(:disabled) {
transform: translateY(0);
}
.confirm-btn:disabled {
background: var(--color-text-disabled, #c8c9cc);
cursor: not-allowed;
box-shadow: none;
}
@media (max-width: 480px) {
.captcha-overlay {
padding: 0.75rem;
}
.captcha-modal {
max-width: 100%;
border-radius: 0.875rem;
}
.captcha-header {
padding: 1rem 1.25rem;
}
.captcha-content {
padding: 1.25rem 1.25rem 0.875rem 1.25rem;
}
.captcha-canvas {
min-height: 140px;
}
.captcha-footer {
padding: 1rem 1.25rem;
}
.captcha-instruction p {
font-size: 0.875rem;
.captcha-instruction p {
font-size: 0.875rem;
}
}
</style>

View File

@@ -72,7 +72,7 @@
</div> -->
<!-- 免责声明 -->
<div class="text-xs text-center text-gray-500 leading-relaxed mt-2">
为保证用户的隐私及数据安全查询结果生成30天后将自动删除
为保证用户的隐私及数据安全查询结果生成{{ appStore.queryRetentionDays || 30 }}天后将自动删除
</div>
</div>
@@ -108,14 +108,14 @@
v-html="featureData.description">
</div>
<div class="mb-2 text-xs italic text-danger">
为保证用户的隐私以及数据安全查询的结果生成30天之后将自动清除
为保证用户的隐私以及数据安全查询的结果生成{{ appStore.queryRetentionDays || 30 }}天之后将自动清除
</div>
</div>
</div>
<!-- 支付组件 -->
<Payment v-model="showPayment" :data="featureData" :id="queryId" type="query" @close="showPayment = false" />
<BindPhoneDialog @bind-success="handleBindSuccess" />
<BindPhoneOnlyDialog @bind-success="handleBindSuccess" />
<!-- 历史查询按钮 - 仅推广查询显示 -->
<div v-if="props.type === 'promotion'" @click="toHistory"
@@ -126,7 +126,7 @@
</template>
<script setup>
import { ref, reactive, computed, onMounted, onUnmounted, nextTick } from "vue";
import { ref, reactive, computed, onMounted, onUnmounted, nextTick, watch } from "vue";
import { aesEncrypt } from "@/utils/crypto";
import { useRoute, useRouter } from "vue-router";
import { useUserStore } from "@/stores/userStore";
@@ -135,9 +135,10 @@ import { useEnv } from "@/composables/useEnv";
import { showConfirmDialog } from "vant";
import Payment from "@/components/Payment.vue";
import BindPhoneDialog from "@/components/BindPhoneDialog.vue";
import BindPhoneOnlyDialog from "@/components/BindPhoneOnlyDialog.vue";
import SectionTitle from "@/components/SectionTitle.vue";
import ReportFeatures from "@/components/ReportFeatures.vue";
import { useAppStore } from "@/stores/appStore";
// Props
const props = defineProps({
@@ -174,7 +175,7 @@ const loadProductBackground = async (productType) => {
return (await import("@/assets/images/report/xwqy_inquire_bg.png")).default;
case 'preloanbackgroundcheck':
return (await import("@/assets/images/report/dqfx_inquire_bg.png")).default;
case 'personalData':
case 'riskassessment':
return (await import("@/assets/images/report/grdsj_inquire_bg.png")).default;
case 'marriage':
return (await import("@/assets/images/report/marriage_inquire_bg.png")).default;
@@ -198,6 +199,7 @@ const router = useRouter();
const dialogStore = useDialogStore();
const userStore = useUserStore();
const { isWeChat } = useEnv();
const appStore = useAppStore();
// 响应式数据
const showPayment = ref(false);
@@ -251,6 +253,9 @@ const backgroundStyle = computed(() => {
// 动态加载牌匾背景图片
const loadTrapezoidBackground = async () => {
if (!props.feature) {
return;
}
try {
let bgModule;
if (props.feature === 'marriage') {
@@ -318,8 +323,7 @@ function handleBindSuccess() {
// 处理输入框点击事件
const handleInputClick = async () => {
if (!isLoggedIn.value) {
// 非微信浏览器环境:未登录用户提示跳转到登录页
if (!isWeChat.value) {
if (!isWeChat.value && props.type !== 'promotion') {
try {
await showConfirmDialog({
title: '提示',
@@ -333,16 +337,14 @@ const handleInputClick = async () => {
}
}
} else {
// 微信浏览器环境:已登录但检查是否需要绑定手机号
if (isWeChat.value && !userStore.mobile) {
if (isWeChat.value && !userStore.mobile && props.type !== 'promotion') {
dialogStore.openBindPhone();
}
}
};
function handleSubmit() {
// 非微信浏览器环境:检查登录状态
if (!isWeChat.value && !isLoggedIn.value) {
if (!isWeChat.value && !isLoggedIn.value && props.type !== 'promotion') {
router.push('/login');
return;
}
@@ -378,7 +380,7 @@ function handleSubmit() {
}
// 检查是否需要绑定手机号
if (!userStore.mobile) {
if (!userStore.mobile && props.type !== 'promotion') {
pendingPayment.value = true;
dialogStore.openBindPhone();
} else {
@@ -421,6 +423,9 @@ async function submitRequest() {
localStorage.setItem("token", data.value.data.accessToken);
localStorage.setItem("refreshAfter", data.value.data.refreshAfter);
localStorage.setItem("accessExpire", data.value.data.accessExpire);
// ⚠️ 重要:保存 token 后立即设置 tokenVersion防止被 checkTokenVersion 清除
const tokenVersion = import.meta.env.VITE_TOKEN_VERSION || "1.1";
localStorage.setItem("tokenVersion", tokenVersion);
}
showPayment.value = true;
@@ -488,18 +493,29 @@ const toHistory = () => {
router.push("/historyQuery");
};
// 加载背景图片
const loadBackgroundImage = async () => {
if (!props.feature) {
return;
}
const background = await loadProductBackground(props.feature);
productBackground.value = background || '';
};
// 监听 feature 变化,重新加载背景图
watch(() => props.feature, async (newFeature) => {
if (newFeature) {
await loadBackgroundImage();
await loadTrapezoidBackground();
}
}, { immediate: true });
// 生命周期
onMounted(async () => {
await loadBackgroundImage();
await loadTrapezoidBackground();
});
// 加载背景图片
const loadBackgroundImage = async () => {
const background = await loadProductBackground(props.feature);
productBackground.value = background || '';
};
onUnmounted(() => {
if (timer) {
clearInterval(timer);
@@ -576,4 +592,4 @@ button:active {
border-radius: 50%;
margin-right: 8px;
}
</style>
</style>

View File

@@ -1,10 +1,6 @@
<template>
<van-popup
v-model:show="show"
position="bottom"
class="flex flex-col justify-between p-6"
:style="{ height: '50%' }"
>
<van-popup v-model:show="show" position="bottom" class="flex flex-col justify-between p-6"
:style="{ height: '50%' }">
<div class="text-center">
<h3 class="text-lg font-bold">支付</h3>
</div>
@@ -12,11 +8,8 @@
<div class="font-bold text-xl">{{ data.product_name }}</div>
<div class="text-3xl text-red-500 font-bold">
<!-- 显示原价和折扣价格 -->
<div
v-if="discountPrice"
class="line-through text-gray-500 mt-4"
:class="{ 'text-2xl': discountPrice }"
>
<div v-if="discountPrice" class="line-through text-gray-500 mt-4"
:class="{ 'text-2xl': discountPrice }">
¥ {{ data.sell_price }}
</div>
<div>
@@ -36,63 +29,46 @@
<!-- 支付方式选择 -->
<div class="">
<van-cell-group inset>
<van-cell
v-if="isWeChat"
title="微信支付"
clickable
@click="selectedPaymentMethod = 'wechat'"
>
<!-- 开发环境测试支付选项 -->
<van-cell v-if="isDevMode" title="测试支付(开发环境)" clickable @click="selectedPaymentMethod = 'test'">
<template #icon>
<van-icon
size="24"
name="wechat-pay"
color="#1AAD19"
class="mr-2"
/>
<van-icon size="24" name="setting" color="#FF9800" class="mr-2" />
</template>
<template #right-icon>
<van-radio
v-model="selectedPaymentMethod"
name="wechat"
/>
<van-radio v-model="selectedPaymentMethod" name="test" />
</template>
</van-cell>
<van-cell v-if="isWeChat" title="微信支付" clickable @click="selectedPaymentMethod = 'wechat'">
<template #icon>
<van-icon size="24" name="wechat-pay" color="#1AAD19" class="mr-2" />
</template>
<template #right-icon>
<van-radio v-model="selectedPaymentMethod" name="wechat" />
</template>
</van-cell>
<!-- 支付宝支付 -->
<van-cell
v-else
title="支付宝支付"
clickable
@click="selectedPaymentMethod = 'alipay'"
>
<van-cell v-else title="支付宝支付" clickable @click="selectedPaymentMethod = 'alipay'">
<template #icon>
<van-icon
size="24"
name="alipay"
color="#00A1E9"
class="mr-2"
/>
<van-icon size="24" name="alipay" color="#00A1E9" class="mr-2" />
</template>
<template #right-icon>
<van-radio
v-model="selectedPaymentMethod"
name="alipay"
/>
<van-radio v-model="selectedPaymentMethod" name="alipay" />
</template>
</van-cell>
</van-cell-group>
</div>
<!-- 确认按钮 -->
<div class="">
<van-button class="w-full" round type="primary" @click="getPayment"
>确认支付</van-button
>
<van-button class="w-full" round type="primary" @click="getPayment">确认支付</van-button>
</div>
</van-popup>
</template>
<script setup>
import { ref, defineProps } from "vue";
import { ref, computed, onMounted } from "vue";
import { useRouter } from "vue-router";
const { isWeChat } = useEnv();
const props = defineProps({
@@ -110,24 +86,31 @@ const props = defineProps({
},
});
const show = defineModel();
const selectedPaymentMethod = ref(isWeChat.value ? "wechat" : "alipay");
// 判断是否为开发环境
const isDevMode = computed(() => {
return import.meta.env.MODE === 'development' || import.meta.env.DEV;
});
// 默认支付方式:开发环境优先使用测试支付,否则根据平台选择
const selectedPaymentMethod = ref(
isDevMode.value ? "test" : (isWeChat.value ? "wechat" : "alipay")
);
onMounted(() => {
if (isWeChat.value) {
selectedPaymentMethod.value = "wechat";
} else {
selectedPaymentMethod.value = "alipay";
if (!isDevMode.value) {
// 非开发环境,根据平台选择支付方式
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")
@@ -139,7 +122,14 @@ async function getPayment() {
.json();
if (data.value && !error.value) {
if (selectedPaymentMethod.value === "alipay") {
// 测试支付模式:直接跳转到结果页面
if (selectedPaymentMethod.value === "test" || selectedPaymentMethod.value === "test_empty") {
orderNo.value = data.value.data.order_no;
router.push({
path: "/payment/result",
query: { orderNo: data.value.data.order_no },
});
} else if (selectedPaymentMethod.value === "alipay") {
orderNo.value = data.value.data.order_no;
// 存储订单ID以便支付宝返回时获取
const prepayUrl = data.value.data.prepay_id;

View File

@@ -16,15 +16,18 @@
</div>
<div class="flex items-center justify-between mt-2">
<div>推广收益为<span class="text-orange-500"> {{ promotionRevenue }} </span></div>
<div>我的成本为<span class="text-orange-500"> {{ costPrice }} </span></div>
</div>
<div class="flex items-center justify-between mt-2">
<div>底价成本为<span class="text-orange-500"> {{ baseCost }} </span></div>
<div>提价成本为<span class="text-orange-500"> {{ raiseCost }} </span></div>
</div>
</div>
<div class="card m-4">
<div class="text-lg mb-2">收益与成本说明</div>
<div>推广收益 = 客户查询价 - 我的成本</div>
<div>我的成本 = 提价成本 + 价成本</div>
<div class="mt-1">提价成本超过平台标准定价部分平台会收取部分成本价</div>
<div>我的成本 = 实际底价 + 价成本</div>
<div class="mt-1">提价成本超过提价阈值部分平台会按比例收取费用</div>
<div class="">设定范围<span class="text-orange-500">{{
productConfig.price_range_min }}</span> - <span class="text-orange-500">{{
productConfig.price_range_max }}</span></div>
@@ -60,24 +63,41 @@ watch(show, () => {
const costPrice = computed(() => {
if (!productConfig.value) return 0.00
// 平台定价成本
let platformPricing = 0
platformPricing += productConfig.value.cost_price
if (price.value > productConfig.value.p_pricing_standard) {
platformPricing += (price.value - productConfig.value.p_pricing_standard) * productConfig.value.p_overpricing_ratio
// 新代理系统:成本价 = 实际底价actual_base_price+ 提价成本
const actualBasePrice = Number(productConfig.value.actual_base_price) || 0;
const priceNum = Number(price.value) || 0;
const priceThreshold = Number(productConfig.value.price_threshold) || 0;
const priceFeeRate = Number(productConfig.value.price_fee_rate) || 0;
// 计算提价成本
let priceCost = 0;
if (priceNum > priceThreshold) {
priceCost = (priceNum - priceThreshold) * priceFeeRate;
}
if (productConfig.value.a_pricing_standard > platformPricing && productConfig.value.a_pricing_end > platformPricing && productConfig.value.a_overpricing_ratio > 0) {
if (price.value > productConfig.value.a_pricing_standard) {
if (price.value > productConfig.value.a_pricing_end) {
platformPricing += (productConfig.value.a_pricing_end - productConfig.value.a_pricing_standard) * productConfig.value.a_overpricing_ratio
} else {
platformPricing += (price.value - productConfig.value.a_pricing_standard) * productConfig.value.a_overpricing_ratio
}
}
}
// 总成本 = 实际底价 + 提价成本
const totalCost = actualBasePrice + priceCost;
return safeTruncate(platformPricing)
return safeTruncate(totalCost);
})
const baseCost = computed(() => {
if (!productConfig.value) return "0.00";
const actualBasePrice = Number(productConfig.value.actual_base_price) || 0;
return safeTruncate(actualBasePrice);
})
const raiseCost = computed(() => {
if (!productConfig.value) return "0.00";
const priceNum = Number(price.value) || 0;
const priceThreshold = Number(productConfig.value.price_threshold) || 0;
const priceFeeRate = Number(productConfig.value.price_fee_rate) || 0;
let priceCost = 0;
if (priceNum > priceThreshold) {
priceCost = (priceNum - priceThreshold) * priceFeeRate;
}
return safeTruncate(priceCost);
})
const promotionRevenue = computed(() => {
@@ -158,4 +178,4 @@ const onBlurPrice = () => {
}
</script>
<style lang="scss" scoped></style>
<style lang="scss" scoped></style>

View File

@@ -1,5 +1,5 @@
<template>
<van-popup v-model:show="show" round position="bottom" :style="{ maxHeight: '95vh' }">
<template v-if="asPage">
<div class="qrcode-popup-container">
<div class="qrcode-content">
<van-swipe class="poster-swiper rounded-lg sm:rounded-xl shadow" indicator-color="white"
@@ -18,57 +18,82 @@
<van-divider class="my-2 sm:my-3">分享到好友</van-divider>
<div class="flex items-center justify-around pb-3 sm:pb-4 px-4">
<!-- 微信环境显示分享保存和复制按钮 -->
<template v-if="isWeChat">
<!-- <div class="flex flex-col items-center justify-center cursor-pointer" @click="shareToFriend">
<img src="@/assets/images/icon_share_friends.svg"
class="share-icon w-9 h-9 sm:w-10 sm:h-10 rounded-full" />
<div class="text-center mt-1 text-gray-600 text-xs">
分享给好友
</div>
</div>
<div class="flex flex-col items-center justify-center cursor-pointer" @click="shareToTimeline">
<img src="@/assets/images/icon_share_wechat.svg"
class="share-icon w-9 h-9 sm:w-10 sm:h-10 rounded-full" />
<div class="text-center mt-1 text-gray-600 text-xs">
分享到朋友圈
</div>
</div> -->
<div class="flex flex-col items-center justify-center cursor-pointer" @click="savePosterForWeChat">
<img src="@/assets/images/icon_share_img.svg"
class="share-icon w-9 h-9 sm:w-10 sm:h-10 rounded-full" />
<div class="text-center mt-1 text-gray-600 text-xs">
保存图片
</div>
<div class="text-center mt-1 text-gray-600 text-xs">保存图片</div>
</div>
<div class="flex flex-col items-center justify-center cursor-pointer" @click="copyUrl">
<img src="@/assets/images/icon_share_url.svg"
class="share-icon w-9 h-9 sm:w-10 sm:h-10 rounded-full" />
<div class="text-center mt-1 text-gray-600 text-xs">
复制链接
</div>
<div class="text-center mt-1 text-gray-600 text-xs">复制链接</div>
</div>
</template>
<!-- 非微信环境显示保存和复制按钮 -->
<template v-else>
<div class="flex flex-col items-center justify-center cursor-pointer" @click="savePoster">
<img src="@/assets/images/icon_share_img.svg"
class="share-icon w-9 h-9 sm:w-10 sm:h-10 rounded-full" />
<div class="text-center mt-1 text-gray-600 text-xs">
保存图片
</div>
<div class="text-center mt-1 text-gray-600 text-xs">保存图片</div>
</div>
<div class="flex flex-col items-center justify-center cursor-pointer" @click="copyUrl">
<img src="@/assets/images/icon_share_url.svg"
class="share-icon w-9 h-9 sm:w-10 sm:h-10 rounded-full" />
<div class="text-center mt-1 text-gray-600 text-xs">
复制链接
</div>
<div class="text-center mt-1 text-gray-600 text-xs">复制链接</div>
</div>
</template>
</div>
</div>
</van-popup>
</template>
<template v-else>
<van-popup v-model:show="show" round position="bottom" :style="{ maxHeight: '95vh' }">
<div class="qrcode-popup-container">
<div class="qrcode-content">
<van-swipe class="poster-swiper rounded-lg sm:rounded-xl shadow" indicator-color="white"
@change="onSwipeChange">
<van-swipe-item v-for="(_, index) in posterImages" :key="index">
<canvas :ref="(el) => (posterCanvasRefs[index] = el)"
class="poster-canvas rounded-lg sm:rounded-xl m-auto"></canvas>
</van-swipe-item>
</van-swipe>
</div>
<div v-if="mode === 'promote'"
class="swipe-tip text-center text-gray-700 text-xs sm:text-sm mb-1 sm:mb-2 px-2">
<span class="swipe-icon"></span> 左右滑动切换海报
<span class="swipe-icon"></span>
</div>
<van-divider class="my-2 sm:my-3">分享到好友</van-divider>
<div class="flex items-center justify-around pb-3 sm:pb-4 px-4">
<template v-if="isWeChat">
<div class="flex flex-col items-center justify-center cursor-pointer"
@click="savePosterForWeChat">
<img src="@/assets/images/icon_share_img.svg"
class="share-icon w-9 h-9 sm:w-10 sm:h-10 rounded-full" />
<div class="text-center mt-1 text-gray-600 text-xs">保存图片</div>
</div>
<div class="flex flex-col items-center justify-center cursor-pointer" @click="copyUrl">
<img src="@/assets/images/icon_share_url.svg"
class="share-icon w-9 h-9 sm:w-10 sm:h-10 rounded-full" />
<div class="text-center mt-1 text-gray-600 text-xs">复制链接</div>
</div>
</template>
<template v-else>
<div class="flex flex-col items-center justify-center cursor-pointer" @click="savePoster">
<img src="@/assets/images/icon_share_img.svg"
class="share-icon w-9 h-9 sm:w-10 sm:h-10 rounded-full" />
<div class="text-center mt-1 text-gray-600 text-xs">保存图片</div>
</div>
<div class="flex flex-col items-center justify-center cursor-pointer" @click="copyUrl">
<img src="@/assets/images/icon_share_url.svg"
class="share-icon w-9 h-9 sm:w-10 sm:h-10 rounded-full" />
<div class="text-center mt-1 text-gray-600 text-xs">复制链接</div>
</div>
</template>
</div>
</div>
</van-popup>
</template>
<!-- 图片保存指引遮罩层 -->
<ImageSaveGuide :show="showImageGuide" :image-url="currentImageUrl" :title="imageGuideTitle"
@@ -85,14 +110,30 @@ import ImageSaveGuide from "./ImageSaveGuide.vue";
const props = defineProps({
linkIdentifier: {
type: String,
required: true,
required: false, // 推广链接模式下需要
},
fullLink: {
type: String,
required: false, // 完整的推广链接(后端返回)
},
inviteLink: {
type: String,
required: false, // 邀请链接模式下需要
},
qrCodeUrl: {
type: String,
required: false, // 邀请链接模式下提供,直接使用后端返回的二维码
},
mode: {
type: String,
default: "promote", // 例如 "promote" | "invitation"
},
asPage: {
type: Boolean,
default: false,
},
});
const { linkIdentifier, mode } = toRefs(props);
const { linkIdentifier, fullLink, inviteLink, qrCodeUrl, mode, asPage } = toRefs(props);
const posterCanvasRefs = ref([]); // 用于绘制海报的canvas数组
const currentIndex = ref(0); // 当前显示的海报索引
const postersGenerated = ref([]); // 标记海报是否已经生成过将在onMounted中初始化
@@ -111,10 +152,17 @@ const showImageGuide = ref(false);
const currentImageUrl = ref('');
const imageGuideTitle = ref('');
const url = computed(() => {
const baseUrl = window.location.origin; // 获取当前站点的域名
return mode.value === "promote"
? `${baseUrl}/agent/promotionInquire/` // 使用动态的域名
: `${baseUrl}/agent/invitationAgentApply/`;
if (mode.value === "invitation" && inviteLink.value) {
// 邀请模式:使用完整的邀请链接(已包含域名)
return inviteLink.value;
}
// 推广链接模式:使用后端返回的完整短链
if (mode.value === "promote" && fullLink.value) {
return fullLink.value;
}
// 如果没有完整链接,返回空字符串(不应该发生,因为后端应该总是返回 full_link
console.warn("推广链接模式但未提供 fullLink");
return "";
});
// 海报图片数组
@@ -198,6 +246,9 @@ onMounted(async () => {
posterImages.value = await loadPosterImages();
// 根据加载的图片数量初始化postersGenerated数组
postersGenerated.value = Array(posterImages.value.length).fill(false);
if (asPage.value && url.value && !postersGenerated.value[0]) {
generatePoster(0);
}
});
// 生成海报并合成二维码
@@ -225,51 +276,67 @@ const generatePoster = async (index) => {
// 2. 绘制海报图片
ctx.drawImage(posterImg, 0, 0);
// 3. 生成二维码
QRCode.toDataURL(
generalUrl(),
{ width: 150, margin: 0 },
(err, qrCodeUrl) => {
if (err) {
console.error(err);
return;
// 3. 生成或加载二维码
const loadQRCode = (qrCodeDataUrl) => {
// 4. 加载二维码图片
const qrCodeImg = new Image();
qrCodeImg.src = qrCodeDataUrl;
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;
};
};
// 生成二维码
const generateQRCode = () => {
QRCode.toDataURL(
generalUrl(),
{ width: 150, margin: 0 },
(err, qrCodeDataUrl) => {
if (err) {
console.error(err);
return;
}
loadQRCode(qrCodeDataUrl);
}
);
};
// 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;
};
}
);
// 邀请模式:直接在前端生成二维码(使用 inviteLink
// 推广模式:也在前端生成二维码(使用 linkIdentifier 构建的 URL
generateQRCode();
};
};
// 监听 show 变化show 为 true 时生成海报
watch(show, (newVal) => {
if (newVal && !postersGenerated.value[currentIndex.value]) {
generatePoster(currentIndex.value); // 当弹窗显示且当前海报未生成时生成海报
if (!asPage.value && newVal && !postersGenerated.value[currentIndex.value]) {
generatePoster(currentIndex.value);
}
});
watch(url, (newVal) => {
if (asPage.value && newVal && !postersGenerated.value[currentIndex.value]) {
generatePoster(currentIndex.value);
}
});
@@ -289,7 +356,7 @@ const shareToFriend = () => {
? "扫码查看一查查推广信息"
: "扫码申请一查查代理权限",
link: shareUrl,
imgUrl: "https://www.quannengcha.com/logo.png"
imgUrl: "https://www.onecha.cn/logo.png"
};
configWeixinShare(shareConfig);
@@ -314,7 +381,7 @@ const shareToTimeline = () => {
? "扫码查看一查查推广信息"
: "扫码申请一查查代理权限",
link: shareUrl,
imgUrl: "https://www.quannengcha.com/logo.png"
imgUrl: "https://www.onecha.cn/logo.png"
};
configWeixinShare(shareConfig);
@@ -484,7 +551,10 @@ const tryShareAPI = async (dataURL) => {
};
const generalUrl = () => {
return url.value + encodeURIComponent(linkIdentifier.value);
// 直接使用 url computed 属性,它已经处理了所有情况
// 推广模式:优先使用 fullLink否则使用 linkIdentifier 构建
// 邀请模式:优先使用 inviteLink否则使用 linkIdentifier 构建
return url.value;
};
const copyUrl = () => {

View File

@@ -1,11 +1,16 @@
<script setup>
import { ref, computed } from "vue";
import { useRouter } from "vue-router";
import { useDialogStore } from "@/stores/dialogStore";
import { useAgentStore } from "@/stores/agentStore";
import { useUserStore } from "@/stores/userStore";
import { showToast } from "vant";
import { realNameAuth } from "@/api/agent";
const router = useRouter();
const dialogStore = useDialogStore();
const agentStore = useAgentStore();
const userStore = useUserStore();
import { showToast } from "vant";
// 表单数据
const realName = ref("");
const idCard = ref("");
@@ -104,14 +109,12 @@ async function handleSubmit() {
return;
}
const { data, error } = await useApiFetch("/agent/real_name")
.post({
name: realName.value,
id_card: idCard.value,
mobile: phoneNumber.value,
code: verificationCode.value,
})
.json();
const { data, error } = await realNameAuth({
name: realName.value,
id_card: idCard.value,
mobile: phoneNumber.value,
code: verificationCode.value,
});
if (data.value && !error.value) {
if (data.value.code === 200) {

View File

@@ -68,7 +68,7 @@ const handleShare = async () => {
try {
// 根据实际使用的标识构建请求参数
const requestData = props.orderId
? { order_id: parseInt(props.orderId) }
? { order_id: props.orderId }
: { order_no: props.orderNo };
const { data, error } = await useApiFetch("/query/generate_share_link")

View File

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

View File

@@ -0,0 +1,63 @@
<template>
<div v-if="!isWeChat" class="wechat-guard-overlay">
<div class="wechat-guard-content">
<van-icon name="warning-o" class="warning-icon" />
<h3 class="guard-title">请在微信中打开</h3>
<p class="guard-message">
本应用仅支持在微信浏览器中打开<br />
请使用微信扫描二维码或通过微信分享链接访问
</p>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useEnv } from '@/composables/useEnv'
const { isWeChat } = useEnv()
</script>
<style scoped>
.wechat-guard-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.85);
z-index: 99999;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.wechat-guard-content {
text-align: center;
color: white;
padding: 40px 32px;
max-width: 320px;
}
.warning-icon {
font-size: 64px;
color: #ff9800;
margin-bottom: 24px;
}
.guard-title {
font-size: 24px;
font-weight: 600;
margin: 0 0 16px 0;
color: white;
}
.guard-message {
font-size: 16px;
line-height: 1.6;
margin: 0;
color: rgba(255, 255, 255, 0.9);
}
</style>