This commit is contained in:
liangzai 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; //
// 200011
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
}
// isSelffalse3
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