This commit is contained in:
2025-04-10 23:01:03 +08:00
parent 52d8f2874f
commit dcc95ab392
78 changed files with 3866 additions and 499 deletions

BIN
public/image/help/13.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

BIN
public/image/help/14.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

BIN
public/image/help/15.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

BIN
public/image/help/18.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 864 KiB

BIN
public/image/help/19.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 619 KiB

BIN
public/image/help/20.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 KiB

BIN
public/image/help/21.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 719 KiB

BIN
public/image/help/22.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 878 KiB

BIN
public/image/help/23.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 929 KiB

BIN
public/image/help/24.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

BIN
public/image/help/25.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 886 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 940 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

BIN
public/image/shot_nonal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
public/image/shot_svip.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
public/image/shot_vip.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -1,9 +1,17 @@
<script setup>
import { RouterLink, RouterView } from 'vue-router'
const { isWeChat } = useEnv()
import { useAgentStore } from '@/stores/agentStore'
import { useUserStore } from '@/stores/userStore'
const agentStore = useAgentStore()
const userStore = useUserStore()
onMounted(() => {
RefreshToken()
const token = localStorage.getItem("token")
if (token) {
agentStore.fetchAgentStatus()
userStore.fetchUserInfo()
}
})
const RefreshToken = async () => {
const token = localStorage.getItem("token")
@@ -94,7 +102,6 @@ const h5WeixinLogin = async () => {
const h5WeixinGetCode = () => {
const currentUrl = window.location.origin;
let redirectUri = encodeURIComponent(currentUrl);
// let redirectUri = "https://www.quannengcha.com"
let appId = 'wxa581992dc74d860e';
let state = "snsapi_base"
let scope = "snsapi_base";

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

@@ -1 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1740754612192" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="39491" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M887.456923 331.598922C867.952667 146.308492 707.042557 2.464607 516.876064 0.026575 307.205315-2.411457 134.105045 163.374716 134.105045 368.169401c0 134.091758 73.140959 253.555324 190.166494 319.382188l7.314096 75.578991c2.438032 14.628192 14.628192 24.38032 29.256383 24.380319h299.877932c14.628192 0 26.818352-12.19016 29.256383-24.380319l7.314096-75.578991c131.653726-70.702927 204.794685-207.232717 190.166494-355.952667zM321.833507 360.855305c0 17.066224-14.628192 29.256384-29.256384 29.256384s-29.256384-12.19016-29.256383-29.256384c0-9.752128 0-17.066224 2.438031-26.818351 2.438032-17.066224 17.066224-26.818352 34.132448-26.818352 17.066224 2.438032 29.256384 17.066224 26.818352 31.694416-2.438032 9.752128-4.876064 14.628192-4.876064 21.942287z m190.166493-185.290429c-70.702927 0-134.091758 36.570479-165.786174 97.521279-4.876064 9.752128-14.628192 14.628192-26.818351 14.628191-4.876064 0-9.752128 0-14.628192-2.438032-14.628192-7.314096-19.504256-24.38032-12.19016-39.008511 43.884575-78.017023 126.777662-126.777662 219.422877-126.777662 17.066224 0 29.256384 12.19016 29.256384 29.256383s-12.19016 26.818352-29.256384 26.818352zM343.775794 833.833507c-2.438032 9.752128-2.438032 17.066224-2.438032 26.818351 0 90.207183 75.578991 163.348142 168.224206 163.348142 92.645215 0 168.224206-73.140959 168.224206-163.348142 0-9.752128 0-19.504256-2.438032-26.818351H343.775794z" fill="#F5B53A" p-id="39492"></path></svg>
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1741855510780" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2636" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M853.64224 76.7488H155.25888c-49.03424 0-88.75008 40.14592-88.75008 89.18528l-0.41472 766.18752c-0.00512 13.45024 16.256 20.18816 25.76384 10.68032l148.16256-148.16256a15.09376 15.09376 0 0 1 10.6752-4.41856h602.94656c57.34912 0 104.26368-46.9248 104.26368-104.2688V181.02784c0-57.35424-46.91456-104.27904-104.26368-104.27904z m-299.33056 544.1792a15.09376 15.09376 0 0 1-15.08864 15.08864H472.73472a15.09376 15.09376 0 0 1-15.09376-15.08864v-60.5696a15.09376 15.09376 0 0 1 15.09376-15.08864h66.4832a15.09376 15.09376 0 0 1 15.08864 15.08864v60.5696h0.00512z m39.45984-199.94112c-35.16928 24.76032-51.65056 48.84992-49.39776 72.30464 0.05632 0.4608 0.08704 0.9216 0.08704 1.3824a15.08864 15.08864 0 0 1-15.09376 15.08864h-48.73216a15.08352 15.08352 0 0 1-15.08864-15.08864v-6.60992c-1.32608-40.61184 15.6672-72.77056 50.92352-96.41984 0.24064-0.16384 0.49152-0.33792 0.72704-0.51712 32.6144-24.86784 48.2816-48.45056 46.99136-70.74816-2.62144-27.33056-18.0992-42.40896-46.4384-45.29152a16.45568 16.45568 0 0 0-1.81248-0.08192c-31.42144 0.3584-52.42368 19.4816-62.98112 57.3696a15.13472 15.13472 0 0 1-17.4592 10.85952L374.1696 331.23328a15.11424 15.11424 0 0 1-11.70944-18.61632c20.54144-80.15872 76.60032-118.99392 168.17152-116.5312 81.30048 5.25312 126.0032 41.89696 134.08256 109.88544 0.0512 0.38912 0.08192 0.78848 0.10752 1.1776 2.3808 44.47744-21.2992 82.41152-71.05024 113.83808z" fill="#397B8B" p-id="2637"></path></svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 480 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 422 KiB

After

Width:  |  Height:  |  Size: 369 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 619 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 719 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 878 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 929 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 KiB

View File

@@ -92,6 +92,7 @@ declare global {
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const showConfirmDialog: typeof import('vant/es')['showConfirmDialog']
const showLoadingToast: typeof import('vant/es')['showLoadingToast']
const showToast: typeof import('vant/es')['showToast']
const syncRef: typeof import('@vueuse/core')['syncRef']
@@ -114,6 +115,7 @@ declare global {
const unrefElement: typeof import('@vueuse/core')['unrefElement']
const until: typeof import('@vueuse/core')['until']
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
const useAgentStore: typeof import('./stores/agentStore.js')['useAgentStore']
const useAnimate: typeof import('@vueuse/core')['useAnimate']
const useApiFetch: typeof import('./composables/useApiFetch.js')['default']
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
@@ -271,6 +273,7 @@ declare global {
const useUni: typeof import('./composables/useUni.js')['useUni']
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
const useUserStore: typeof import('./stores/userStore.js')['useUserStore']
const useVModel: typeof import('@vueuse/core')['useVModel']
const useVModels: typeof import('@vueuse/core')['useVModels']
const useVibrate: typeof import('@vueuse/core')['useVibrate']

View File

@@ -111,9 +111,7 @@ const getSmsCode = async () => {
}
};
let timer = null
onMounted(() => {
console.log("ancestor", ancestor.value)
})
function startCountdown() {
isCountingDown.value = true
countdown.value = 60

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, "ff83609b2b24fc73196aac3d3dfb874f");
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,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>

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

@@ -0,0 +1,253 @@
<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>
<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;
}
</style>

View File

@@ -0,0 +1,13 @@
<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,14 @@
<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: "agentVip" })
}
</script>
<style lang="scss" scoped></style>

View File

@@ -54,8 +54,8 @@ const route = useRoute();
const tabbar = ref('index');
const menu = reactive([
{ title: '首页', icon: 'home-o', name: 'index' },
// { title: '推广', icon: 'balance-o', name: 'agent' },
{ title: 'AI律师', icon: 'chat-o', name: 'ai' },
{ title: '资产', icon: 'gold-coin-o', name: 'agent' },
{ title: '智能助手', icon: 'chat-o', name: 'ai' },
{ title: '我的', icon: 'user-o', name: 'me' },
{ title: '更多功能', icon: 'more-o', name: 'more' },
@@ -71,7 +71,18 @@ const onClickOverlay = () => { }
// 跳转到相应页面
const tabChange = (name) => {
if (name === 'more') {
window.location.href = 'https://www.tianyuancha.cn?_um_campaign=67bfea1c9a16fe6dcd53b9a4&_um_channel=67bfea1d9a16fe6dcd53b9a5'
showConfirmDialog({
title: name === 'marriage' ? '婚恋风险' : '更多功能',
message:
`是否前往天远查查询${name === 'marriage' ? '婚恋风险' : '更多功能'}页面?`,
})
.then(() => {
window.location.href = 'https://www.tianyuancha.cn?_um_campaign=67bfea1c9a16fe6dcd53b9a4&_um_channel=67bfea1d9a16fe6dcd53b9a5'
})
.catch(() => {
});
return
} else {
router.push({ name }); // 使用 Vue Router 进行跳转
}

View File

@@ -4,6 +4,8 @@ import GlobalLayout from "@/layouts/GlobalLayout.vue";
import HomeLayout from "@/layouts/HomeLayout.vue";
import PageLayout from "@/layouts/PageLayout.vue";
import index from "@/views/index.vue";
import { useAgentStore } from "@/stores/agentStore";
import { storeToRefs } from "pinia";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
@@ -45,7 +47,25 @@ const router = createRouter({
path: "/historyQuery",
name: "history",
component: () => import("@/views/HistoryQuery.vue"),
meta: { title: "历史报告" },
meta: { title: "历史报告", requiresAuth: true },
},
{
path: "/help",
name: "help",
component: () => import("@/views/Help.vue"),
meta: { title: "帮助中心" },
},
{
path: "/help/detail",
name: "helpDetail",
component: () => import("@/views/HelpDetail.vue"),
meta: { title: "帮助中心" },
},
{
path: "/help/guide",
name: "helpGuide",
component: () => import("@/views/HelpGuide.vue"),
meta: { title: "引导指南" },
},
{
path: "/promote",
@@ -75,7 +95,7 @@ const router = createRouter({
path: "/report",
name: "report",
component: () => import("@/views/Report.vue"),
meta: { title: "报告结果" },
meta: { title: "报告结果", requiresAuth: true },
},
{
path: "/example",
@@ -126,6 +146,103 @@ const router = createRouter({
},
],
},
{
path: "agent",
component: PageLayout,
children: [
{
path: "promoteDetails",
name: "promoteDetails",
component: () =>
import("@/views/AgentPromoteDetails.vue"),
meta: {
title: "直推报告收益明细",
requiresAuth: true,
requiresAgent: true,
},
},
{
path: "rewardsDetails",
name: "rewardsDetails",
component: () =>
import("@/views/AgentRewardsDetails.vue"),
meta: {
title: "代理奖励收益明细",
requiresAuth: true,
requiresAgent: true,
},
},
{
path: "promote",
name: "promote",
component: () => import("@/views/Promote.vue"),
meta: {
title: "直推报告",
requiresAuth: true,
requiresAgent: true,
},
},
{
path: "invitation",
name: "invitation",
component: () => import("@/views/Invitation.vue"),
meta: {
title: "邀请下级",
requiresAuth: true,
requiresAgent: true,
},
},
{
path: "agentVip",
name: "agentVip",
component: () => import("@/views/AgentVip.vue"),
meta: {
title: "代理会员",
requiresAuth: true,
requiresAgent: true,
},
},
{
path: "vipConfig",
name: "agentVipConfig",
component: () =>
import("@/views/AgentVipConfig.vue"),
meta: {
title: "代理会员报告配置",
requiresAuth: true,
requiresAgent: true,
},
},
{
path: "withdraw",
name: "withdraw",
component: () => import("@/views/Withdraw.vue"),
meta: {
title: "提现",
requiresAuth: true,
requiresAgent: true,
},
},
{
path: "withdrawDetails",
name: "withdrawDetails",
component: () =>
import("@/views/WithdrawDetails.vue"),
meta: {
title: "提现记录",
requiresAuth: true,
requiresAgent: true,
},
},
{
path: "invitationAgentApply/self",
name: "invitationAgentApplySelf",
component: () =>
import("@/views/InvitationAgentApply.vue"),
meta: { title: "代理申请", requiresAuth: true },
},
],
},
{
path: "app",
children: [{
@@ -172,11 +289,6 @@ const router = createRouter({
name: "login",
component: () => import("@/views/Login.vue"),
},
{
path: "/promotionInquire/:feature",
name: "promotionInquire",
component: () => import("@/views/PromotionInquire.vue"),
},
{
path: "/agent/promotionInquire/:linkIdentifier",
name: "promotionInquire",
@@ -220,9 +332,25 @@ NProgress.configure({
});
// 路由导航守卫
router.beforeEach((to, from, next) => {
router.beforeEach(async (to, from, next) => {
NProgress.start(); // 启动进度条
next();
const isAuthenticated = localStorage.getItem("token");
const agentStore = useAgentStore();
const { isAgent, isLoaded } = storeToRefs(agentStore);
if (to.meta.requiresAuth && !isAuthenticated) {
next("/login");
} else if (to.meta.requiresAgent && !isAgent.value) {
if (!isLoaded.value) {
await agentStore.fetchAgentStatus();
}
if (!isAgent.value) {
next("/agent/invitationAgentApply/self");
} else {
next();
}
} else {
next();
}
});
router.afterEach(() => {
@@ -232,11 +360,8 @@ router.afterEach(() => {
arguments: [
{
is_auto: false,
},
{
param1: 111,
param2: "222",
},
}
],
});
NProgress.done(); // 结束进度条

68
src/stores/agentStore.js Normal file
View File

@@ -0,0 +1,68 @@
import { defineStore } from "pinia";
export const useAgentStore = defineStore("agent", {
state: () => ({
isLoaded: false,
level: "",
status: 3, // 0=待审核1=审核通过2=审核未通过3=未申请
isAgent: false,
ancestorID: null,
agentID: null,
mobile: "",
}),
actions: {
async fetchAgentStatus() {
const { data, error } = await useApiFetch("/agent/info")
.get()
.json();
if (data.value && !error.value) {
if (data.value.code === 200) {
this.level = data.value.data.level;
this.isAgent = data.value.data.is_agent; // 判断是否是代理
this.status = data.value.data.status; // 获取代理状态 0=待审核1=审核通过2=审核未通过3=未申请
this.agentID = data.value.data.agent_id;
this.mobile = data.value.data.mobile;
// 保存到localStorage
localStorage.setItem(
"agentInfo",
JSON.stringify({
isAgent: this.isAgent,
level: this.level,
status: this.status,
agentID: this.agentID,
mobile: this.mobile,
})
);
} else {
console.log("Error fetching agent info", data.value);
}
}
this.isLoaded = true;
},
// 更新代理信息
updateAgentInfo(agentInfo) {
if (agentInfo) {
this.isAgent = agentInfo.isAgent || false;
this.level = agentInfo.level || "";
this.status = agentInfo.status || 3;
this.agentID = agentInfo.agentID || null;
this.mobile = agentInfo.mobile || "";
this.isLoaded = true;
}
},
// 重置代理信息
resetAgent() {
this.isLoaded = false;
this.level = "";
this.status = 3;
this.isAgent = false;
this.ancestorID = null;
this.agentID = null;
this.mobile = "";
},
},
});

View File

@@ -1,12 +0,0 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})

49
src/stores/userStore.js Normal file
View File

@@ -0,0 +1,49 @@
import { defineStore } from "pinia";
export const useUserStore = defineStore("user", {
state: () => ({
userName: "",
userAvatar: "",
isLoggedIn: false,
}),
actions: {
async fetchUserInfo() {
const { data, error } = await useApiFetch("/user/detail")
.get()
.json();
if (data.value && !error.value) {
if (data.value.code === 200) {
const userinfo = data.value.data.userInfo;
this.userName = userinfo.mobile || "";
this.userAvatar = userinfo.userAvatar;
this.isLoggedIn = true;
// 保存到localStorage
localStorage.setItem(
"userInfo",
JSON.stringify({
nickName: this.userName,
avatar: this.userAvatar,
})
);
}
}
},
// 更新用户信息
updateUserInfo(userInfo) {
if (userInfo) {
this.userName = userInfo.mobile || userInfo.nickName || "";
this.userAvatar = userInfo.avatar || "";
this.isLoggedIn = true;
}
},
// 重置用户信息
resetUser() {
this.userName = "";
this.userAvatar = "";
this.isLoggedIn = false;
},
},
});

View File

@@ -1,62 +1,227 @@
<template>
<div class="p-4">
<div class="p-4 bg-gradient-to-b from-blue-50/30 to-gray-50 min-h-screen">
<!-- 资产卡片 -->
<div class="bg-white bg-opacity-80 shadow-lg rounded-xl p-6 mb-6">
<div class="flex justify-between items-center">
<span class="text-xl text-gray-900 font-semibold">可提现金额</span>
<span class="text-3xl text-blue-500 font-bold">¥ 0.0</span>
<div class="rounded-xl shadow-lg mb-4 bg-gradient-to-r from-blue-50/70 to-blue-100/50 p-6">
<div class="flex justify-between items-center mb-3">
<div class="flex items-center">
<van-icon name="balance-pay" class="text-blue-500 text-xl mr-2" />
<span class="text-lg font-bold text-gray-800">余额</span>
</div>
<span class="text-3xl text-blue-600 font-bold">¥ {{ (data?.balance || 0).toFixed(2) }}</span>
</div>
<div class="mt-2 text-sm text-gray-600">累计收益¥ 0.0</div>
<div class="mt-6 grid grid-cols-3 gap-4">
<van-button type="primary" round icon="after-sale" @click="withDraw">前往提现</van-button>
<van-button type="primary" round icon="todo-list-o">提现记录</van-button>
<van-button type="primary" round icon="balance-list-o">收入明细</van-button>
</div>
</div>
<!-- 销售金额 -->
<div class="bg-white bg-opacity-80 shadow-lg rounded-xl p-6 mb-6">
<div class="flex justify-between items-center">
<span class="text-lg text-gray-900">销售金额</span>
<span class="text-xl text-blue-500">¥ 0</span>
</div>
<div class="mt-2 text-sm text-gray-600">累计销售 0 </div>
</div>
<!-- 邀请下级收益 -->
<div class="bg-white bg-opacity-80 shadow-lg rounded-xl p-6 mb-6">
<div class="flex justify-between items-center">
<span class="text-lg text-gray-900">邀请下级收益</span>
<span class="text-xl text-blue-500">¥ 0</span>
</div>
<div class="mt-2 text-sm text-gray-600">累计邀请 1 </div>
</div>
<!-- 收益统计 -->
<div class="bg-white bg-opacity-80 shadow-lg rounded-xl p-6 mb-6">
<div class="flex justify-between items-center">
<span class="text-lg text-gray-900">收益统计</span>
</div>
<div class="mt-4 flex justify-between items-center">
<button class="bg-blue-500 text-white px-6 py-2 rounded-lg shadow-md">
近7天
<div class="text-sm text-gray-500 mb-2">累计收益¥ {{ (data?.total_earnings || 0).toFixed(2) }}</div>
<div class="text-sm text-gray-500 mb-6">冻结余额¥ {{ (data?.frozen_balance || 0).toFixed(2) }}</div>
<div class="grid grid-cols-2 gap-3">
<button @click="toWithdraw" class="bg-gradient-to-r from-blue-500 to-blue-400 text-white rounded-full py-2 px-4
shadow-md flex items-center justify-center">
<van-icon name="gold-coin" class="mr-1" />
提现
</button>
<button class="bg-gray-300 text-gray-700 px-6 py-2 rounded-lg shadow-md">
近1个月
<button @click="toWithdrawDetails" class="bg-white/90 text-gray-600 border border-gray-200/50 rounded-full py-2 px-4
shadow-sm flex items-center justify-center">
<van-icon name="notes" class="mr-1" />
提现记录
</button>
</div>
<div class="mt-6 text-xl text-gray-900">近7天累计收益¥ 0.00 </div>
</div>
<!-- 直推报告收益 -->
<div class="rounded-xl shadow-lg mb-4 bg-gradient-to-r from-blue-50/40 to-cyan-50/50 p-6">
<div class="flex justify-between items-center mb-4">
<div class="flex items-center">
<van-icon name="balance-list" class="text-blue-400 text-xl mr-2" />
<span class="text-lg font-bold text-gray-800">直推报告收益</span>
</div>
<div class="text-right">
<div class="text-2xl text-blue-600 font-bold">¥ {{ (data?.direct_push?.total_commission ||
0).toFixed(2) }}</div>
<div class="text-sm text-gray-500 mt-1">有效报告 {{ data?.direct_push?.total_report || 0 }} </div>
</div>
</div>
<!-- 日期选择 -->
<div class="grid grid-cols-3 gap-2 mb-6">
<button v-for="item in promoteDateOptions" :key="item.value" @click="selectedPromoteDate = item.value"
class="rounded-full transition-all py-1 px-4 text-sm" :class="[
selectedPromoteDate === item.value
? 'bg-blue-500 text-white shadow-md'
: 'bg-white/90 text-gray-600 border border-gray-200/50'
]">
{{ item.label }}
</button>
</div>
<div class="grid grid-cols-2 gap-4 mb-6">
<div class="bg-blue-50/60 p-3 rounded-lg backdrop-blur-sm">
<div class="flex items-center text-sm text-gray-500">
<van-icon name="gold-coin" class="mr-1" />本日收益
</div>
<div class="text-xl text-blue-600 font-bold mt-1">¥ {{ currentPromoteData.commission?.toFixed(2) ||
'0.00' }}</div>
</div>
<div class="bg-blue-50/60 p-3 rounded-lg backdrop-blur-sm">
<div class="flex items-center text-sm text-gray-500">
<van-icon name="description" class="mr-1" />有效报告
</div>
<div class="text-xl text-blue-600 font-bold mt-1">{{ currentPromoteData.report || 0 }} </div>
</div>
</div>
<div class="flex items-center justify-between text-blue-500 text-sm font-semibold cursor-pointer pt-4"
@click="goToPromoteDetail">
<span>查看收益明细</span>
<span class="text-lg"></span>
</div>
</div>
<!-- 活跃下级奖励 -->
<div class="rounded-xl shadow-lg bg-gradient-to-r from-green-50/40 to-cyan-50/30 p-6">
<div class="flex justify-between items-center mb-4">
<div class="flex items-center">
<van-icon name="friends" class="text-green-500 text-xl mr-2" />
<span class="text-lg font-bold text-gray-800">活跃下级奖励</span>
</div>
<div class="text-right">
<div class="text-2xl text-green-600 font-bold">¥ {{ (data?.active_reward?.total_reward ||
0).toFixed(2) }}</div>
<div class="text-sm text-gray-500 mt-1">活跃下级 0 </div>
</div>
</div>
<!-- 日期选择 -->
<div class="grid grid-cols-3 gap-2 mb-6">
<button v-for="item in activeDateOptions" :key="item.value" @click="selectedActiveDate = item.value"
class="rounded-full transition-all py-1 px-4 text-sm" :class="[
selectedActiveDate === item.value
? 'bg-green-500 text-white shadow-md'
: 'bg-white/90 text-gray-600 border border-gray-200/50'
]">
{{ item.label }}
</button>
</div>
<div class="grid grid-cols-2 gap-2 mb-6">
<div class="bg-green-50/60 p-3 rounded-lg backdrop-blur-sm">
<div class="flex items-center text-sm text-gray-500">
<van-icon name="medal" class="mr-1" />本日奖励
</div>
<div class="text-xl text-green-600 font-bold mt-1">¥ {{ (currentActiveData.active_reward ||
0).toFixed(2) }}</div>
</div>
<div class="bg-green-50/60 p-3 rounded-lg backdrop-blur-sm">
<div class="flex items-center text-sm text-gray-500">
<van-icon name="discount" class="mr-1" />下级推广奖励
</div>
<div class="text-xl text-green-600 font-bold mt-1">¥ {{ (currentActiveData.sub_promote_reward ||
0).toFixed(2) }}</div>
</div>
<div class="bg-green-50/60 p-3 rounded-lg backdrop-blur-sm">
<div class="flex items-center text-sm text-gray-500">
<van-icon name="contact" class="mr-1" />新增活跃奖励
</div>
<div class="text-xl text-green-600 font-bold mt-1">¥ {{ (currentActiveData.sub_upgrade_reward ||
0).toFixed(2) }}</div>
</div>
<div class="bg-green-50/60 p-3 rounded-lg backdrop-blur-sm">
<div class="flex items-center text-sm text-gray-500">
<van-icon name="fire" class="mr-1" />下级转化奖励
</div>
<div class="text-xl text-green-600 font-bold mt-1">¥ {{ (currentActiveData.sub_withdraw_reward ||
0).toFixed(2) }}</div>
</div>
</div>
<div class="flex items-center justify-between text-green-500 text-sm font-semibold cursor-pointer pt-4"
@click="goToActiveDetail">
<span>查看奖励明细</span>
<span class="text-lg"></span>
</div>
</div>
</div>
</template>
<script setup>
const router = useRouter()
const withDraw = () => {
router.push({ name: "withdraw" })
}
import { storeToRefs } from 'pinia';
import { ref, computed } from 'vue';
import { useRouter } from 'vue-router';
const agentStore = useAgentStore()
const { isAgent } = storeToRefs(agentStore)
const router = useRouter();
const data = ref(null);
// 日期选项映射
const dateRangeMap = {
today: 'today',
week: 'last7d',
month: 'last30d'
};
// 直推报告数据
const promoteDateOptions = [
{ label: '今日', value: 'today' },
{ label: '近7天', value: 'week' },
{ label: '近1月', value: 'month' }
];
const selectedPromoteDate = ref('today');
// 活跃下级数据
const activeDateOptions = [
{ label: '今日', value: 'today' },
{ label: '近7天', value: 'week' },
{ label: '近1月', value: 'month' }
];
const selectedActiveDate = ref('today');
// 计算当前直推数据
const currentPromoteData = computed(() => {
const range = dateRangeMap[selectedPromoteDate.value];
return data.value?.direct_push?.[range] || { commission: 0, report: 0 };
});
// 计算当前活跃数据
const currentActiveData = computed(() => {
const range = dateRangeMap[selectedActiveDate.value];
return data.value?.active_reward?.[range] || {
active_reward: 0,
sub_promote_reward: 0,
sub_upgrade_reward: 0,
sub_withdraw_reward: 0
};
});
const getData = async () => {
const { data: res, error } = await useApiFetch("/agent/revenue")
.get()
.json();
if (res.value?.code === 200 && !error.value) {
data.value = res.value.data;
}
};
onMounted(() => {
if (isAgent.value) {
getData();
}
});
// 路由跳转
const goToPromoteDetail = () => router.push({ name: "promoteDetails" });
const goToActiveDetail = () => router.push({ name: "rewardsDetails" });
const toWithdraw = () => router.push({ name: "withdraw" });
const toWithdrawDetails = () => router.push({ name: "withdrawDetails" });
</script>
<style scoped>
/* 在这里你可以添加额外的样式 */
<style>
/* 添加按钮悬停效果 */
button {
transition: all 0.2s ease;
}
button:hover {
transform: translateY(-1px);
}
</style>

View File

@@ -5,7 +5,7 @@
<p class="indent-8 mb-2"><span></span><strong>前言</strong></p>
<p class="indent-8 mb-2">
海南省学宇思网络科技有限公司为加强对全国代理的统一管理规范各代理行为确保"全能查"的顺利推广特依据如下原则制定代理管理制度望各级代理认真贯彻严格遵守
海南天远大数据科技有限公司为加强对全国代理的统一管理规范各代理行为确保"天远数据"的顺利推广特依据如下原则制定代理管理制度望各级代理认真贯彻严格遵守
</p>
<p class="indent-8 mb-2">1.谨慎性原则</p>
<p class="indent-8 mb-2">
@@ -13,7 +13,7 @@
</p>
<p class="indent-8 mb-2">2.用心协助原则</p>
<p class="indent-8 mb-2">
海南省学宇思网络科技有限公司配合各代理的工作对于代理在推广工作中遇到的问题用心配合解决
海南天远大数据科技有限公司配合各代理的工作对于代理在推广工作中遇到的问题用心配合解决
</p>
<p class="indent-8 mb-2">3.诚信的原则</p>
<p class="indent-8 mb-2">双方务必诚实有信用决不提供虚假信息</p>
@@ -23,7 +23,7 @@
</p>
<p class="indent-8 mb-2">5.双方共赢原则</p>
<p class="indent-8 mb-2">
海南省学宇思网络科技有限公司的目标是与代理共赢共同发展
海南天远大数据科技有限公司的目标是与代理共赢共同发展
</p>
<p class="indent-8 mb-2">6.长期性原则</p>
<p class="indent-8 mb-2">
@@ -33,13 +33,13 @@
<p class="indent-8 mb-2"><strong>总则</strong></p>
<p class="indent-8 mb-2">第一条 代理期限为一年代理协议实行一年一签制</p>
<p class="indent-8 mb-2">
第二条 本制度规定海南省学宇思网络科技有限公司代理(以下称代理)权限运作及业务处理等相关事项旨在使海南省学宇思网络科技有限公司与各代理之间持续良好合作关系促进双方共同发展;
第二条 本制度规定海南天远大数据科技有限公司代理(以下称代理)权限运作及业务处理等相关事项旨在使海南天远大数据科技有限公司与各代理之间持续良好合作关系促进双方共同发展;
</p>
<p class="indent-8 mb-2">
第三条 代理经海南省学宇思网络科技有限公司授权并自代理协议书生效之日起应严格依照代理协议及本制度的规定履行义务享受权利
第三条 代理经海南天远大数据科技有限公司授权并自代理协议书生效之日起应严格依照代理协议及本制度的规定履行义务享受权利
</p>
<p class="indent-8 mb-2">
第四条 海南省学宇思网络科技有限公司确定的代理应遵循海南省学宇思网络科技有限公司的规定从事代理活动不得做出损害海南省学宇思网络科技有限公司利益和形象的行为;
第四条 海南天远大数据科技有限公司确定的代理应遵循海南天远大数据科技有限公司的规定从事代理活动不得做出损害海南天远大数据科技有限公司利益和形象的行为;
</p>
<p class="indent-8 mb-2">
第五条 代理在代理推广过程中应妥善处理做好售前售中售后的咨询维护工作
@@ -50,11 +50,11 @@
<p class="indent-8 mb-2">1完全民事行为能力人</p>
<p class="indent-8 mb-2">2本人实名认证的手机号</p>
<p class="indent-8 mb-2">3首次提现时必须进行本人实名认证并进行人脸识别</p>
<p class="indent-8 mb-2">4全面赞同全能查的各项制度并能积极参加全能查为各代理所举办的各种活动;</p>
<p class="indent-8 mb-2">4全面赞同天远数据的各项制度并能积极参加天远数据为各代理所举办的各种活动;</p>
<p class="indent-8 mb-2">企业类</p>
<p class="indent-8 mb-2">
1具有独立法人资格并能提供有效营业执照组织代码证等相关文件复印件经审查合格签定代理协议后即成为海南省学宇思网络科技有限公司认证代理
1具有独立法人资格并能提供有效营业执照组织代码证等相关文件复印件经审查合格签定代理协议后即成为海南天远大数据科技有限公司认证代理
</p>
<p class="indent-8 mb-2">
2应具备良好的经营规模办公条件设备及人员有固定的营业场所良好的资信潜力和商业信誉并提供以下资料
@@ -63,58 +63,58 @@
<p class="indent-8 mb-2">身份证复印件</p>
<p class="indent-8 mb-2">代理合作协议</p>
<p class="indent-8 mb-2">业务场景展示</p>
<p class="indent-8 mb-2">3全面赞同全能查的各项制度并能积极参加全能查为各代理所举办的各种活动;</p>
<p class="indent-8 mb-2">3全面赞同天远数据的各项制度并能积极参加天远数据为各代理所举办的各种活动;</p>
<p class="indent-8 mb-4"><strong>代理权利和义务</strong></p>
<p class="indent-8 mb-2">
在成为海南省学宇思网络科技有限公司的认证代理后可享有如下权利并承担相应的义务:
在成为海南天远大数据科技有限公司的认证代理后可享有如下权利并承担相应的义务:
</p>
<p class="indent-8 mb-2">1使用全能查开展广告宣传市场推广活动;</p>
<p class="indent-8 mb-2">2维护海南省学宇思网络科技有限公司及其产品的良好形象;</p>
<p class="indent-8 mb-2">1使用天远数据开展广告宣传市场推广活动;</p>
<p class="indent-8 mb-2">2维护海南天远大数据科技有限公司及其产品的良好形象;</p>
<p class="indent-8 mb-2">3开拓下级业务推广并负责对其定期进行业务培训;</p>
<p class="indent-8 mb-2">4推广过程中做好售前售中售后工作</p>
<p class="indent-8 mb-2">
5如用户需要开具发票代理则需向用户开具咨询费发票如代理未开具发票全能查有义务配合税务机关采取相关措施
5如用户需要开具发票代理则需向用户开具咨询费发票如代理未开具发票天远数据有义务配合税务机关采取相关措施
</p>
<p class="indent-8 mb-2">
6代理业务推广过程中未经海南省学宇思网络科技有限公司授权不得使用"全能查官方"词汇用于广告宣传
6代理业务推广过程中未经海南天远大数据科技有限公司授权不得使用"天远数据官方"词汇用于广告宣传
</p>
<p class="font-bold mb-2">推广管理</p>
<p class="indent-8 mb-2">
1全能查负责建立与代理之间的沟通与联系渠道不定期地向代理提供宣传资料信息政策以及推广方案与管理制度等方面的支持
1天远数据负责建立与代理之间的沟通与联系渠道不定期地向代理提供宣传资料信息政策以及推广方案与管理制度等方面的支持
</p>
<p class="indent-8 mb-2">
2海南省学宇思网络科技有限公司充分尊重代理代理推广权但有下列状况之一时海南省学宇思网络科技有限公司将保留或者取消该代理的权利:
2海南天远大数据科技有限公司充分尊重代理代理推广权但有下列状况之一时海南天远大数据科技有限公司将保留或者取消该代理的权利:
</p>
<p class="indent-8 mb-2">a代理经营管理不善造成工作无法正常开展的;</p>
<p class="indent-8 mb-2">b国家政策变化等不可抗力发生时;</p>
<p class="indent-8 mb-2">c遇有客户投诉经确认属代理操作不当的;</p>
<p class="indent-8 mb-2">d其他严重损害海南省学宇思网络科技有限公司形象与产品形象的行为发生时;</p>
<p class="indent-8 mb-2">d其他严重损害海南天远大数据科技有限公司形象与产品形象的行为发生时;</p>
<p class="indent-8 mb-2">e违反国家法律法规时;</p>
<p class="indent-8 mb-2">
3当代理名下发生投诉时代理需配合相关的协调否则海南省学宇思网络科技有限公司有权无条件取消其代理资格终止其代理协议
3当代理名下发生投诉时代理需配合相关的协调否则海南天远大数据科技有限公司有权无条件取消其代理资格终止其代理协议
</p>
<p class="indent-8 mb-2">4代理应合规宣传海南省学宇思网络科技有限公司产品形象</p>
<p class="indent-8 mb-2">4代理应合规宣传海南天远大数据科技有限公司产品形象</p>
<p class="indent-8 mb-2">
5市场运作过程中各代理在接到市场投诉时应及时做好记录并报海南省学宇思网络科技有限公司相关部门妥善处理
5市场运作过程中各代理在接到市场投诉时应及时做好记录并报海南天远大数据科技有限公司相关部门妥善处理
</p>
<p class="indent-8 mb-2"><strong>违规处罚</strong></p>
<p class="indent-8 mb-2">
1各代理在推广海南省学宇思网络科技有限公司过程中有损害海南省学宇思网络科技有限公司产品信誉行为时视情节轻重海南省学宇思网络科技有限公司将对其提出书面警告直至取消其代理资格;
1各代理在推广海南天远大数据科技有限公司过程中有损害海南天远大数据科技有限公司产品信誉行为时视情节轻重海南天远大数据科技有限公司将对其提出书面警告直至取消其代理资格;
</p>
<p class="indent-8 mb-2">
2未按海南省学宇思网络科技有限公司有关规定和本制度开展工作的海南省学宇思网络科技有限公司将提出书面警告并限期整改;
2未按海南天远大数据科技有限公司有关规定和本制度开展工作的海南天远大数据科技有限公司将提出书面警告并限期整改;
</p>
<p class="indent-8 mb-2">
3不遵守海南省学宇思网络科技有限公司的相关规章制度造成与其他推广代理纠纷时海南省学宇思网络科技有限公司将视其情节轻重处以20000元以上50000元以下的罚款并取消其代理资格
3不遵守海南天远大数据科技有限公司的相关规章制度造成与其他推广代理纠纷时海南天远大数据科技有限公司将视其情节轻重处以20000元以上50000元以下的罚款并取消其代理资格
</p>
<p class="indent-8 mb-2">
4违反保密义务导致海南省学宇思网络科技有限公司重大损失的海南省学宇思网络科技有限公司将对其处以5000-20000元罚款情节严重者将直接取消其代理资格
4违反保密义务导致海南天远大数据科技有限公司重大损失的海南天远大数据科技有限公司将对其处以5000-20000元罚款情节严重者将直接取消其代理资格
</p>
<p class="indent-8 mb-2">
5代理如严重违反海南省学宇思网络科技有限公司相关规章制度海南省学宇思网络科技有限公司可随时解除双方约定的部分或全部协议
5代理如严重违反海南天远大数据科技有限公司相关规章制度海南天远大数据科技有限公司可随时解除双方约定的部分或全部协议
</p>
<p class="indent-8 mb-2"><strong>投诉类处罚</strong></p>
@@ -379,12 +379,12 @@
<p class="mb-2">14恶意投诉比如没有异议非说有异议且无法提供有效证明材料各种奇葩投诉</p>
<p class="mb-2">15租用账号发布不良言论诈骗信息</p>
<p class="mb-4">16发布不当政治言论或者任何违反国家法规政策的言论</p>
<p class="mb-4">更多详细内容请认真阅读全能查代理协议</p>
<p class="mb-4">更多详细内容请认真阅读天远数据代理协议</p>
<h3 class="font-bold mb-2">退款的规则及途径</h3>
<h4 class="font-bold mb-2">退款规则</h4>
<p class="mb-2">1自订单支付完成后30天内为有效期在30天内可申请退款</p>
<p class="mb-2">2超过报告有效期30则无法办理退款</p>
<p class="mb-2">1自订单支付完成后3天内为有效期在3天内可申请退款</p>
<p class="mb-2">2超过报告有效期3天则无法办理退款</p>
<p class="mb-2">3符合相关退款条件的用户退款时仅退还实付金额</p>
<p class="mb-2">
4用户购买报告成功后因不可抗力等法定原因或平台原因导致平台无法提供服务用户可联系客服发起退款
@@ -434,23 +434,23 @@
<h4 class="font-bold mb-2">补充说明</h4>
<p class="indent-8 mb-4">
如您需要退款的产品类型不在以上30或者超出了30 天限制则无法办理退款如您有产品使用方面的疑问您可以通过联系客服进行反馈
如您需要退款的产品类型不在以上3天或者超出了30 天限制则无法办理退款如您有产品使用方面的疑问您可以通过联系客服进行反馈
</p>
<p class="indent-8 mb-2"><strong>附则</strong></p>
<p class="indent-8 mb-2">1本制度作为代理协议之附件与代理协议具有同等法律效力</p>
<p class="indent-8 mb-2">
2海南省学宇思网络科技有限公司将本着"诚信为本、长期服务"的宗旨和"公平合理"的原则对代理进行合理布局和调整以实现互利互惠共同快速发展的目的
2海南天远大数据科技有限公司将本着"诚信为本、长期服务"的宗旨和"公平合理"的原则对代理进行合理布局和调整以实现互利互惠共同快速发展的目的
</p>
<p class="indent-8 mb-2">3因其他原因需终止代理关系需向海南省学宇思网络科技有限公司提出书面申请</p>
<p class="indent-8 mb-2">3因其他原因需终止代理关系需向海南天远大数据科技有限公司提出书面申请</p>
<p class="indent-8 mb-2">
4代理之间发生业务竞争和冲突海南省学宇思网络科技有限公司将依据公平公正公开的原则按相关制度予以调解处理
4代理之间发生业务竞争和冲突海南天远大数据科技有限公司将依据公平公正公开的原则按相关制度予以调解处理
</p>
<p class="indent-8 mb-2">
5如海南省学宇思网络科技有限公司与各代理之间出现协议上的纠纷由海南省学宇思网络科技有限公司所在地法院裁决
5如海南天远大数据科技有限公司与各代理之间出现协议上的纠纷由海南天远大数据科技有限公司所在地法院裁决
</p>
<p class="indent-8 mb-2">
6本制度的制定修改与废止皆经由海南省学宇思网络科技有限公司讨论决定解释权归海南省学宇思网络科技有限公司所有
6本制度的制定修改与废止皆经由海南天远大数据科技有限公司讨论决定解释权归海南天远大数据科技有限公司所有
</p>
<p class="indent-8 mb-4">7本制度于2022年1月1日起实施公司将根据实施情况对本制度进行修正和调整</p>

View File

@@ -0,0 +1,117 @@
<template>
<div class="min-h-screen bg-gray-50">
<!-- 收益列表 -->
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
<div v-for="(item, index) in data.list" :key="index" class="mx-4 my-2 bg-white rounded-lg p-4 shadow-sm">
<div class="flex justify-between items-center mb-2">
<span class="text-gray-500 text-sm">{{ item.create_time || '-' }}</span>
<span class="text-green-500 font-bold">+{{ item.amount.toFixed(2) }}</span>
</div>
<div class="flex items-center">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium"
:class="getReportTypeStyle(item.product_name)">
<span class="w-2 h-2 rounded-full mr-1" :class="getDotColor(item.product_name)"></span>
{{ item.product_name }}
</span>
</div>
</div>
</van-list>
</div>
</template>
<script setup>
// 颜色配置(根据产品名称映射)
const typeColors = {
'老板企业报告': { bg: 'bg-blue-100', text: 'text-blue-800', dot: 'bg-blue-500' },
'人事背调': { bg: 'bg-green-100', text: 'text-green-800', dot: 'bg-green-500' },
'家政风险': { bg: 'bg-purple-100', text: 'text-purple-800', dot: 'bg-purple-500' },
'婚恋风险': { bg: 'bg-pink-100', text: 'text-pink-800', dot: 'bg-pink-500' },
'贷前背调': { bg: 'bg-orange-100', text: 'text-orange-800', dot: 'bg-orange-500' },
'租赁风险': { bg: 'bg-indigo-100', text: 'text-indigo-800', dot: 'bg-indigo-500' },
'个人风险': { bg: 'bg-red-100', text: 'text-red-800', dot: 'bg-red-500' },
// 默认类型
'default': { bg: 'bg-gray-100', text: 'text-gray-800', dot: 'bg-gray-500' }
}
const page = ref(1)
const pageSize = ref(10)
const data = ref({
total: 0,
list: []
})
const loading = ref(false)
const finished = ref(false)
// 获取颜色样式
const getReportTypeStyle = (name) => {
const color = typeColors[name] || typeColors.default
return `${color.bg} ${color.text}`
}
// 获取小圆点颜色
const getDotColor = (name) => {
return (typeColors[name] || typeColors.default).dot
}
// 加载更多数据
const onLoad = async () => {
if (!finished.value) {
page.value++
await getData()
}
}
// 获取数据
const getData = async () => {
try {
loading.value = true
const { data: res, error } = await useApiFetch(
`/agent/commission?page=${page.value}&page_size=${pageSize.value}`
).get().json()
if (res.value?.code === 200 && !error.value) {
// 首次加载
if (page.value === 1) {
data.value = res.value.data
} else {
// 分页加载
data.value.list.push(...res.value.data.list)
}
// 判断是否加载完成
if (data.value.list.length >= res.value.data.total ||
res.value.data.list.length < pageSize.value) {
finished.value = true
}
}
} finally {
loading.value = false
}
}
// 初始化加载
onMounted(() => {
getData()
})
</script>
<style scoped>
/* 列表项入场动画 */
.list-enter-active {
transition: all 0.3s ease;
}
.list-enter-from {
opacity: 0;
transform: translateY(20px);
}
/* 适配vant组件 */
:deep(.van-list__finished-text) {
@apply py-4 text-gray-400 text-sm;
}
:deep(.van-list__loading) {
@apply py-4;
}
</style>

View File

@@ -0,0 +1,137 @@
<template>
<div class="min-h-screen bg-gray-50">
<!-- 收益列表 -->
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
<div v-for="(item, index) in data.list" :key="index" class="mx-4 my-2 bg-white rounded-lg p-4 shadow-sm">
<div class="flex justify-between items-center mb-2">
<span class="text-gray-500 text-sm">{{ item.create_time || '-' }}</span>
<span class="text-green-500 font-bold">+{{ item.amount.toFixed(2) }}</span>
</div>
<div class="flex items-center">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium"
:class="getReportTypeStyle(item.type)">
<span class="w-2 h-2 rounded-full mr-1" :class="getDotColor(item.type)"></span>
{{ typeToChinese(item.type) }}
</span>
</div>
</div>
</van-list>
</div>
</template>
<script setup>
// 类型映射配置
const typeConfig = {
descendant_promotion: {
chinese: '下级推广奖励',
color: { bg: 'bg-blue-100', text: 'text-blue-800', dot: 'bg-blue-500' }
},
descendant_upgrade_vip: {
chinese: '下级升级VIP奖励',
color: { bg: 'bg-green-100', text: 'text-green-800', dot: 'bg-green-500' }
},
descendant_upgrade_svip: {
chinese: '下级升级SVIP奖励',
color: { bg: 'bg-purple-100', text: 'text-purple-800', dot: 'bg-purple-500' }
},
descendant_stay_activedescendant: {
chinese: '下级活跃奖励',
color: { bg: 'bg-pink-100', text: 'text-pink-800', dot: 'bg-pink-500' }
},
new_active: {
chinese: '新增活跃奖励',
color: { bg: 'bg-orange-100', text: 'text-orange-800', dot: 'bg-orange-500' }
},
descendant_withdraw: {
chinese: '下级提现奖励',
color: { bg: 'bg-indigo-100', text: 'text-indigo-800', dot: 'bg-indigo-500' }
},
default: {
chinese: '其他奖励',
color: { bg: 'bg-gray-100', text: 'text-gray-800', dot: 'bg-gray-500' }
}
}
const page = ref(1)
const pageSize = ref(10)
const data = ref({
total: 0,
list: []
})
const loading = ref(false)
const finished = ref(false)
// 类型转中文
const typeToChinese = (type) => {
return typeConfig[type]?.chinese || typeConfig.default.chinese
}
// 获取颜色样式
const getReportTypeStyle = (type) => {
const config = typeConfig[type] || typeConfig.default
return `${config.color.bg} ${config.color.text}`
}
// 获取小圆点颜色
const getDotColor = (type) => {
return typeConfig[type]?.color.dot || typeConfig.default.color.dot
}
// 加载更多数据
const onLoad = async () => {
if (!finished.value) {
page.value++
await getData()
}
}
// 获取数据
const getData = async () => {
try {
loading.value = true
const { data: res, error } = await useApiFetch(
`/agent/rewards?page=${page.value}&page_size=${pageSize.value}`
).get().json()
if (res.value?.code === 200 && !error.value) {
if (page.value === 1) {
data.value = res.value.data
} else {
data.value.list.push(...res.value.data.list)
}
if (data.value.list.length >= res.value.data.total ||
res.value.data.list.length < pageSize.value) {
finished.value = true
}
}
} finally {
loading.value = false
}
}
// 初始化加载
onMounted(() => {
getData()
})
</script>
<style scoped>
/* 保持原有样式不变 */
.list-enter-active {
transition: all 0.3s ease;
}
.list-enter-from {
opacity: 0;
transform: translateY(20px);
}
:deep(.van-list__finished-text) {
@apply py-4 text-gray-400 text-sm;
}
:deep(.van-list__loading) {
@apply py-4;
}
</style>

View File

@@ -6,7 +6,7 @@
<p class="text-left"><span></span></p>
<p class="text-left"><span class="text-black">甲方</span></p>
<p class="text-left"><span class="text-black">乙方</span><span
class="text-black">海南省学宇思网络科技有限公司</span>
class="text-black">海南天远大数据科技有限公司</span>
</p>
<p class="text-left">&nbsp;</p>
<p class="text-left"><span class="text-black">鉴于</span></p>
@@ -18,7 +18,7 @@
<p class="text-left"><span class="text-black">
现双方根据中华人民共和国</span><span class="text-black">民法典</span><span
class="text-black">等相关法律法规本着诚实信用公平促进社会诚信发展为原则经友好协商就</span><span
class="text-black">海南省学宇思网络科技有限公司</span><span
class="text-black">海南天远大数据科技有限公司</span><span
class="text-black">信息技术服务事宜达成一致签订本合同</span></p>
<p class="text-left"><span></span></p>
<p class="text-left"></p>
@@ -27,7 +27,7 @@
<p class="text-left"><span class="text-black">
除上下文另有约定外下列用语具有如下含义</span></p>
<p class="text-left"><span class="text-black">1.1
&nbsp; &nbsp;</span><span class="text-black">海南省学宇思网络科技有限公司</span><span
&nbsp; &nbsp;</span><span class="text-black">海南天远大数据科技有限公司</span><span
class="text-black">信息技术服务服务
指乙方通过信息化人工智能和信息科技等技术手段对</span><span class="text-black">大数据</span><span
class="text-black">进行以公众号小程序APPweb页面以下简称平台或标准接口形式为客户提供的服务协助客户完成信息的整理管理等业务流程</span>
@@ -62,7 +62,7 @@
合作内容与方式</span></p>
<p class="text-left"><span class="text-black">2.1
&nbsp; &nbsp;根据本合同约定的条件和条款甲方使用乙方提供的</span><span
class="text-black">海南省学宇思网络科技有限公司</span><span class="text-black">相关</span><span
class="text-black">海南天远大数据科技有限公司</span><span class="text-black">相关</span><span
class="text-black">信息技术服务简称乙方服务本服务</span></p>
<p class="text-left"><span class="text-black">2.2
&nbsp; &nbsp;

21
src/views/AgentVip.vue Normal file
View File

@@ -0,0 +1,21 @@
<template>
<div class="relative">
<img class="" src="@/assets/images/vip_bg.png" alt="代理会员">
<div @click="toService" class="absolute left-[50%] translate-x-[-50%] bottom-80
bg-gradient-to-r from-gray-900 via-black to-gray-900 <!-- 按钮自身渐变 -->
py-2 px-4 rounded-lg text-white text-[24px] font-bold
shadow-[0_0_15px_rgba(255,255,255,0.3)] <!-- 发光效果 -->
hover:scale-105 transition-transform"> <!-- 悬停动画 -->
点击马上报名
</div>
</div>
</template>
<script setup>
function toService() {
// window.location.href = '/service' // 跳转到客服页面
window.location.href = 'https://work.weixin.qq.com/kfid/kfc8a32720024833f57' // 跳转到客服页面
}
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,434 @@
<template>
<div class="p-4 max-w-3xl mx-auto min-h-screen">
<!-- 标题部分 -->
<div class="card mb-4 p-4 bg-gradient-to-r from-blue-500 to-blue-600 rounded-lg shadow-lg text-white">
<h1 class="text-2xl font-extrabold mb-2">专业报告定价配置</h1>
<p class="opacity-90">请选择报告类型并设置定价策略助您实现精准定价</p>
</div>
<!-- 报告选择器 -->
<div class="mb-4">
<van-field readonly clickable name="reportType" v-model="selectedReportText" label="报告类型"
placeholder="点击选择报告" @click="showPicker = true" class="card">
<template #label>
<span class="text-blue-600 font-medium">📝 选择报告</span>
</template>
<template #right-icon>
<van-icon name="arrow-down" class="text-gray-400" />
</template>
</van-field>
<van-popup v-model:show="showPicker" position="bottom">
<van-picker :columns="reportOptions" :default-index="0" @confirm="onConfirm"
@cancel="showPicker = false" />
</van-popup>
</div>
<div v-if="selectedReportText" class="space-y-6">
<!-- 配置卡片 -->
<div class="card">
<!-- 当前报告标题 -->
<div class="flex items-center mb-6">
<van-icon name="description" class="text-blue-500 text-xl mr-2" />
<h2 class="text-xl font-semibold text-gray-800">
{{ selectedReportText }}配置
</h2>
</div>
<!-- 显示当前产品的基础成本信息 -->
<div v-if="currentProductConf && currentProductConf.cost_price"
class="px-4 py-2 mb-4 bg-gray-50 border border-gray-200 rounded-lg shadow-sm">
<div class="text-lg font-semibold text-gray-700">产品基础信息</div>
<div class="mt-1 text-sm text-gray-600">
<div>基础成本价<span class="font-medium">{{ currentProductConf.cost_price }}</span> </div>
<div>最高设定金额上限<span class="font-medium">{{ currentProductConf.price_range_max }}</span> </div>
<div>最高设定比例上限<span class="font-medium">{{ priceRatioMax }}</span> %</div>
</div>
</div>
<!-- 分隔线 -->
<van-divider :style="{ borderColor: '#e5e7eb', padding: '0 16px' }" class="my-6">
<van-icon name="exchange" class="text-gray-400 mx-2" />
<span class="text-gray-400 text-sm">成本策略配置</span>
</van-divider>
<!-- 加价金额 -->
<van-field v-model.number="currentConfig.price_increase_amount" label="加价金额" type="number"
placeholder="0" @blur="validateDecimal('price_increase_amount')" class="custom-field"
:class="{ 'van-field--error': increaseError }">
<template #label>
<span class="text-gray-600 font-medium">🚀 加价金额</span>
</template>
<template #extra>
<span class="text-blue-500 font-medium ml-2"></span>
</template>
</van-field>
<div class="text-xs text-gray-400 mt-1">
提示最大加价金额为{{ priceIncreaseAmountMax }}<br>
说明加价金额是在基础成本价上增加的额外费用决定下级报告的最低定价您将获得所有输入的金额利润
</div>
<!-- 分隔线 -->
<van-divider :style="{ borderColor: '#e5e7eb', padding: '0 16px' }" class="my-6">
<van-icon name="exchange" class="text-gray-400 mx-2" />
<span class="text-gray-400 text-sm">定价策略配置</span>
</van-divider>
<!-- 定价区间最低 -->
<van-field v-model.number="currentConfig.price_range_from" label="定价区间最低" type="number" placeholder="0"
@blur="() => { validateDecimal('price_range_from'); validateRange(); }" class="custom-field"
:class="{ 'van-field--error': rangeError }">
<template #label>
<span class="text-gray-600 font-medium">💰 最低金额</span>
</template>
<template #extra>
<span class="text-blue-500 font-medium ml-2"></span>
</template>
</van-field>
<div class="text-xs text-gray-400 mt-1">
提示最低金额不能低于基础最低 {{ currentProductConf?.price_range_min || 0 }} + 加价金额<br>
说明设定的最低金额为定价区间的起始值若下级设定的报告金额在区间内则区间内部分将按比例获得收益
</div>
<!-- 定价区间最高 -->
<van-field v-model.number="currentConfig.price_range_to" label="定价区间最高" type="number" placeholder="0"
@blur="() => { validateDecimal('price_range_to'); validateRange(); }" class="custom-field"
:class="{ 'van-field--error': rangeError }">
<template #label>
<span class="text-gray-600 font-medium">💰 最高金额</span>
</template>
<template #extra>
<span class="text-blue-500 font-medium ml-2"></span>
</template>
</van-field>
<div class="text-xs text-gray-400 mt-1">
提示最高金额不能超过上限{{ currentProductConf?.price_range_max || 0 }}且不得小于最低金额{{ priceIncreaseMax }}<br>
说明设定的最高金额为定价区间的结束值若下级设定的报告金额在区间内则区间内部分将按比例获得收益
</div>
<!-- 收取比例 -->
<van-field v-model.number="currentConfig.price_ratio" label="收取比例" type="digit" placeholder="0"
@blur="() => { validateRatio(); }" class="custom-field" :class="{ 'van-field--error': ratioError }">
<template #label>
<span class="text-gray-600 font-medium">📈 收取比例</span>
</template>
<template #extra>
<span class="text-blue-500 font-medium ml-2">%</span>
</template>
</van-field>
<div class="text-xs text-gray-400 mt-1">
提示最大收取比例为{{ priceRatioMax }}%<br>
说明收取比例表示对定价区间内即报告金额超过最低金额小于最高金额部分的金额按此比例进行利润分成
</div>
</div>
<!-- 保存按钮 -->
<van-button type="primary" block
class="shadow-lg bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white rounded-xl h-12"
@click="handleSubmit">
<van-icon name="success" class="mr-2" />
保存当前报告配置
</van-button>
</div>
<!-- 未选择提示 -->
<div v-else class="text-center py-12">
<van-icon name="warning" class="text-gray-400 text-4xl mb-4" />
<p class="text-gray-500">请先选择需要配置的报告类型</p>
</div>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted, watch } from 'vue';
import { showToast } from 'vant';
// 报告类型选项
const reportOptions = [
{ text: '人事背调', value: 'backgroundcheck', id: 1 },
{ text: '家政风险', value: 'homeservice', id: 3 },
{ text: '婚恋风险', value: 'marriage', id: 4 },
{ text: '贷前背调', value: 'preloanbackgroundcheck', id: 5 },
{ text: '租赁风险', value: 'rentalrisk', id: 6 },
{ text: '个人风险', value: 'riskassessment', id: 7 },
{ text: '老板企业报告', value: 'companyinfo', id: 2 },
];
// 状态管理
const showPicker = ref(false);
const selectedReport = ref(reportOptions[0]);
const selectedReportText = ref(reportOptions[0].text);
const selectedReportId = ref(reportOptions[0].id);
const rangeError = ref(false);
const ratioError = ref(false);
const increaseError = ref(false);
// 缓存当前报告配置数据agent_membership_user_config
const configCache = reactive({});
// 缓存产品配置数据product_config
const productConfigCache = reactive({});
// 监听 selectedReportId 变化,更新当前报告信息,并获取配置数据
watch(selectedReportId, (newVal) => {
const report = reportOptions.find(r => r.id === newVal);
selectedReport.value = report;
selectedReportText.value = report.text;
getConfig();
});
// 当前配置计算属性agent_membership_user_config
const currentConfig = computed(() => {
if (!configCache[selectedReportId.value]) {
configCache[selectedReportId.value] = {
price_range_from: null,
price_range_to: null,
price_ratio: null,
price_increase_amount: null,
};
}
return configCache[selectedReportId.value];
});
// 当前产品配置计算属性
const currentProductConf = computed(() => {
return productConfigCache[selectedReportId.value] || {};
});
// 定义动态限制值
const priceIncreaseMax = ref(null);
const priceIncreaseAmountMax = ref(null);
const priceRatioMax = ref(null);
// 获取配置接口,根据当前报告的 product_id 作为 query 参数
const getConfig = async () => {
try {
const { data } = await useApiFetch("/agent/membership/user_config?product_id=" + selectedReportId.value)
.get()
.json();
if (data.value?.code === 200) {
// 处理 agent_membership_user_config
const cfg = data.value.agent_membership_user_config;
if (!cfg || Number(cfg.product_id) === 0) {
// 未配置状态,初始化为空数据
configCache[selectedReportId.value] = {
price_range_from: null,
price_range_to: null,
price_ratio: null,
price_increase_amount: null,
product_id: 0
};
} else {
// 已配置,映射接口字段(示例映射)
configCache[selectedReportId.value] = {
price_range_from: cfg.a_pricing_standard,
price_range_to: cfg.a_pricing_end,
price_ratio: cfg.a_overpricing_ratio * 100,
price_increase_amount: cfg.price_increase_amount ?? null,
product_id: cfg.product_id
};
}
// 保存产品配置数据
productConfigCache[selectedReportId.value] = data.value.product_config;
// 更新动态限制值
priceIncreaseMax.value = data.value.price_increase_max;
priceIncreaseAmountMax.value = data.value.price_increase_amount;
priceRatioMax.value = data.value.price_ratio * 100;
}
} catch (error) {
showToast('配置加载失败');
}
};
onMounted(() => {
getConfig();
});
// 金额输入格式验证,确保最多两位小数
const validateDecimal = (field) => {
const value = currentConfig.value[field];
if (value === null || value === undefined) return;
const numValue = Number(value);
if (isNaN(numValue)) {
currentConfig.value[field] = null;
return;
}
const fixedValue = parseFloat(numValue.toFixed(2));
currentConfig.value[field] = fixedValue;
if (field === 'price_increase_amount') {
if (fixedValue > priceIncreaseAmountMax.value) {
currentConfig.value[field] = priceIncreaseAmountMax.value;
showToast(`加价金额最大为${priceIncreaseAmountMax.value}`);
increaseError.value = true;
setTimeout(() => {
increaseError.value = false;
}, 2000);
} else {
increaseError.value = false;
}
// 当加价金额变化后,重新验证价格区间
validateRange();
}
};
// 价格区间验证
const validateRange = () => {
if (currentConfig.value.price_range_from === null || currentConfig.value.price_range_to === null) {
rangeError.value = false;
return;
}
if (isNaN(currentConfig.value.price_range_from) || isNaN(currentConfig.value.price_range_to)) return;
const productConf = currentProductConf.value;
const additional = currentConfig.value.price_increase_amount || 0;
const minAllowed = productConf.cost_price + additional;
const maxAllowed = productConf.price_range_max;
if (currentConfig.value.price_range_from < minAllowed) {
currentConfig.value.price_range_from = minAllowed;
showToast(`最低金额不能低于成本价 ${minAllowed}`);
rangeError.value = true;
closeRangeError();
currentConfig.value.price_range_to = currentConfig.value.price_range_from + priceIncreaseMax.value;
return;
}
if (currentConfig.value.price_range_to < currentConfig.value.price_range_from) {
showToast('最高金额不能低于最低金额');
currentConfig.value.price_range_to = (currentConfig.value.price_range_from + priceIncreaseMax.value) > maxAllowed
? maxAllowed
: currentConfig.value.price_range_from + priceIncreaseMax.value;
rangeError.value = true;
closeRangeError();
return;
}
const diff = currentConfig.value.price_range_to - currentConfig.value.price_range_from;
if (diff > priceIncreaseMax.value) {
showToast(`价格区间最大差值为${priceIncreaseMax.value}`);
currentConfig.value.price_range_to = currentConfig.value.price_range_from + priceIncreaseMax.value;
closeRangeError();
return;
}
if (currentConfig.value.price_range_to > maxAllowed) {
currentConfig.value.price_range_to = maxAllowed;
showToast(`最高金额不能超过 ${maxAllowed}`);
closeRangeError();
}
if (!rangeError.value) {
rangeError.value = false;
}
};
const closeRangeError = () => {
setTimeout(() => {
rangeError.value = false;
}, 2000);
};
// 收取比例验证(保留两位小数)
const validateRatio = () => {
let value = currentConfig.value.price_ratio;
if (value === null || value === undefined) return;
const numValue = Number(value);
if (isNaN(numValue)) {
currentConfig.value.price_ratio = null;
ratioError.value = true;
return;
}
if (numValue > priceRatioMax.value) {
currentConfig.value.price_ratio = priceRatioMax.value;
showToast(`收取比例最大为${priceRatioMax.value}%`);
ratioError.value = true;
setTimeout(() => {
ratioError.value = false;
}, 1000);
} else if (numValue < 0) {
currentConfig.value.price_ratio = 0;
ratioError.value = true;
} else {
currentConfig.value.price_ratio = parseFloat(numValue.toFixed(2));
ratioError.value = false;
}
};
// 提交处理
const handleSubmit = async () => {
try {
const submitData = {
product_id: selectedReportId.value,
price_range_from: currentConfig.value.price_range_from || 0,
price_range_to: currentConfig.value.price_range_to || 0,
price_ratio: (currentConfig.value.price_ratio || 0) / 100,
price_increase_amount: currentConfig.value.price_increase_amount || 0,
};
const isValid = Object.values(currentConfig.value).every(val => val !== null) &&
currentConfig.value.price_range_to >= currentConfig.value.price_range_from;
if (!isValid) return;
// 提交请求示例,请根据实际接口修改
// const { data } = await useApiFetch("/agent/membership/user_config")
// .post({ configs: submitData })
// .json();
// if (data.value?.code === 200) {
// showToast({ message: '配置保存成功', position: 'top' });
// getConfig();
// }
console.log("submitData", submitData);
} catch (error) {
showToast('保存失败,请稍后重试');
}
};
// Picker确认
const onConfirm = ({ selectedOptions }) => {
selectedReport.value = selectedOptions[0];
selectedReportText.value = selectedOptions[0].text;
selectedReportId.value = selectedOptions[0].id;
showPicker.value = false;
rangeError.value = false;
ratioError.value = false;
increaseError.value = false;
// 切换报告后获取最新配置
getConfig();
};
onMounted(() => {
getConfig();
});
</script>
<style>
.custom-field {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.custom-field .van-field__body {
@apply bg-gray-50 rounded-lg px-3 py-2;
transition: all 0.3s ease;
}
.custom-field:focus-within .van-field__body {
@apply ring-2 ring-blue-200;
}
.van-picker__toolbar {
@apply bg-gray-50 rounded-t-lg;
}
.van-picker__confirm {
@apply text-blue-500 font-medium;
}
.van-divider {
@apply before:bg-gray-100 after:bg-gray-100;
}
.van-field--error .van-field__control {
color: #ee0a24;
}
.van-field--error .van-field__label {
color: inherit;
}
.van-field--error .van-field__body {
@apply ring-2 ring-red-200 bg-red-50;
}
</style>

View File

@@ -0,0 +1,457 @@
<template>
<div class="p-4 max-w-3xl mx-auto min-h-screen">
<!-- 标题部分 -->
<div class="card mb-4 p-4 bg-gradient-to-r from-blue-500 to-blue-600 rounded-lg shadow-lg text-white">
<h1 class="text-2xl font-extrabold mb-2">专业报告定价配置</h1>
<p class=" opacity-90">请选择报告类型并设置定价策略助您实现精准定价</p>
</div>
<div class="mb-4">
<van-field readonly clickable name="reportType" v-model="selectedReportText" label="报告类型"
placeholder="点击选择报告" @click="showPicker = true" class="card">
<template #label>
<span class="text-blue-600 font-medium">📝 选择报告</span>
</template>
<template #right-icon>
<van-icon name="arrow-down" class="text-gray-400" />
</template>
</van-field>
<van-popup v-model:show="showPicker" position="bottom">
<van-picker :columns="reportOptions" :default-index="0" @confirm="onConfirm"
@cancel="showPicker = false" />
</van-popup>
</div>
<div v-if="selectedReportText" class="space-y-6">
<!-- 配置卡片 -->
<div class="card">
<!-- 当前报告标题 -->
<div class="flex items-center mb-6">
<van-icon name="description" class="text-blue-500 text-xl mr-2" />
<h2 class="text-xl font-semibold text-gray-800">
{{ selectedReportText }}配置
</h2>
</div>
<!-- 显示当前产品的基础成本信息 -->
<div v-if="productConfigData && productConfigData.cost_price"
class="px-4 py-2 mb-4 bg-gray-50 border border-gray-200 rounded-lg shadow-sm">
<div class="text-lg font-semibold text-gray-700">报告基础配置信息</div>
<div class="mt-1 text-sm text-gray-600">
<div>基础成本价<span class="font-medium">{{ productConfigData.cost_price }}</span> </div>
<!-- <div>区间起始价<span class="font-medium">{{ productConfigData.price_range_min }}</span> </div> -->
<div>最高设定金额上限<span class="font-medium">{{ productConfigData.price_range_max }}</span> </div>
<div>最高设定比例上限<span class="font-medium">{{ priceRatioMax }}</span> %</div>
</div>
</div>
<!-- 分隔线 -->
<van-divider :style="{ borderColor: '#e5e7eb', padding: '0 16px' }" class="my-6">
<van-icon name="exchange" class="text-gray-400 mx-2" />
<span class="text-gray-400 text-sm">成本策略配置</span>
</van-divider>
<!-- 加价金额 -->
<van-field v-model.number="configData.price_increase_amount" label="加价金额" type="number" placeholder="0"
@blur="validateDecimal('price_increase_amount')" class="custom-field"
:class="{ 'van-field--error': increaseError }">
<template #label>
<span class="text-gray-600 font-medium">🚀 加价金额</span>
</template>
<template #extra>
<span class="text-blue-500 font-medium ml-2"></span>
</template>
</van-field>
<div class="text-xs text-gray-400 mt-1">
提示最大加价金额为{{ priceIncreaseAmountMax }}<br>
说明加价金额是在基础成本价上增加的额外费用决定下级报告的最低定价您将获得所有输入的金额利润
</div>
<!-- 分隔线 -->
<van-divider :style="{ borderColor: '#e5e7eb', padding: '0 16px' }" class="my-6">
<van-icon name="exchange" class="text-gray-400 mx-2" />
<span class="text-gray-400 text-sm">定价策略配置</span>
</van-divider>
<!-- 定价区间最低 -->
<van-field v-model.number="configData.price_range_from" label="定价区间最低" type="number" placeholder="0"
@blur="() => { validateDecimal('price_range_from'); validateRange(); }" class="custom-field"
:class="{ 'van-field--error': rangeError }">
<template #label>
<span class="text-gray-600 font-medium">💰 最低金额</span>
</template>
<template #extra>
<span class="text-blue-500 font-medium ml-2"></span>
</template>
</van-field>
<div class="text-xs text-gray-400 mt-1">
提示最低金额不能低于基础最低 {{ productConfigData?.price_range_min || 0 }} + 加价金额<br>
说明设定的最低金额为定价区间的起始值若下级设定的报告金额在区间内则区间内部分将按比例获得收益
</div>
<!-- 定价区间最高 -->
<van-field v-model.number="configData.price_range_to" label="定价区间最高" type="number" placeholder="0"
@blur="() => { validateDecimal('price_range_to'); validateRange(); }" class="custom-field"
:class="{ 'van-field--error': rangeError }">
<template #label>
<span class="text-gray-600 font-medium">💰 最高金额</span>
</template>
<template #extra>
<span class="text-blue-500 font-medium ml-2"></span>
</template>
</van-field>
<div class="text-xs text-gray-400 mt-1">
提示最高金额不能超过上限{{ productConfigData?.price_range_max || 0 }}和大于最低金额{{ priceIncreaseMax
}}<br>
说明设定的最高金额为定价区间的结束值若下级设定的报告金额在区间内则区间内部分将按比例获得收益
</div>
<!-- 收取比例 -->
<van-field v-model.number="configData.price_ratio" label="收取比例" type="digit" placeholder="0"
@blur="() => { validateRatio(); }" class="custom-field" :class="{ 'van-field--error': ratioError }">
<template #label>
<span class="text-gray-600 font-medium">📈 收取比例</span>
</template>
<template #extra>
<span class="text-blue-500 font-medium ml-2">%</span>
</template>
</van-field>
<div class="text-xs text-gray-400 mt-1">
提示最大收取比例为{{ priceRatioMax }}%<br>
说明收取比例表示对定价区间内即报告金额超过最低金额小于最高金额的部分的金额按此比例进行利润分成
</div>
</div>
<!-- 保存按钮 -->
<van-button type="primary" block
class="shadow-lg bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white rounded-xl h-12"
@click="handleSubmit">
<van-icon name="success" class="mr-2" />
保存当前报告配置
</van-button>
</div>
<!-- 未选择提示 -->
<div v-else class="text-center py-12">
<van-icon name="warning" class="text-gray-400 text-4xl mb-4" />
<p class="text-gray-500">请先选择需要配置的报告类型</p>
</div>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue';
import { showToast } from 'vant';
import { settings } from 'nprogress';
// 报告类型选项
const reportOptions = [
{ text: '人事背调', value: 'backgroundcheck', id: 1 },
{ text: '老板企业报告', value: 'companyinfo', id: 2 },
{ text: '家政风险', value: 'homeservice', id: 3 },
{ text: '婚恋风险', value: 'marriage', id: 4 },
{ text: '贷前背调', value: 'preloanbackgroundcheck', id: 5 },
{ text: '租赁风险', value: 'rentalrisk', id: 6 },
{ text: '个人风险', value: 'riskassessment', id: 7 },
];
// 状态管理
const showPicker = ref(false);
const selectedReport = ref(reportOptions[0]);
const selectedReportText = ref(reportOptions[0].text);
const selectedReportId = ref(reportOptions[0].id);
const configData = ref({});
const productConfigData = ref({});
const priceIncreaseMax = ref(null);
const priceIncreaseAmountMax = ref(null);
const priceRatioMax = ref(null);
const rangeError = ref(false);
const ratioError = ref(false);
const increaseError = ref(false);
// 金额输入格式验证:确保最多两位小数
const validateDecimal = (field) => {
const value = configData.value[field];
if (value === null || value === undefined) return;
const numValue = Number(value);
if (isNaN(numValue)) {
configData.value[field] = null;
return;
}
const fixedValue = parseFloat(numValue.toFixed(2));
configData.value[field] = fixedValue;
if (field === 'price_increase_amount') {
if (fixedValue > priceIncreaseAmountMax.value) {
configData.value[field] = priceIncreaseAmountMax.value;
showToast(`加价金额最大为${priceIncreaseAmountMax.value}`);
increaseError.value = true;
setTimeout(() => {
increaseError.value = false;
}, 2000);
} else {
increaseError.value = false;
}
// 当加价金额改变后,重新验证价格区间
validateRange();
}
};
// 价格区间验证(在 @blur 中调用)
const validateRange = () => {
console.log("configData.value.price_range_from", configData.value.price_range_from)
console.log("configData.value.price_range_to", configData.value.price_range_to)
if (configData.value.price_range_from === null || configData.value.price_range_to === null) {
rangeError.value = false;
return;
}
if (isNaN(configData.value.price_range_from) || isNaN(configData.value.price_range_to)) return;
const additional = configData.value.price_increase_amount || 0;
const minAllowed = parseFloat(
(Number(productConfigData.value.cost_price) + Number(additional)).toFixed(2)
); // 使用成本价作为最小值
const maxAllowed = productConfigData.value.price_range_max; // 使用产品配置中的最大价格作为最大值
if (configData.value.price_range_from < minAllowed) {
configData.value.price_range_from = minAllowed;
showToast(`最低金额不能低于成本价 ${minAllowed}`);
rangeError.value = true;
closeRangeError()
configData.value.price_range_to = parseFloat(
(Number(configData.value.price_range_from) + Number(priceIncreaseMax.value)).toFixed(2)
);
return
}
if (configData.value.price_range_to < configData.value.price_range_from) {
showToast('最高金额不能低于最低金额');
if (configData.value.price_range_from + priceIncreaseMax.value > maxAllowed) {
configData.value.price_range_to = maxAllowed
} else {
configData.value.price_range_to = configData.value.price_range_from + priceIncreaseMax.value
}
rangeError.value = true;
closeRangeError()
return;
}
const diff = parseFloat(
(configData.value.price_range_to - configData.value.price_range_from).toFixed(2)
)
if (diff > priceIncreaseMax.value) {
showToast(`价格区间最大差值为${priceIncreaseMax.value}`);
configData.value.price_range_to = configData.value.price_range_from + priceIncreaseMax.value
closeRangeError()
return;
}
if (configData.value.price_range_to > maxAllowed) {
configData.value.price_range_to = maxAllowed;
showToast(`最高金额不能超过 ${maxAllowed}`);
closeRangeError()
}
if (!rangeError.value) {
rangeError.value = false;
}
};
// 收取比例验证(修改为保留两位小数,不再四舍五入取整)
const validateRatio = () => {
let value = configData.value.price_ratio;
if (value === null || value === undefined) return;
const numValue = Number(value);
if (isNaN(numValue)) {
configData.value.price_ratio = null;
ratioError.value = true;
return;
}
if (numValue > priceRatioMax.value) {
configData.value.price_ratio = priceRatioMax.value;
showToast(`收取比例最大为${priceRatioMax.value}%`);
ratioError.value = true;
setTimeout(() => {
ratioError.value = false;
}, 1000);
} else if (numValue < 0) {
configData.value.price_ratio = 0;
ratioError.value = true;
} else {
configData.value.price_ratio = parseFloat(numValue.toFixed(2));
ratioError.value = false;
}
};
// 获取配置
const getConfig = async () => {
try {
const { data, error } = await useApiFetch("/agent/membership/user_config?product_id=" + selectedReportId.value)
.get()
.json();
if (data.value?.code === 200) {
const respConfigData = data.value.data.agent_membership_user_config
configData.value = {
id: respConfigData.product_id,
price_range_from: respConfigData.price_range_from || null,
price_range_to: respConfigData.price_range_to || null,
price_ratio: respConfigData.price_ratio * 100 || null, // 转换为百分比
price_increase_amount: respConfigData.price_increase_amount || null,
}
console.log("configData", configData.value)
// const respProductConfigData = data.value.data.product_config
productConfigData.value = data.value.data.product_config
// 设置动态限制值
priceIncreaseMax.value = data.value.data.price_increase_max;
priceIncreaseAmountMax.value = data.value.data.price_increase_amount;
priceRatioMax.value = data.value.data.price_ratio * 100;
}
} catch (error) {
showToast('配置加载失败');
}
};
// 提交处理
const handleSubmit = async () => {
try {
if (!finalValidation()) {
return;
}
// 前端数据转换
const submitData = {
product_id: configData.value.id,
price_range_from: configData.value.price_range_from || 0,
price_range_to: configData.value.price_range_to || 0,
price_ratio: (configData.value.price_ratio || 0) / 100, // 转换为小数
price_increase_amount: configData.value.price_increase_amount || 0,
};
console.log("submitData", submitData)
const { data, error } = await useApiFetch("/agent/membership/save_user_config")
.post(submitData)
.json();
if (data.value?.code === 200) {
setTimeout(() => {
showToast('保存成功');
}, 500);
getConfig();
}
} catch (error) {
showToast('保存失败,请稍后重试');
}
};
// 最终验证函数
const finalValidation = () => {
// 校验最低金额不能为空且大于0
if (!configData.value.price_range_from || configData.value.price_range_from <= 0) {
showToast("最低金额不能为空");
return false;
}
// 校验最高金额不能为空且大于0
if (!configData.value.price_range_to || configData.value.price_range_to <= 0) {
showToast("最高金额不能为空");
return false;
}
// 校验收取比例不能为空且大于0
if (!configData.value.price_ratio || configData.value.price_ratio <= 0) {
showToast("收取比例不能为空");
return false;
}
// 验证最低金额必须小于最高金额
if (configData.value.price_range_from >= configData.value.price_range_to) {
showToast("最低金额必须小于最高金额");
return false;
}
// 验证价格区间差值不能超过最大允许差值
const finalDiff = parseFloat(
(configData.value.price_range_to - configData.value.price_range_from).toFixed(2)
);
if (finalDiff > priceIncreaseMax.value) {
showToast(`价格区间最大差值为${priceIncreaseMax.value}`);
return false;
}
// 验证最高金额不能超过产品配置中设定的上限
if (configData.value.price_range_to > productConfigData.value.price_range_max) {
showToast(`最高金额不能超过${productConfigData.value.price_range_max}`);
return false;
}
// 验证最低金额不能低于成本价+加价金额(加价金额允许为空)
const additional = configData.value.price_increase_amount || 0;
if (configData.value.price_range_from < productConfigData.value.cost_price + additional) {
showToast(`最低金额不能低于成本价${productConfigData.value.cost_price + additional}`);
return false;
}
return true;
};
// 选择器确认
const onConfirm = ({ selectedOptions }) => {
selectedReport.value = selectedOptions[0];
selectedReportText.value = selectedOptions[0].text;
selectedReportId.value = selectedOptions[0].id;
showPicker.value = false;
// 重置错误状态
rangeError.value = false;
ratioError.value = false;
increaseError.value = false;
getConfig()
};
const closeRangeError = () => {
setTimeout(() => {
rangeError.value = false;
}, 2000)
}
onMounted(() => {
getConfig();
});
</script>
<style>
.custom-field {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.custom-field .van-field__body {
@apply bg-gray-50 rounded-lg px-3 py-2;
transition: all 0.3s ease;
}
.custom-field:focus-within .van-field__body {
@apply ring-2 ring-blue-200;
}
.van-picker__toolbar {
@apply bg-gray-50 rounded-t-lg;
}
.van-picker__confirm {
@apply text-blue-500 font-medium;
}
.van-divider {
@apply before:bg-gray-100 after:bg-gray-100;
}
/* 错误状态样式 */
.van-field--error .van-field__control {
color: #ee0a24;
}
.van-field--error .van-field__label {
color: inherit;
}
.van-field--error .van-field__body {
@apply ring-2 ring-red-200 bg-red-50;
}
</style>

View File

@@ -1,5 +1,16 @@
<template>
<div class="flex flex-col box-border from-blue-100 to-white bg-gradient-to-b pt-4 flex-1 pb-4">
<!-- 顶部标题 -->
<div class="flex border-blue-300 mx-4 p-3 bg-blue-300 text-white rounded-xl mb-4 justify-between items-center">
<div class="flex items-center">
<img src="@/assets/images/ai_qinggan.png" class="w-10 h-10 rounded-xl mr-2" alt="智能助手" />
<span class="text-lg font-medium">智能助手</span>
</div>
<div @click="contactCustomerService"
class="flex items-center px-3 py-1 bg-green-500 rounded-lg cursor-pointer shadow-sm">
<span class="text-white text-sm">联系人工客服</span>
</div>
</div>
<div class="mx-4 flex flex-col flex-1 rounded-xl shadow-lg">
<!-- Chat Window -->
<div class="w-full text-center py-2 text-slate-800 font-bold text-xl bg-white rounded-t-xl">AI律师</div>
@@ -8,7 +19,7 @@
<div class=" flex-1 overflow-y-auto">
<div v-for="(message, index) in messages" :key="index" class="mb-4">
<div v-if="message.sender === 'ai'" class="flex justify-start items-start">
<img class="w-10 h-10 rounded-xl mr-2" src="@/assets/images/ai_picture.webp" alt="AI律师">
<img class="w-10 h-10 rounded-xl mr-2" src="@/assets/images/ai_qinggan.png" alt="AI律师">
<div
class="inline-block max-w-max rounded-xl bg-white p-2 text-left text-green-600 font-medium shadow-md">
<!-- If AI message, show loading or text -->
@@ -78,7 +89,7 @@ async function sendMessage() {
body: JSON.stringify({
prompt: userMessage.value,
platform_id: 2,
roleid: 1,
role_id: 2,
openid: 'openid' + localStorage.getItem("token"),
userid: 'userid' + localStorage.getItem("token"),
sessionid: sessionID.value // 可以在请求中添加 sessionID
@@ -150,7 +161,9 @@ onBeforeUnmount(() => {
}
});
const contactCustomerService = () => {
window.location.href = 'https://work.weixin.qq.com/kfid/kfc8a32720024833f57' // 跳转到客服页面
}
</script>
<style scoped>

114
src/views/Help.vue Normal file
View File

@@ -0,0 +1,114 @@
<template>
<div class="help-center">
<van-tabs v-model:active="activeTab" sticky :offset-top="46">
<van-tab v-for="(category, index) in categories" :key="index" :title="category.title" :name="category.name">
<van-cell-group inset class="help-list" size="large">
<van-cell v-for="item in category.items" :key="item.id" :title="item.title" is-link
@click="goToDetail(item.id, item.type)" class="help-item">
<template #label>
<van-tag v-if="item.type === 'guide'" type="primary" size="small"
class="guide-tag">引导指南</van-tag>
</template>
</van-cell>
</van-cell-group>
</van-tab>
</van-tabs>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const activeTab = ref('report')
const categories = [
{
title: '推广报告',
name: 'report',
items: [
{ id: 'report_guide', title: '直推报告页面引导', type: 'guide' },
{ id: 'invite_guide', title: '邀请下级页面引导', type: 'guide' },
{ id: 'report_push', title: '如何推广报告' },
{ id: 'report_efficiency', title: '报告推广效率飙升指南' },
{ id: 'report_calculation', title: '推广报告的收益是如何计算的?' },
{ id: 'report_cost', title: '推广报告的成本是如何计算的?' },
{ id: 'report_secret', title: '报告推广秘籍大公开' },
{ id: 'report_types', title: '天远数据有哪些大数据报告类型' },
]
},
{
title: '邀请下级',
name: 'invite',
items: [
{ id: 'invite_earnings', title: '邀请下级赚取收益' }
]
},
{
title: '其他',
name: 'other',
items: [
{ id: 'vip_guide', title: '如何成为VIP代理和SVIP代理?' }
]
}
]
const goToDetail = (id, type) => {
if (type === 'guide') {
router.push({
path: '/help/guide',
query: { id }
})
} else {
router.push({
path: '/help/detail',
query: { id }
})
}
}
</script>
<style lang="scss" scoped>
.help-center {
min-height: 100vh;
background-color: #f7f8fa;
.help-list {
margin-top: 12px;
.guide-tag {
margin-top: 4px;
}
}
:deep(.help-item) {
.van-cell__title {
font-size: 16px;
}
}
}
.help-detail {
padding: 20px;
&-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
h3 {
margin: 0;
font-size: 18px;
}
}
&-content {
white-space: pre-line;
line-height: 1.6;
color: #666;
}
}
</style>

80
src/views/HelpDetail.vue Normal file
View File

@@ -0,0 +1,80 @@
<template>
<div class="help-detail">
<h2>{{ currentHelp.title }}</h2>
<template v-if="Array.isArray(currentHelp.images)">
<img v-for="(image, index) in currentHelp.images" :key="index" :src="image" :alt="currentHelp.title"
class="help-image">
</template>
<img v-else-if="currentHelp.image" :src="currentHelp.image" :alt="currentHelp.title" class="help-image">
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const currentHelp = ref({
title: '',
image: '',
images: null
})
// 图片路径映射
const imageMap = {
report_calculation: '/image/help/report-calculation.jpg',
report_efficiency: '/image/help/report-efficiency.jpg',
report_cost: '/image/help/report-cost.jpg',
report_types: '/image/help/report-types.jpg',
report_push: '/image/help/report-push.jpg',
report_secret: ['/image/help/report-secret-1.jpg', '/image/help/report-secret-2.jpg'],
invite_earnings: '/image/help/invite-earnings.jpg',
direct_earnings: '/image/help/direct-earnings.jpg',
vip_guide: '/image/help/vip-guide.jpg'
}
// 标题映射
const titleMap = {
report_calculation: '推广报告的收益是如何计算的?',
report_efficiency: '报告推广效率飙升指南',
report_cost: '推广报告的成本是如何计算的?',
report_types: '天远数据有哪些大数据报告类型',
report_push: '如何推广报告',
report_secret: '报告推广秘籍大公开',
invite_earnings: '邀请下级赚取收益',
direct_earnings: '直推报告赚取收益',
vip_guide: '如何成为VIP代理和SVIP代理?'
}
onMounted(() => {
const id = route.query.id
if (id && titleMap[id]) {
currentHelp.value = {
title: titleMap[id],
image: Array.isArray(imageMap[id]) ? null : imageMap[id],
images: Array.isArray(imageMap[id]) ? imageMap[id] : null
}
}
})
</script>
<style lang="scss" scoped>
.help-detail {
min-height: 100vh;
padding: 20px;
background-color: #fff;
h2 {
margin: 0 0 20px;
font-size: 22px;
color: #323233;
font-weight: 500;
}
.help-image {
width: 100%;
border-radius: 8px;
margin-bottom: 12px;
}
}
</style>

103
src/views/HelpGuide.vue Normal file
View File

@@ -0,0 +1,103 @@
<template>
<div class="help-guide">
<div class="guide-content" @click="handleImageClick">
<img :src="currentStep.image" :alt="currentStep.title" class="guide-image">
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
const currentStepIndex = ref(0)
// 引导步骤数据
const guideSteps = {
report_guide: [
{
title: '第一步:进入直推报告页面',
image: '/image/help/report-step1.jpg'
},
{
title: '第二步:选择报告类型',
image: '/image/help/report-step2.jpg'
},
{
title: '第三步:填写推广信息',
image: '/image/help/report-step3.jpg'
},
{
title: '第四步:完成推广',
image: '/image/help/report-step4.jpg'
}
],
invite_guide: [
{
title: '第一步:进入邀请页面',
image: '/image/help/invite-step1.jpg'
},
{
title: '第二步:获取邀请码',
image: '/image/help/invite-step2.jpg'
},
{
title: '第三步:分享邀请链接',
image: '/image/help/invite-step3.jpg'
},
]
}
const currentGuide = computed(() => {
const id = route.query.id
return guideSteps[id] || []
})
const totalSteps = computed(() => currentGuide.value.length)
const currentStep = computed(() => currentGuide.value[currentStepIndex.value])
const handleImageClick = () => {
if (currentStepIndex.value < totalSteps.value - 1) {
currentStepIndex.value++
} else {
// 最后一步,返回列表页
router.back()
}
}
const onClickLeft = () => {
router.back()
}
onMounted(() => {
const id = route.query.id
if (!guideSteps[id]) {
router.back()
}
})
</script>
<style lang="scss" scoped>
.help-guide {
background-color: #666666;
display: flex;
flex-direction: column;
height: calc(100vh - 46px);
.guide-content {
flex: 1;
height: calc(100vh - 46px);
overflow: hidden;
position: relative;
.guide-image {
width: 100%;
object-fit: contain;
display: block;
}
}
}
</style>

35
src/views/Invitation.vue Normal file
View File

@@ -0,0 +1,35 @@
<template>
<div>
<img src="@/assets/images/invitation.png" alt="邀请下级">
<div @click="showQRcode = true"
class="bg-gradient-to-t from-orange-500 to-orange-300 fixed bottom-0 h-12 w-full bg-orange-400 shadow-xl text-white rounded-t-xl flex items-center justify-center font-bold">
立即邀请好友</div>
</div>
<QRcode v-model:show="showQRcode" mode="invitation" :linkIdentifier="linkIdentifier" />
</template>
<script setup>
import { storeToRefs } from "pinia";
import { aesEncrypt } from "@/utils/crypto";
import { useAgentStore } from '@/stores/agentStore'
const agentStore = useAgentStore()
const { mobile, agentID } = storeToRefs(agentStore); // 响应式解构
const showQRcode = ref(false);
const linkIdentifier = ref("")
onBeforeMount(() => {
encryptIdentifire(agentID.value, mobile.value)
})
const encryptIdentifire = (agentID, mobile) => {
const linkIdentifierJSON = {
agentID,
mobile
}
const linkIdentifierStr = JSON.stringify(linkIdentifierJSON);
const encodeData = aesEncrypt(linkIdentifierStr, "8e3e7a2f60edb49221e953b9c029ed10");
linkIdentifier.value = encodeURIComponent(encodeData)
}
</script>
<style lang="scss" scoped></style>

View File

@@ -4,19 +4,41 @@
<img src="@/assets/images/invitation_agent_apply.png" alt="邀请代理申请">
<!-- 统一状态处理容器 -->
<div class="flex flex-col items-center justify-centerx">
<!-- 申请成功状态 -->
<div v-if="showSuccessMessage" class="text-center">
<span class="text-xs text-gray-500">申请成功</span>
<div class="bg-green-100 p-1 rounded-3xl shadow-xl mt-1">
<!-- 审核中状态 -->
<div v-if="displayStatus === 0" class="text-center">
<span class="text-xs text-gray-500">您的申请正在审核中</span>
<div class="bg-gray-200 p-1 rounded-3xl shadow-xl mt-1">
<div
class="text-xl font-bold px-8 py-2 bg-gradient-to-t from-green-500 to-green-300 text-white rounded-3xl shadow-lg">
请下载"全能查APP"开始进行代理
class="text-xl font-bold px-8 py-2 bg-gray-400 text-white rounded-3xl shadow-lg cursor-not-allowed">
审核进行中
</div>
</div>
</div>
<!-- 初始状态 - 未申请 -->
<div v-else class="text-center">
<!-- 审核通过状态 -->
<div v-if="displayStatus === 1" class="text-center">
<span class="text-xs text-gray-500">您已成为认证代理方</span>
<div class="bg-green-100 p-1 rounded-3xl shadow-xl mt-1" @click="goToHome">
<div
class="text-xl font-bold px-8 py-2 bg-gradient-to-t from-green-500 to-green-300 text-white rounded-3xl shadow-lg cursor-pointer">
进入应用首页
</div>
</div>
</div>
<!-- 审核未通过状态 -->
<div v-if="displayStatus === 2" class="text-center">
<span class="text-xs text-red-500">审核未通过请重新提交</span>
<div class="bg-red-100 p-1 rounded-3xl shadow-xl mt-1" @click="agentApply">
<div
class="text-xl font-bold px-8 py-2 bg-gradient-to-t from-red-500 to-red-300 text-white rounded-3xl shadow-lg cursor-pointer">
重新提交申请
</div>
</div>
</div>
<!-- 未申请状态包含邀请状态 -->
<div v-if="displayStatus === 3" class="text-center">
<span class="text-xs text-gray-500">{{ isSelf ? '立即申请成为代理人' : '邀您注册代理人' }}</span>
<div class="bg-gray-100 p-1 rounded-3xl shadow-xl mt-1" @click="agentApply">
<div
@@ -33,36 +55,44 @@
<script setup>
import AgentApplicationForm from "@/components/AgentApplicationForm.vue";
import { aesDecrypt } from "@/utils/crypto"
import { ref, onBeforeMount } from 'vue';
import { useRoute } from 'vue-router';
const showApplyPopup = ref(false);
const showSuccessMessage = ref(false);
const route = useRoute();
const ancestor = ref("");
const isSelf = ref(false);
const showApplyPopup = ref(false)
const route = useRoute()
const router = useRouter()
import { storeToRefs } from 'pinia';
import { ref } from 'vue';
const store = useAgentStore();
const { status } = storeToRefs(store); // 响应式解构
const ancestor = ref("")
const isSelf = ref(false)
const agentApply = () => {
showApplyPopup.value = true;
showApplyPopup.value = true
}
// 计算显示状态当isSelf为false时强制显示为3
const displayStatus = computed(() => {
return isSelf.value ? 3 : status.value
})
// 跳转到首页
const goToHome = () => {
clearInterval(intervalId);
router.push('/')
}
onBeforeMount(() => {
if (route.name === "invitationAgentApplySelf") {
isSelf.value = true;
isSelf.value = true
} else {
try {
const linkIdentifier = route.params.linkIdentifier;
const decryptDataStr = aesDecrypt(decodeURIComponent(linkIdentifier), "8e3e7a2f60edb49221e953b9c029ed10");
const decryptData = JSON.parse(decryptDataStr);
ancestor.value = decryptData.mobile;
} catch (err) {
console.error('解析邀请链接失败', err);
}
const linkIdentifier = route.params.linkIdentifier
const decryptDataStr = aesDecrypt(decodeURIComponent(linkIdentifier), "8e3e7a2f60edb49221e953b9c029ed10")
const decryptData = JSON.parse(decryptDataStr)
ancestor.value = decryptData.mobile
}
const token = localStorage.getItem("token")
if (token) {
store.fetchAgentStatus();
}
});
})
const submitApplication = async (formData) => {
// 提交代理申请的数据
const { region, mobile, wechat_id, code } = formData;
@@ -73,37 +103,54 @@ const submitApplication = async (formData) => {
code,
};
if (!isSelf.value) {
postData.ancestor = ancestor.value;
postData.ancestor = ancestor.value
}
const { data, error } = await useApiFetch("/agent/apply")
.post(postData)
.json();
try {
const { data, error } = await useApiFetch("/agent/apply")
.post(postData)
.json();
if (data.value && !error.value) {
if (data.value.code === 200) {
showApplyPopup.value = false;
showToast({ message: "申请提交成功" });
// 保存token等信息
// if (data.value.data?.accessToken) {
// localStorage.setItem('token', data.value.data.accessToken);
// localStorage.setItem('refreshAfter', data.value.data.refreshAfter);
// localStorage.setItem('accessExpire', data.value.data.accessExpire);
// }
// 显示成功信息
showSuccessMessage.value = true;
} else {
// showToast({ message: data.value.message || "申请提交失败" });
if (data.value && !error.value) {
if (data.value.code === 200) {
showApplyPopup.value = false;
showToast({ message: "已提交申请" });
// refreshAgentStatus()
if (data.value.data.accessToken) {
localStorage.setItem('token', data.value.data.accessToken)
localStorage.setItem('refreshAfter', data.value.data.refreshAfter)
localStorage.setItem('accessExpire', data.value.data.accessExpire)
refreshAgentStatus()
}
} else {
console.log('申请失败', data.value);
}
} catch (err) {
console.error('申请提交出错', err);
showToast({ message: "网络错误,请稍后重试" });
}
};
let intervalId = null; // 保存定时器 ID
const refreshAgentStatus = () => {
// 当 status.value 变化时(如通过监听或事件)
if (status.value === 3) {
// 清除已有定时器(避免重复创建)
if (intervalId) clearInterval(intervalId);
intervalId = setInterval(() => {
// 每次执行前检查状态是否仍为 3
if (status.value !== 3) {
clearInterval(intervalId);
intervalId = null;
return;
}
store.fetchAgentStatus();
}, 2000);
} else {
// 状态不是 3 时主动清除定时器
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
}
}
</script>
<style lang="scss" scoped></style>

View File

@@ -1,7 +1,6 @@
<template>
<div class="p-4 ">
<div class="card">
<div class="min-h-screen p-4 promote">
<div class="mb-4 card !bg-gradient-to-b from-orange-200 to-orange-200/80">
<div class="">
<div class="text-lg font-bold text-orange-500">直推用户查询</div>
<div class="font-bold text-orange-400 mt-1">
@@ -11,71 +10,261 @@
<div class="mt-6">
<div class="mt-2 text-gray-600 bg-orange-100 rounded-xl px-4 py-2">
在下方 自定义价格 处选择报告类型设置客户查询价即可了解推广收益及我的成本价
在下方 自定义价格 处选择报告类型设置客户查询价即可立即推广
</div>
</div>
</div>
<div class="card mt-6">
<div class="">
<h2 class="text-xl font-semibold mb-2">生成推广码</h2>
<van-cell-group inset>
<van-field v-model="pickerFieldVal" is-link readonly label="报告类型" placeholder="请选择报告类型"
@click="showPicker = true" />
<van-popup v-model:show="showPicker" destroy-on-close round position="bottom">
<van-picker :model-value="selectedReportType" :columns="reportTypes"
@cancel="showPicker = false" @confirm="onConfirm" />
</van-popup>
<van-field v-model="clientPrice" type="number" label="客户查询价" placeholder="请输入价格" />
<div class="flex items-center justify-between my-2">
<div class="text-sm text-gray-500">推广收益为 {{ promotionRevenue }} </div>
<div class="text-sm text-gray-500">我的成本为 {{ costPrice }} </div>
</div>
</van-cell-group>
</div>
<VipBanner />
<!-- 判断是否是代理 -->
<div>
<div class="card mb-4">
<div class="">
<h2 class="text-xl font-semibold mb-2">生成推广码</h2>
<van-cell-group inset>
<!-- 报告类型 -->
<van-field v-model="pickerFieldText" is-link readonly label="报告类型" placeholder="请选择报告类型"
@click="showTypePicker = true" />
<van-popup v-model:show="showTypePicker" destroy-on-close round position="bottom">
<van-picker :model-value="selectedReportType" :columns="reportTypes"
@cancel="showTypePicker = false" @confirm="onConfirmType" />
</van-popup>
<!-- 定价 -->
<van-field v-model="clientPrice" is-link readonly label="客户查询价" placeholder="请输入价格"
@click="showPricePicker = true" />
<PriceInputPopup v-model:show="showPricePicker" :default-price="clientPrice"
:product-config="pickerProductConfig" @change="onPriceChange" />
<div class="flex items-center justify-between my-2">
<div class="text-sm text-gray-500">推广收益为 <span class="text-orange-500">{{ promotionRevenue
}}</span> </div>
<div class="text-sm text-gray-500">我的成本为 <span class="text-orange-500">{{ costPrice
}}</span> </div>
</div>
</van-cell-group>
</div>
<div class="mt-6">
<van-button type="primary" class="w-full" @click="generatePromotionCode">点击立即推广</van-button>
<div class="mt-6">
<van-button type="primary" class="w-full" @click="generatePromotionCode">点击立即推广</van-button>
</div>
</div>
</div>
<!-- 如果不是代理展示根据status显示不同内容 -->
<!-- <div>
<div v-if="status === 0" class="card mt-6">
<div class="font-semibold text-lg text-gray-700">申请审核中</div>
<div class="text-sm text-gray-500 mt-4 mb-8">
您的申请正在审核中请耐心等待
</div>
</div>
<div v-else-if="status === 2" class="card mt-6">
<div class="font-semibold text-lg text-gray-700">申请未通过</div>
<div class="text-sm text-gray-500 mt-4 mb-8">
很抱歉您的代理申请未通过请检查您的信息或重新申请
</div>
<van-button type="primary" round class="w-full" @click="showApplyPopup = true">
申请成为代理
</van-button>
</div>
<div v-else-if="status === 3" class="card mt-6">
<div class="font-semibold text-lg text-gray-700">未申请成为代理</div>
<div class="text-sm text-gray-500 mt-4 mb-8">
您还没有申请成为代理立即申请即可开始推广
</div>
<van-button type="primary" round class="w-full" @click="showApplyPopup = true">
申请成为代理
</van-button>
</div>
</div>
<AgentApplicationForm v-model:show="showApplyPopup" @submit="submitApplication"
@close="showApplyPopup = false" /> -->
<QRcode v-model:show="showQRcode" :linkIdentifier="linkIdentifier" />
</div>
<PromoteQRcode v-model:show="showQRcode" />
</template>
<script setup>
import { ref, computed } from 'vue';
import PriceInputPopup from '@/components/PriceInputPopup.vue';
import VipBanner from '@/components/VipBanner.vue';
const reportTypes = [
{ text: '个人风险报告', value: 'personalRisk' },
{ text: '婚恋风险', value: 'marriageRisk' },
{ text: '家政风险', value: 'domesticService' },
{ text: '租赁风险', value: 'rentalRisk' },
{ text: '人事背调', value: 'hrBackgroundCheck' },
{ text: '老板企业报告', value: 'enterpriseReport' },
{ text: '贷前背调', value: 'preLoanBackgroundCheck' }
{ text: '人事背调', value: 'backgroundcheck', id: 1 },
{ text: '老板企业报告', value: 'companyinfo', id: 2 },
{ text: '家政风险', value: 'homeservice', id: 3 },
{ text: '婚恋风险', value: 'marriage', id: 4 },
{ text: '贷前背调', value: 'preloanbackgroundcheck', id: 5 },
{ text: '租赁风险', value: 'rentalrisk', id: 6 },
{ text: '个人风险', value: 'riskassessment', id: 7 },
];
const showPicker = ref(false);
const pickerFieldVal = ref('')
const showTypePicker = ref(false);
const showApplyPopup = ref(false); // 用来控制申请代理弹窗的显示
const showPricePicker = ref(false);
const pickerFieldText = ref('')
const pickerFieldVal = ref(null)
const pickerProductConfig = ref(null)
const selectedReportType = ref([]);
const onConfirm = ({ selectedValues, selectedOptions }) => {
showPicker.value = false;
selectedReportType.value = selectedValues;
pickerFieldVal.value = selectedOptions[0].text;
};
const clientPrice = ref(null);
const productConfig = ref(null);
const linkIdentifier = ref("")
const clientPrice = ref(49.9);
const costPrice = ref(10.31);
// const costPrice = computed(() => {
// if (!pickerProductConfig.value) return 0.00
// // 平台定价成本
// let platformPricing = 0
// if (clientPrice.value > pickerProductConfig.value.p_pricing_standard) {
// platformPricing = (clientPrice.value - pickerProductConfig.value.p_pricing_standard) * pickerProductConfig.value.p_overpricing_ratio
// }
// return (pickerProductConfig.value.cost_price + platformPricing).toFixed(2)
// })
const costPrice = computed(() => {
if (!pickerProductConfig.value) return 0.00
// 平台定价成本
let platformPricing = 0
platformPricing += pickerProductConfig.value.cost_price
if (clientPrice.value > pickerProductConfig.value.p_pricing_standard) {
platformPricing += (clientPrice.value - pickerProductConfig.value.p_pricing_standard) * pickerProductConfig.value.p_overpricing_ratio
}
if (pickerProductConfig.value.a_pricing_standard > platformPricing && pickerProductConfig.value.a_pricing_end > platformPricing && pickerProductConfig.value.a_overpricing_ratio > 0) {
if (clientPrice.value > pickerProductConfig.value.a_pricing_standard) {
if (clientPrice.value > pickerProductConfig.value.a_pricing_end) {
platformPricing += (pickerProductConfig.value.a_pricing_end - pickerProductConfig.value.a_pricing_standard) * pickerProductConfig.value.a_overpricing_ratio
} else {
platformPricing += (clientPrice.value - pickerProductConfig.value.a_pricing_standard) * pickerProductConfig.value.a_overpricing_ratio
}
}
}
return safeTruncate(platformPricing)
})
const promotionRevenue = computed(() => {
// 计算推广收益 (客户查询价 - 我的成本)
return (clientPrice.value - costPrice.value).toFixed(2);
return safeTruncate(clientPrice.value - costPrice.value)
});
const showQRcode = ref(false)
const showQRcode = ref(false);
function safeTruncate(num, decimals = 2) {
if (isNaN(num) || !isFinite(num)) return "0.00";
const generatePromotionCode = () => {
const factor = 10 ** decimals;
const scaled = Math.trunc(num * factor);
const truncated = scaled / factor;
return truncated.toFixed(decimals);
}
const generatePromotionCode = async () => {
if (selectedReportType.value.length === 0) {
showToast({ message: '请选择报告类型' });
return;
}
if (!clientPrice.value) {
showToast({ message: '请输入查询价格' });
return;
}
const { data, error } = await useApiFetch("/agent/generating_link")
.post({ product: pickerFieldVal.value, price: clientPrice.value })
.json()
if (data.value && !error.value) {
if (data.value.code === 200) {
linkIdentifier.value = data.value.data.link_identifier
} else {
console.log("Error fetching agent info", data.value);
}
}
if (!linkIdentifier.value) return
showQRcode.value = true;
};
</script>
onMounted(() => {
getPromoteConfig();
// getAgentInfo();
});
const SelectTypePicker = (reportType) => {
selectedReportType.value = [reportType];
pickerFieldText.value = reportType.text;
pickerFieldVal.value = reportType.value;
for (let i of productConfig.value) {
if (i.product_id === reportType.id) {
pickerProductConfig.value = i
clientPrice.value = i.cost_price
}
}
};
const getPromoteConfig = async () => {
const { data, error } = await useApiFetch("/agent/product_config")
.get()
.json()
if (data.value && !error.value) {
if (data.value.code === 200) {
productConfig.value = data.value.data.AgentProductConfig;
SelectTypePicker(reportTypes[0])
} else {
console.log("Error fetching agent info", data.value);
}
}
}
const onPriceChange = (price) => {
clientPrice.value = price
}
// const getAgentInfo = async () => {
// const { data, error } = await useApiFetch("/agent/info")
// .get()
// .json()
// if (data.value && !error.value) {
// if (data.value.code === 200) {
// isAgent.value = data.value.data.is_agent; // 判断是否是代理
// status.value = data.value.data.status; // 获取代理状态
// agentID.value = data.value.data.agent_id
// } else {
// console.log("Error fetching agent info", data.value);
// }
// }
// };
const onConfirmType = ({ selectedValues, selectedOptions }) => {
SelectTypePicker(selectedOptions[0])
showTypePicker.value = false;
};
const submitApplication = async (formData) => {
// 提交代理申请的数据
const { region, mobile, wechat_id, code } = formData;
const { data, error } = await useApiFetch("/agent/apply")
.post({ region, mobile, wechat_id, code })
.json();
if (data.value && !error.value) {
if (data.value.code === 200) {
showApplyPopup.value = false;
// 这里可以提示成功,或者刷新代理状态
showToast({ message: "已提交申请" });
} else {
console.log('申请失败', data.value);
}
}
};
</script>
<style scoped>
/* 自定义样式可以添加在这里 */
/* .promote {
background-color: #ffeee0;
background-image: linear-gradient(45deg,
rgba(255, 235, 205, 0.3) 25%,
transparent 25%,
transparent 50%,
rgba(255, 235, 205, 0.3) 50%,
rgba(255, 235, 205, 0.3) 75%,
transparent 75%,
transparent);
background-size: 40px 40px;
min-height: 100vh;
}
*/
</style>

View File

@@ -568,10 +568,10 @@ onUnmounted(() => {
</van-popup>
<Payment v-model="showPayment" :data="featureData" :id="queryId" @close="showPayment = false" />
<RecordFooter v-if="!webviewEnv" />
<!-- <div
<div
class=" fixed right-2 top-3/4 px-4 py-2 text-sm bg-blue-400 rounded-xl cursor-pointer text-white font-bold shadow active:bg-blue-500">
历史查询
</div> -->
</div>
</template>
<style scoped>

View File

@@ -1,65 +1,342 @@
<template>
<div class="p-6">
<!-- 提现信息 -->
<div class="bg-white bg-opacity-90 shadow-lg rounded-xl p-6 mb-6">
<div class="flex justify-between items-center">
<span class="text-xl text-gray-900 font-semibold">提现到</span>
<span class="text-lg text-blue-600">银行账户</span>
</div>
<div class="flex justify-between items-center mt-4">
<span class="text-sm text-gray-600">银行卡号</span>
<span class="text-sm text-blue-500">UPCash</span>
<button class="text-sm text-blue-500">修改</button>
</div>
</div>
<div class="p-4 bg-gradient-to-b from-blue-50/30 to-gray-50 min-h-screen">
<div> <!-- 提现卡片 -->
<div class="rounded-xl shadow-lg bg-gradient-to-r from-blue-50/70 to-blue-100/50 p-6 mb-4">
<div class="flex items-center mb-6">
<van-icon name="alipay" class="text-blue-500 text-xl mr-2" />
<h1 class="text-xl font-bold text-gray-800">支付宝提现</h1>
</div>
<!-- 提现金额 -->
<div class="bg-white bg-opacity-90 shadow-lg rounded-xl p-6 mb-6">
<div class="flex justify-between items-center">
<span class="text-xl text-gray-900 font-semibold">提现金额</span>
<span class="text-3xl text-blue-600 font-bold">¥ 0.0</span>
</div>
<div class="mt-4 text-sm text-gray-600">
<p>可提现金额 0.0 </p>
<p class="mt-2">每天最多提现2次最低提现金额20元提现金额超过1000元会进行人工审核</p>
</div>
<div class="mt-6 flex justify-end">
<button class="bg-blue-600 hover:bg-blue-500 text-white py-2 px-6 rounded-lg shadow-md">
全部提现
</button>
</div>
</div>
<!-- 支付宝账号 -->
<div class="mb-6">
<label class="text-sm text-gray-600 mb-2 block">支付宝账号</label>
<van-field v-model="alipayAccount" placeholder="请输入支付宝账号"
class="flex items-center rounded-lg bg-white/90 backdrop-blur-sm shadow-sm"
:rules="[{ required: true, message: ' ' }]">
<template #left-icon>
<van-icon name="phone-o" class="text-gray-500" />
</template>
</van-field>
<small class="text-gray-400 text-xs mt-1 block">可填写支付宝账户绑定的手机号</small>
<!-- 短信验证码 -->
<div class="bg-white bg-opacity-90 shadow-lg rounded-xl p-6 mb-6">
<div class="flex justify-between items-center">
<span class="text-lg text-gray-900">短信验证码</span>
<button class="text-sm text-blue-500">手机为优先查看注册号码</button>
</div>
<div class="mt-4">
<input type="text" placeholder="请输入短信验证码"
class="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" />
</div>
<div class="mt-4 flex justify-between items-center">
<button class="bg-blue-600 text-white py-2 px-6 rounded-lg shadow-md flex items-center space-x-2">
<van-icon name="refresh" size="20" />
<span>获取验证码</span>
</button>
</div>
</div>
</div>
<!-- 支付宝实名姓名 -->
<div class="mb-6">
<label class="text-sm text-gray-600 mb-2 block">实名姓名</label>
<van-field v-model="realName" placeholder="请输入支付宝认证姓名"
class="flex items-center rounded-lg bg-white/90 backdrop-blur-sm shadow-sm" :rules="[{
required: true,
message: ' ',
validator: (val) => /^[\u4e00-\u9fa5]{2,4}$/.test(val)
}]">
<template #left-icon>
<van-icon name="contact-o" class="text-gray-500" />
</template>
</van-field>
<small class="text-gray-400 text-xs mt-1 block">请填写支付宝账户认证的真实姓名</small>
<!-- 确认提现 -->
<div class="mt-6">
<button class="bg-blue-600 hover:bg-blue-500 text-white py-3 px-6 rounded-lg w-full shadow-md">
确认提现
</button>
</div>
<!-- 提现金额 -->
<div class="mb-4">
<label class="text-sm text-gray-600 mb-2 block">提现金额</label>
<van-field v-model.number="amount" type="number" placeholder="请输入提现金额"
class="flex items-center rounded-lg bg-white/90 backdrop-blur-sm shadow-sm"
:rules="[{ required: true, message: ' ' }, { validator: validateAmount, message: ' ' }]">
<template #left-icon>
<van-icon name="gold-coin-o" class="text-gray-500" />
</template>
<template #right-icon>
</template>
<template #button>
<van-button size="small" type="primary"
class="bg-gradient-to-r from-blue-500/20 to-blue-400/20 text-blue-600 rounded-full px-3 shadow-sm"
@click="fillMaxAmount">
全部提现
</van-button>
</template>
</van-field>
</div>
<!-- 金额提示 -->
<div class="text-sm text-gray-500 mb-6">
可提现金额<span class="text-blue-600 font-semibold">¥{{ availableAmount }}</span>
</div>
<!-- 提现规则 -->
<div class="bg-blue-50/60 p-4 rounded-xl backdrop-blur-sm">
<div class="flex items-center text-sm text-blue-500 mb-2">
<van-icon name="warning" class="mr-1" />提现须知
</div>
<ul class="text-xs text-gray-600 space-y-1">
<li>· 每日限提现1次最低50元</li>
<li>· 超过800元需人工审核1-3个工作日</li>
<li>· 到账时间24小时内</li>
</ul>
</div>
</div>
<!-- 提交按钮 -->
<van-button type="primary" block :loading="isSubmitting"
class="bg-gradient-to-r from-blue-500 to-blue-400 text-white rounded-xl shadow-lg h-12 font-bold text-base"
@click="handleSubmit">
立即提现
</van-button>
</div>
<van-popup v-model:show="showStatusPopup" round position="center"
:style="{ width: '85%', borderRadius: '20px' }" :overlay-style="{ backgroundColor: 'rgba(0,0,0,0.4)' }">
<div class="p-8 bg-gradient-to-b from-white to-blue-50/30 relative">
<!-- 状态内容 -->
<div class="text-center space-y-5">
<!-- 状态图标 -->
<div class="relative inline-block">
<div class="absolute inset-0 bg-gradient-to-r opacity-20 rounded-full animate-pulse blur-sm"
:class="statusBg[status]"></div>
<van-icon :name="statusIcon[status]" size="56" class="p-1 rounded-full border-[3px]"
:class="statusIconClass[status]" />
</div>
<!-- 状态文案 -->
<div>
<h2 class="text-xl font-semibold mb-1" :class="statusTextColors[status]">
{{ statusMessages[status] }}
</h2>
<template v-if="status === 2">
<p class="text-sm text-gray-500">
已向 <span class="text-blue-500">{{ alipayAccount }}</span> 转账
</p>
<p class="text-2xl font-bold text-green-600 mt-2">¥{{ amount }}</p>
</template>
<template v-if="status === 3">
<p class="text-red-500 text-sm px-4">{{ failMsg }}</p>
</template>
</div>
<!-- 进度条处理中状态 -->
<van-progress v-if="status === 1" :percentage="60" stroke-width="8"
color="linear-gradient(to right, #3b82f6, #60a5fa)" track-color="#e0f2fe"
class="!rounded-full" />
<!-- 辅助文案 -->
<div class="text-xs text-gray-400 space-y-1.5">
<template v-if="status === 2">
<p>预计24小时内到账</p>
<p>可在支付宝账单中查看详情</p>
</template>
<template v-if="status === 1">
<p>您的申请已进入处理队列</p>
<p>5分钟后结果在提现记录种查看</p>
</template>
</div>
<!-- 操作按钮 -->
<van-button block round size="small" :color="statusButtonColor[status]"
class="mt-4 h-11 font-medium shadow-sm" @click="handlePopupAction">
{{ status === 1 ? '知道了' : status === 2 ? '完成' : '重新提现' }}
</van-button>
</div>
</div>
</van-popup>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import { showToast } from 'vant';
// 状态管理
const status = ref(null);
const failMsg = ref('');
const isSubmitting = ref(false);
const showStatusPopup = ref(false);
// 样式配置
const statusIcon = {
1: 'clock',
2: 'checked',
3: 'close'
};
const statusIconClass = {
1: 'text-blue-400 border-blue-100 bg-blue-50',
2: 'text-green-500 border-green-100 bg-green-50',
3: 'text-red-500 border-red-100 bg-red-50'
};
const statusBg = {
1: 'from-blue-100 to-blue-50',
2: 'from-green-100 to-green-50',
3: 'from-red-100 to-red-50'
};
const statusTextColors = {
1: 'text-blue-600',
2: 'text-green-600',
3: 'text-red-600'
};
const statusButtonColor = {
1: '#e0f2fe',
2: '#4ade80',
3: '#fca5a5'
};
const statusMessages = {
1: '提现申请处理中,请稍后再查询结果',
2: '提现成功',
3: '提现失败'
};
// 表单数据
const alipayAccount = ref('');
const amount = ref(null);
const availableAmount = ref(null);
const realName = ref('');
const getData = async () => {
const { data: res, error } = await useApiFetch("/agent/revenue")
.get()
.json();
if (res.value?.code === 200 && !error.value) {
availableAmount.value = res.value.data.balance;
}
};
onBeforeMount(() => {
getData();
});
// 表单验证
const validateAmount = (val) => {
const num = Number(val);
return num >= 50 && num <= availableAmount.value;
};
const validateForm = () => {
if (!realName.value.trim()) {
showToast('请输入账户实名姓名');
return false;
}
if (!/^[\u4e00-\u9fa5]{2,4}$/.test(realName.value)) {
showToast('请输入2-4位中文姓名');
return false;
}
if (!alipayAccount.value.trim()) {
showToast('请输入支付宝账号');
return false;
}
const amountNum = Number(amount.value);
if (!amount.value || isNaN(amountNum)) {
showToast('请输入有效金额');
return false;
}
if (amountNum < 50) {
showToast('提现金额不能低于50元');
return false;
}
if (amountNum > availableAmount.value) {
showToast('超过可提现金额');
return false;
}
return true;
};
const handleSubmit = async () => {
// 先进行表单验证
if (!validateForm()) return;
isSubmitting.value = true;
try {
const { data, error } = await useApiFetch("/agent/withdrawal")
.post({ payee_account: alipayAccount.value, amount: amount.value, payee_name: realName.value })
.json();
if (data.value?.code === 200) {
status.value = data.value.data.status;
showStatusPopup.value = true;
if (status.value === 3) {
failMsg.value = data.value.data.fail_msg;
}
}
} catch {
} finally {
isSubmitting.value = false;
}
};
// 弹窗操作
const handlePopupAction = () => {
if (status.value === 3) {
showStatusPopup.value = false;
resetForm();
} else {
showStatusPopup.value = false;
if (status.value === 2) resetPage();
}
};
// 填充最大金额
const fillMaxAmount = () => {
amount.value = availableAmount.value;
};
// 重置页面
const resetForm = () => {
status.value = null;
alipayAccount.value = '';
amount.value = '';
realName.value = '';
};
</script>
<style scoped>
/* 如果需要额外的样式,可以在这里添加 */
<style>
/* 自定义表单样式 */
.van-field__control {
@apply py-1 px-4 text-gray-800;
}
.van-field__error-message {
@apply mt-1;
}
.van-button--disabled {
@apply opacity-60 cursor-not-allowed;
}
/* 弹窗入场动画 */
.van-popup {
transition: transform 0.4s cubic-bezier(0.22, 0.61, 0.36, 1), opacity 0.3s ease;
}
.van-popup-enter-active,
.van-popup-leave-active {
transition: opacity 0.3s;
}
.van-popup-enter-from,
.van-popup-leave-to {
opacity: 0;
}
.van-popup-enter-active {
transform: scale(0.95);
}
.van-popup-enter-to {
transform: scale(1);
}
/* 状态图标动画 */
.animate-pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes pulse {
50% {
opacity: 0.5;
}
}
</style>

View File

@@ -0,0 +1,160 @@
<template>
<div class="min-h-screen bg-gray-50">
<!-- 提现记录列表 -->
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
<div v-for="(item, index) in data.list" :key="index" class="mx-4 my-2 bg-white rounded-lg p-4 shadow-sm">
<div class="flex justify-between items-center mb-2">
<span class="text-gray-500 text-sm">{{ item.create_time || '-' }}</span>
<span class="font-bold" :class="getAmountColor(item.status)">{{ item.amount.toFixed(2) }}</span>
</div>
<div class="flex items-center mb-2">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium"
:class="getStatusStyle(item.status)">
<span class="w-2 h-2 rounded-full mr-1" :class="getDotColor(item.status)"></span>
{{ statusToChinese(item.status) }}
</span>
</div>
<div class="text-xs text-gray-500">
<div v-if="item.payee_account">收款账户{{ maskName(item.payee_account) }}</div>
<div v-if="item.remark">备注{{ item.remark }}</div>
</div>
</div>
</van-list>
</div>
</template>
<script setup>
// 状态映射配置
const statusConfig = {
1: {
chinese: '处理中',
color: {
bg: 'bg-yellow-100',
text: 'text-yellow-800',
dot: 'bg-yellow-500',
amount: 'text-yellow-500'
}
},
2: {
chinese: '提现成功',
color: {
bg: 'bg-green-100',
text: 'text-green-800',
dot: 'bg-green-500',
amount: 'text-green-500'
}
},
3: {
chinese: '提现失败',
color: {
bg: 'bg-red-100',
text: 'text-red-800',
dot: 'bg-red-500',
amount: 'text-red-500'
}
}
}
const page = ref(1)
const pageSize = ref(10)
const data = ref({
total: 0,
list: []
})
const loading = ref(false)
const finished = ref(false)
// 账户脱敏处理
const maskName = (name) => {
if (!name || typeof name !== 'string') return ''
if (name.length <= 7) return name
return name.substring(0, 3) + '****' + name.substring(7)
}
// 状态转中文
const statusToChinese = (status) => {
return statusConfig[status]?.chinese || '未知状态'
}
// 获取状态样式
const getStatusStyle = (status) => {
const config = statusConfig[status] || {}
return `${config.color?.bg || 'bg-gray-100'} ${config.color?.text || 'text-gray-800'}`
}
// 获取小圆点颜色
const getDotColor = (status) => {
return statusConfig[status]?.color.dot || 'bg-gray-500'
}
// 获取金额颜色
const getAmountColor = (status) => {
return statusConfig[status]?.color.amount || 'text-gray-500'
}
// 加载更多数据
const onLoad = async () => {
if (!finished.value) {
await getData()
}
}
// 获取数据(修改分页逻辑)
const getData = async () => {
try {
loading.value = true
const { data: res, error } = await useApiFetch(
`/agent/withdrawal?page=${page.value}&page_size=${pageSize.value}`
).get().json()
if (res.value?.code === 200 && !error.value) {
// 保留首次加载数据
if (page.value === 1) {
data.value = res.value.data
} else {
data.value.list.push(...res.value.data.list)
}
// 更新分页状态
page.value++
// 判断是否加载完成
if (data.value.list.length >= res.value.data.total ||
res.value.data.list.length < pageSize.value) {
finished.value = true
}
}
} finally {
loading.value = false
}
}
// 初始化加载
onMounted(async () => {
// 重置分页状态
page.value = 1
finished.value = false
await getData()
})
</script>
<style scoped>
/* 保持原有样式不变 */
.list-enter-active {
transition: all 0.3s ease;
}
.list-enter-from {
opacity: 0;
transform: translateY(20px);
}
:deep(.van-list__finished-text) {
@apply py-4 text-gray-400 text-sm;
}
:deep(.van-list__loading) {
@apply py-4;
}
</style>

View File

@@ -8,14 +8,34 @@ import indexIcon5 from '@/assets/images/index_icon_5.png'
import indexIcon6 from '@/assets/images/index_icon_6.png'
import indexIcon7 from '@/assets/images/index_icon_7.png'
function toInquire(name) {
if (name === 'more') {
window.location.href = 'https://www.tianyuancha.cn?_um_campaign=67bfea1c9a16fe6dcd53b9a4&_um_channel=67bfea1d9a16fe6dcd53b9a5'
if (name === 'more' || name === "marriage") {
showConfirmDialog({
title: name === 'marriage' ? '婚恋风险' : '更多功能',
message:
`是否前往天远查查询${name === 'marriage' ? '婚恋风险' : '更多功能'}页面?`,
})
.then(() => {
window.location.href = 'https://www.tianyuancha.cn?_um_campaign=67bfea1c9a16fe6dcd53b9a4&_um_channel=67bfea1d9a16fe6dcd53b9a5'
})
.catch(() => {
});
return
}
router.push(`/inquire/${name}`)
}
function toInvitation() {
router.push({ name: "invitation" })
}
const toPromote = () => {
router.push("/promote")
router.push({ name: "promote" })
}
function toAgentApply() {
router.push({ name: "invitationAgentApplySelf" })
}
const toHelp = () => {
router.push("/help")
}
const services = ref([
{
@@ -89,8 +109,8 @@ function toHistory() {
<div class="relative p-4">
<img class="h-full w-full rounded-xl overflow-hidden" src="@/assets/images/banner.png" />
</div>
<!-- <div>
<div class="flex items-center justify-around gap-4 px-6 pb-1">
<div>
<div class="flex items-center justify-around gap-3 px-6 pb-1">
<div class="" @click="toPromote">
<div
class="h-16 w-16 p-2 bg-gradient-to-b from-white to-blue-100/10 rounded-full shadow-lg flex items-center justify-center">
@@ -98,29 +118,30 @@ function toHistory() {
</div>
<div class="text-center mt-1 font-bold">直推报告</div>
</div>
<div class="">
<div class="" @click="toInvitation">
<div
class="h-16 w-16 p-2 bg-gradient-to-b from-white to-blue-100/10 rounded-full shadow-lg flex items-center justify-center">
<img src="@/assets/images/icon_xj.svg" alt="邀请下级" class="w-12 h-12" />
</div>
<div class="text-center mt-1 font-bold">邀请下级</div>
</div>
<div class="">
<div class="" @click="toHelp">
<div
class="h-16 w-16 p-2 bg-gradient-to-b from-white to-blue-100/10 rounded-full shadow-lg flex items-center justify-center">
<img src="@/assets/images/icon_xj.svg" alt="邀请下级" class="w-12 h-12" />
<img src="@/assets/images/icon_bz.svg" alt="帮助中心" class="w-12 h-12" />
</div>
<div class="text-center mt-1 font-bold">邀请下级</div>
<div class="text-center mt-1 font-bold">帮助中心</div>
</div>
<div class="">
<div class="" @click="toHistory">
<div
class="h-16 w-16 p-2 bg-gradient-to-b from-white to-blue-100/10 rounded-full shadow-lg flex items-center justify-center">
<img src="@/assets/images/icon_xj.svg" alt="邀请下级" class="w-12 h-12" />
<img src="@/assets/images/icon_bg.svg" alt="我的报告" class="w-12 h-12" />
</div>
<div class="text-center mt-1 font-bold">邀请下级</div>
<div class="text-center mt-1 font-bold">我的报告</div>
</div>
</div>
</div> -->
</div>
<div class="relative p-4 pb-4 pt-2">
<div class="grid grid-cols-2 gap-3">

View File

@@ -38,7 +38,7 @@ export default defineConfig({
"@vueuse/core", // 自动引入 VueUse 中的工具函数(可选)
],
dts: "src/auto-imports.d.ts", // 生成类型定义文件(可选)
dirs: ["src/composables", "src/stores", "src/components"],
dirs: ["src/composables", "src/stores", "src/components", "src/stores"],
resolvers: [VantResolver()],
}),
Components({

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long